From ce9eb473514d677e4232247983155e180af01f3b Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 3 Nov 2016 18:51:30 +0100 Subject: [PATCH 01/38] Initial state of the file manager app --- www/file/index.html | 41 +++++++++++++++++++ www/file/inner.html | 54 +++++++++++++++++++++++++ www/file/main.js | 99 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 www/file/index.html create mode 100644 www/file/inner.html create mode 100644 www/file/main.js diff --git a/www/file/index.html b/www/file/index.html new file mode 100644 index 000000000..1f5d4f4df --- /dev/null +++ b/www/file/index.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/www/file/inner.html b/www/file/inner.html new file mode 100644 index 000000000..1a4772167 --- /dev/null +++ b/www/file/inner.html @@ -0,0 +1,54 @@ + + + + + + + + + +
+
+
+
+ + + diff --git a/www/file/main.js b/www/file/main.js new file mode 100644 index 000000000..05d6fba22 --- /dev/null +++ b/www/file/main.js @@ -0,0 +1,99 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/customize/messages.js?app=pad', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/hyperjson/hyperjson.js', + '/common/toolbar.js', + '/common/cursor.js', + '/bower_components/chainpad-json-validator/json-ot.js', + '/common/TypingTests.js', + 'json.sortify', + '/bower_components/textpatcher/TextPatcher.amd.js', + '/common/cryptpad-common.js', + '/common/visible.js', + '/common/notify.js', + '/bower_components/file-saver/FileSaver.min.js', + '/bower_components/diff-dom/diffDOM.js', + '/bower_components/jquery/dist/jquery.min.js', + '/customize/pad.js' +], function (Messages, Crypto, realtimeInput, Hyperjson, + Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, + Visible, Notify) { + var $ = window.jQuery; + var saveAs = window.saveAs; + var $iframe = $('#pad-iframe').contents(); + var ifrw = $('#pad-iframe')[0].contentWindow; + var files = { + root: { + "Directory 1": { + "Dir A": { + "File a": "#hash_a", + "File b": "#hash_b" + }, + "Dir B": {}, + "File A": "#hash_A" + }, + "Directory 2": { + "File B": "#hash_B", + "File C": "#hash_C" + } + }, + trash: { + "File Z": "#hash_Z" + } + }; + + var $tree = $iframe.find("#tree"); + var $content = $iframe.find("#content"); + var $folderIcon = $('', { + "class": "fa fa-folder folder", + style: "font-family: FontAwesome" + }); + var $fileIcon = $('', { + "class": "fa fa-file file", + style: "font-family: FontAwesome" + }); + + var displayDirectory = function (name, root) { + $content.html(""); + var $title = $('

').text(name); + var $dirContent = $('
', {id: "folderContent"}); + var $list = $('
    ').appendTo($dirContent); + // display sub directories + Object.keys(root).forEach(function (key) { + if (typeof(root[key]) === "string") { return; } + var $name = $('').text(key).prepend($folderIcon.clone()); + var $element = $('
  • ').append($name).click(function () { + displayDirectory(key, root[key]); + }); + $element.appendTo($list); + }); + // display files + Object.keys(root).forEach(function (key) { + if (typeof(root[key]) !== "string") { return; } + var $name = $('').text(key).prepend($fileIcon.clone()); + var $element = $('
  • ').append($name).click(function () { + window.location.hash = root[key]; + }); + $element.appendTo($list); + }); + $content.append($title).append($dirContent); + }; + + var createTree = function (root, $container) { + if (Object.keys(root).length === 0) { return; } + var $list = $('
      ').appendTo($container); + Object.keys(root).forEach(function (key) { + // Do not display files in the menu + if (typeof(root[key]) === "string") { return; } + var $name = $('').text(key).click(function () { + displayDirectory(key, root[key]); + }); + var $element = $('
    • ').append($name); + $element.appendTo($list); + createTree(root[key], $element[0]); + }); + }; + createTree(files.root, $tree); +}); From 8e1bff706b60c31233e8b996e878cbb0d8b3b90d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 4 Nov 2016 18:52:26 +0100 Subject: [PATCH 02/38] Add drag and drop, rename and delete actions --- www/file/inner.html | 48 +---- www/file/main.js | 437 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 435 insertions(+), 50 deletions(-) diff --git a/www/file/inner.html b/www/file/inner.html index 1a4772167..ea8ad82db 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -3,52 +3,22 @@ + + -
      + diff --git a/www/file/main.js b/www/file/main.js index 05d6fba22..e4303ec59 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -16,15 +16,18 @@ define([ '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/bootstrap/dist/js/bootstrap.min.js', '/customize/pad.js' ], function (Messages, Crypto, realtimeInput, Hyperjson, Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Visible, Notify) { + var module = {}; + var $ = window.jQuery; var saveAs = window.saveAs; var $iframe = $('#pad-iframe').contents(); var ifrw = $('#pad-iframe')[0].contentWindow; - var files = { + var files = module.files = { root: { "Directory 1": { "Dir A": { @@ -43,57 +46,469 @@ define([ "File Z": "#hash_Z" } }; + var currentPath = module.currentPath = ['root']; + var lastSelectTime; + var selectedElement; + + // TODO translate + // TODO translate contextmenu in inner.html + var ROOT_NAME = "My files"; + var TRASH_NAME = "Trash"; + var TIME_BEFORE_RENAME = 1000; var $tree = $iframe.find("#tree"); var $content = $iframe.find("#content"); + var $contextMenu = $iframe.find("#contextMenu"); var $folderIcon = $('', { "class": "fa fa-folder folder", style: "font-family: FontAwesome" }); + var $folderEmptyIcon = $('', { + "class": "fa fa-folder-o folder", + style: "font-family: FontAwesome" + }); + var $folderOpenedIcon = $('', { + "class": "fa fa-folder-open folder", + style: "font-family: FontAwesome" + }); + var $folderOpenedEmptyIcon = $('', { + "class": "fa fa-folder-open-o folder", + style: "font-family: FontAwesome" + }); var $fileIcon = $('', { "class": "fa fa-file file", style: "font-family: FontAwesome" }); + var $upIcon = $('', { + "class": "fa fa-arrow-circle-up", + style: "font-family: FontAwesome" + }); + var $trashIcon = $('', { + "class": "fa fa-trash", + style: "font-family: FontAwesome" + }); + + var removeSelected = function () { + $content.find('.selected').removeClass("selected"); + }; + var removeInput = function () { + $content.find('li > span:hidden').show(); + $content.find('li > input').remove(); + }; - var displayDirectory = function (name, root) { + var comparePath = function (a, b) { + if (!a || !b || !$.isArray(a) || !$.isArray(b)) { return false; } + if (a.length !== b.length) { return false; } + var result = true; + var i = a.length - 1; + while (result && i >= 0) { + result = a[i] === b[i]; + i--; + } + return result; + }; + + var now = function () { + return new Date().getTime(); + }; + + // Find an element in a object following a path, resursively + var findElement = function (root, pathInput) { + if (!pathInput) { + console.error("Invalid path:\n", pathInput, "\nin root\n", root); + //TODO + return; + } + if (pathInput.length === 0) { return root; } + var path = pathInput.slice(); + var key = path.shift(); + if (typeof root[key] === "undefined") { + console.error("Unable to find the key '" + key + "' in the root object provided:\n", root); + //TODO + return; + } + return findElement(root[key], path); + }; + + var moveElement = function (elementPath, newParentPath) { + if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do... + var element = findElement(files, elementPath); + var parentPath = elementPath.slice(); + var name = parentPath.pop(); + var parentEl = findElement(files, parentPath); + var newParent = findElement(files, newParentPath); + if (typeof(newParent[name]) !== "undefined") { + console.error("A file with the same name already exist at the new location"); + //TODO + return; + } + newParent[name] = element; + delete parentEl[name]; + displayDirectory(newParentPath); + }; + + var removeElement = function (path) { + moveElement(path, ['trash']); + }; + + var onDrag = function (ev, path) { + console.log("dragging", path); + var data = { + 'path': path + }; + ev.dataTransfer.setData("data", JSON.stringify(data)); + }; + + var onDrop = function (ev) { + ev.preventDefault(); + var data = ev.dataTransfer.getData("data"); + var oldPath = JSON.parse(data).path; + var newPath = $(ev.target).data('path') || $(ev.target).parent('li').data('path'); + console.log("dropping ", oldPath, " to ", newPath); + if (!oldPath || !newPath) { return; } + moveElement(oldPath, newPath); + }; + + var renameElement = function (path, newName) { + if (path.length <= 1) { + console.error('Renaming "root" is forbidden'); + //TODO + return; + } + if (!newName || newName.trim() === "") { return; } + var isCurrentDirectory = comparePath(path, currentPath); + // Copy the element path and remove the last value to have the parent path and the old name + var element = findElement(files, path); + var parentPath = path.slice(); + var oldName = parentPath.pop(); + if (oldName === newName) { + // Nothing to do... + // TODO ? + return; + } + var parentEl = findElement(files, parentPath); + if (typeof(parentEl[newName]) !== "undefined") { + console.error('Name already used.'); + //TODO + return; + } + parentEl[newName] = element; + delete parentEl[oldName]; + resetTree(); + displayDirectory(currentPath); + }; + + var displayRenameInput = function ($element, path) { + if (!path || path.length < 2) { return; } // TODO error + $element.hide(); + removeSelected(); + var name = path[path.length - 1]; + var $input = $('', { + placeholder: name, + value: name + }); + $input.on('keyup', function (e) { + if (e.which === 13) { + renameElement(path, $input.val()); + removeInput(); + } + }); + $input.insertAfter($element); + $input.focus(); + $input.select(); + $input.click(function (e) { + removeSelected(); + e.stopPropagation(); + }); + }; + + var onElementClick = function ($element, path) { + // If the element was already selected, check if the rename action is available + /*if ($element.hasClass("selected")) { + if($content.find('.selected').length === 1 && + lastSelectTime && + (now() - lastSelectTime) > TIME_BEFORE_RENAME) { + //$element. + renameElement(path, "File renamed"); + } + return; + }*/ + removeSelected(); + if ($element.not('li')) { + $element = $element.parent('li'); + } + if (!$element.length) { return ; } //TODO error + if (!$element.hasClass("selected")) { + $element.addClass("selected"); + lastSelectTime = now(); + } + }; + + var openContextMenu = function (e) { + onElementClick($(e.target)); + e.stopPropagation(); + var path = $(e.target).data('path') || $(e.target).parent('li').data('path'); + if (!path) { return; } + $contextMenu.css({ + display: "block", + left: e.pageX, + top: e.pageY + }); + $contextMenu.find('a').data('path', path); + $contextMenu.find('a').data('element', $(e.target)); + return false; + }; + + var displayDirectory = function (path) { + currentPath = path; + module.resetTree(); $content.html(""); + if (!path || path.length === 0) { + path = ['root']; + } + var root = findElement(files, path); + if (typeof(root) === "undefined") { + // TODO translate + // TODO error + $content.html("Unable to locate the selected directory..."); + return; + } + + // Forbid drag&drop inside the trash + var droppable = root[0] !== "trash"; + + // Display title and "Up" icon + var name = path[path.length - 1]; + if (name === "root" && path.length === 1) { name = ROOT_NAME; } + else if (name === "trash" && path.length === 1) { name = TRASH_NAME; } var $title = $('

      ').text(name); + if (path.length > 1) { + var $parentFolder = $upIcon.clone().addClass("parentFolder") + .click(function() { + var newPath = path.slice(); + newPath.pop(); + var name = newPath[newPath.length -1]; + if (name === "root" && newPath.length === 1) { name = ROOT_NAME; } + displayDirectory(newPath); + }); + $title.append($parentFolder); + } var $dirContent = $('
      ', {id: "folderContent"}); var $list = $('
        ').appendTo($dirContent); + // display sub directories Object.keys(root).forEach(function (key) { if (typeof(root[key]) === "string") { return; } - var $name = $('').text(key).prepend($folderIcon.clone()); - var $element = $('
      • ').append($name).click(function () { - displayDirectory(key, root[key]); + var newPath = path.slice(); + newPath.push(key); + var $icon = Object.keys(root[key]).length === 0 ? $folderEmptyIcon.clone() : $folderIcon.clone(); + var $name = $('', { 'class': 'folder-element element' }).text(key); + var $element = $('
      • ', { + draggable: true + }).append($icon).append($name).dblclick(function () { + displayDirectory(newPath); }); + $element.data('path', newPath); + $element.on('dragstart', function (e) { + onDrag(e.originalEvent, newPath); + }); + if (droppable) { + $element.on('dragover', function (e) { + e.preventDefault(); + }); + $element.on('drop', function (e) { + onDrop(e.originalEvent); + }); + } + $element.click(function(e) { + e.stopPropagation(); + onElementClick($element, newPath); + }); + $element.contextmenu(openContextMenu); $element.appendTo($list); }); // display files Object.keys(root).forEach(function (key) { if (typeof(root[key]) !== "string") { return; } - var $name = $('').text(key).prepend($fileIcon.clone()); - var $element = $('
      • ').append($name).click(function () { + var newPath = path.slice(); + newPath.push(key); + var $name = $('', { 'class': 'file-element element' }).text(key); + var $element = $('
      • ', { + draggable: true + }).append($fileIcon.clone()).append($name).dblclick(function () { window.location.hash = root[key]; }); + $element.data('path', newPath); + $element.on('dragstart', function (e) { + console.log(e.target); + onDrag(e.originalEvent, newPath); + }); + $element.click(function(e) { + e.stopPropagation(); + onElementClick($element, newPath); + }); + $element.contextmenu(openContextMenu); $element.appendTo($list); }); $content.append($title).append($dirContent); }; - var createTree = function (root, $container) { + // TODO: add + and - in the tree (collapse), and link elements with lines + // Cf: https://codepen.io/khoama/pen/hpljA + var createTreeElement = function (name, $icon, path, draggable) { + var $name = $('', { 'class': 'folder-element' }).text(name).prepend($icon) + .click(function () { + displayDirectory(path); + }); + var $element = $('
      • ', { + draggable: draggable + }).append($name); + $element.data('path', path); + $element.on('dragstart', function (e) { + e.stopPropagation(); + onDrag(e.originalEvent, path); + }); + $element.on('dragover', function (e) { + e.preventDefault(); + }); + $element.on('drop', function (e) { + onDrop(e.originalEvent); + }); + return $element; + }; + var createTree = function ($container, path) { + var root = findElement(files, path); if (Object.keys(root).length === 0) { return; } + + // Display the root elemnt in the tree + var displayingRoot = comparePath(['root'], path); + if (displayingRoot) { + var isRootOpened = comparePath(['root'], currentPath); + var $rootIcon = Object.keys(files['root']).length === 0 ? + (isRootOpened ? $folderOpenedEmptyIcon : $folderEmptyIcon) : + (isRootOpened ? $folderOpenedIcon : $folderIcon); + var $rootElement = createTreeElement(ROOT_NAME, $rootIcon.clone(), ['root'], false); + var $root = $('
          ').append($rootElement).appendTo($container); + $container = $rootElement; + } + + // Display root content var $list = $('
            ').appendTo($container); Object.keys(root).forEach(function (key) { // Do not display files in the menu if (typeof(root[key]) === "string") { return; } - var $name = $('').text(key).click(function () { + var newPath = path.slice(); + newPath.push(key); + var isCurrentFolder = comparePath(newPath, currentPath); + var $icon = Object.keys(root[key]).length === 0 ? + (isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) : + (isCurrentFolder ? $folderOpenedIcon : $folderIcon); + var $element = createTreeElement(key, $icon.clone(), newPath, true); + $element.appendTo($list); + createTree($element, newPath); + }); + }; + + var createTrash = function ($container, path) { + var $trash = $('', { + 'class': 'tree-trash' + }).text(TRASH_NAME).prepend($trashIcon.clone()) + .click(function () { + displayDirectory(path); + }); + $trash.data('path', ['trash']); + var $trashElement = $('
          • ').append($trash); + $trashElement.on('dragover', function (e) { + e.preventDefault(); + }); + $trashElement.on('drop', function (e) { + onDrop(e.originalEvent); + }); + var $trashList = $('
              ').append($trashElement); + $container.append($trashList); + }; + + var resetTree = module.resetTree = function () { + $tree.html(''); + createTree($tree, ['root']); + createTrash($tree, ['trash']); + }; + displayDirectory(currentPath); + //resetTree(); //already called by displayDirectory + + var hideMenu = function () { + $contextMenu.hide(); + }; + $contextMenu.on("click", "a", function(e) { + e.stopPropagation(); + var path = $(this).data('path'); + var $element = $(this).data('element'); + if (!$element || !path || path.length < 2) { return; } // TODO: error + if ($(this).hasClass("rename")) { + displayRenameInput($element, path); + } + else if($(this).hasClass("delete")) { + var name = path[path.length - 1]; + // TODO translate + Cryptpad.confirm("Are you sure you want to move " + name + " to the trash?", function(res) { + if (!res) { return; } + console.log("Removing ", path); + removeElement(path); + }); + } + else if ($(this).hasClass('open')) { + $element.dblclick(); + } + hideMenu(); + }); + + $(ifrw).on('click', function (e) { + if (e.which !== 1) { return ; } + removeSelected(e); + removeInput(e); + hideMenu(e); + }); + + /* var displayDirectory = function (name, root, path) { + $content.html(""); + var $title = $('

              ').text(name); + var $dirContent = $('
              ', {id: "folderContent"}); + var $list = $('
                ').appendTo($dirContent); + // display sub directories + Object.keys(root).forEach(function (key) { + if (typeof(root[key]) === "string") { return; } + var $name = $('', { 'class': 'folder-element' }).text(key).prepend($folderIcon.clone()); + var $element = $('
              • ').append($name).dblclick(function () { displayDirectory(key, root[key]); }); + $element.appendTo($list); + }); + // display files + Object.keys(root).forEach(function (key) { + if (typeof(root[key]) !== "string") { return; } + var $name = $('', { 'class': 'file-element' }).text(key).prepend($fileIcon.clone()); + var $element = $('
              • ').append($name).dblclick(function () { + window.location.hash = root[key]; + }); + $element.appendTo($list); + }); + $content.append($title).append($dirContent); + }; + + var createTree = function ($container, root, path) { + if (Object.keys(root).length === 0) { return; } + var $list = $('
                  ').appendTo($container); + Object.keys(root).forEach(function (key) { + // Do not display files in the menu + if (typeof(root[key]) === "string") { return; } + var $icon = Object.keys(root[key]).length === 0 ? $folderEmptyIcon.clone() : $folderIcon.clone(); + var $name = $('', { 'class': 'folder-element' }).text(key).prepend($icon) + .click(function () { + displayDirectory(key, root[key]); + }); var $element = $('
                • ').append($name); $element.appendTo($list); createTree(root[key], $element[0]); }); - }; - createTree(files.root, $tree); + };*/ }); From c9cd06514ca7aea0769f29c31effbf0181689015 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 7 Nov 2016 18:50:42 +0100 Subject: [PATCH 03/38] Add "restore" from trash, improve drag and drop and update css --- bower.json | 3 +- www/file/file.css | 163 +++++++++++ www/file/inner.html | 13 +- www/file/main.js | 688 ++++++++++++++++++++++++++++++++------------ 4 files changed, 674 insertions(+), 193 deletions(-) create mode 100644 www/file/file.css diff --git a/bower.json b/bower.json index c7100f505..c92757dea 100644 --- a/bower.json +++ b/bower.json @@ -41,6 +41,7 @@ "diff-dom": "#gh-pages", "alertifyjs": "^1.0.11", "spin.js": "^2.3.2", - "scrypt-async": "^1.2.0" + "scrypt-async": "^1.2.0", + "bootstrap": "^3.3.7" } } diff --git a/www/file/file.css b/www/file/file.css new file mode 100644 index 000000000..444669457 --- /dev/null +++ b/www/file/file.css @@ -0,0 +1,163 @@ +/* PAGE */ + +html, body { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 0; + margin: 0; + position: relative; +} + +.fa { + width: 17px; + font-family: FontAwesome; +} + +ul { + list-style: none; + padding-left: 10px; +} + +li { + padding: 0px 5px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +li > span.element:hover { + text-decoration: underline; +} + +.folder, .file { + margin-right: 5px; +} + +.contextMenu { + display: none; + position: absolute; +} + +.droppable { + background-color: #FE9A2E; + color: #222; +} + +.selected { + border: 1px dotted #bbb; + background: #666; + color: #eee; +} + +/* TREE */ + +#tree { + position:absolute; + top: 0; + left: 0; + bottom: 0; + right: 70%; + border: 2px solid blue; + box-sizing: border-box; + background: white; +} + +#tree li { + cursor: auto; +} + +#tree span.element { + cursor: pointer; +} + +#tree .active { + text-decoration: underline; +} + +#tree #trashTree { + margin-top: 2em; +} + +#tree .fa.expcol { + margin-left: -10px; +} + +#tree .non-collapsable { + padding-left: 12px; +} + +/* Tree lines */ + +#tree ul { + margin: 0px 0px 0px 10px; + list-style: none; +} +#tree ul li { + position: relative; +} +#tree ul li:before { + position: absolute; + left: -15px; + top: 0px; + content: ''; + display: block; + border-left: 1px solid #ddd; + height: 0.75em; + border-bottom: 1px solid #ddd; + width: 10px; +} +#tree ul li.non-collapsable:before { + width: 27px; +} +#tree ul li:after { + position: absolute; + left: -15px; + bottom: -7px; + content: ''; + display: block; + border-left: 1px solid #ddd; + height: 100%; +} +#tree ul li.root { + margin: 0px 0px 0px -10px; +} +#tree ul li.root:before { + display: none; +} +#tree ul li.root:after { + display: none; +} +#tree ul li:last-child:after { + display: none; +} + + +/* CONTENT */ + +#content { + position: absolute; + top: 0; + left: 30%; + bottom: 0; + right: 0; + border: 2px solid green; + box-sizing: border-box; + background: #eee; +} + +.parentFolder { + cursor: pointer; + margin-left: 10px; +} + +.parentFolder:hover { + text-decoration: underline; +} + +#folderContent { + display: inline-block; +} + diff --git a/www/file/inner.html b/www/file/inner.html index ea8ad82db..fd17e8251 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -12,13 +12,24 @@
              -