From cfe3d38197f1347958b0f979664bd308229de192 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 8 Nov 2016 18:53:47 +0100 Subject: [PATCH] Add expend/collapse to the tree, fix CSS, add list/grid view modes --- www/file/file.css | 101 +++++++++-- www/file/main.js | 439 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 461 insertions(+), 79 deletions(-) diff --git a/www/file/file.css b/www/file/file.css index 444669457..6f5bae12b 100644 --- a/www/file/file.css +++ b/www/file/file.css @@ -7,6 +7,8 @@ html, body { padding: 0; margin: 0; position: relative; + font-size: 20px; + overflow: auto; } .fa { @@ -28,10 +30,6 @@ li { user-select: none; } -li > span.element:hover { - text-decoration: underline; -} - .folder, .file { margin-right: 5px; } @@ -63,6 +61,7 @@ li > span.element:hover { border: 2px solid blue; box-sizing: border-box; background: white; + overflow: auto; } #tree li { @@ -73,6 +72,10 @@ li > span.element:hover { cursor: pointer; } +#tree li > span.element:hover { + text-decoration: underline; +} + #tree .active { text-decoration: underline; } @@ -83,10 +86,29 @@ li > span.element:hover { #tree .fa.expcol { margin-left: -10px; + font-size: 14px; + position: absolute; + left: -20px; + top: 9px; + width: auto; + height: 11px; + padding: 0; + margin: 0; + background: white; + z-index: 10; + cursor: default; +} +#tree .fa.expcol:before { + position:relative; + top: -1px; +} + +#tree li.collapsed ul { + display: none; } -#tree .non-collapsable { - padding-left: 12px; +#tree li input { + width: calc(100% - 30px); } /* Tree lines */ @@ -101,16 +123,13 @@ li > span.element:hover { #tree ul li:before { position: absolute; left: -15px; - top: 0px; + top: -0.25em; 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; + border-left: 1px solid #888; + height: 1em; + border-bottom: 1px solid #888; + width: 17.5px; } #tree ul li:after { position: absolute; @@ -118,7 +137,7 @@ li > span.element:hover { bottom: -7px; content: ''; display: block; - border-left: 1px solid #ddd; + border-left: 1px solid #888; height: 100%; } #tree ul li.root { @@ -146,6 +165,12 @@ li > span.element:hover { border: 2px solid green; box-sizing: border-box; background: #eee; + overflow: auto; +} + +.changeViewModeContainer { + border: 1px solid #ccc; + float: right; } .parentFolder { @@ -158,6 +183,52 @@ li > span.element:hover { } #folderContent { + /*display: inline-block;*/ +} + +#content li:hover .name { + text-decoration: underline; +} + +#content .grid li { + display: inline-block; + margin: 10px 10px; + width: 140px; + text-align: center; + height: 70px; + vertical-align: top; +} + +#content .grid li input { + width: 100%; +} + +#content .grid li .fa { + display: block; + margin: auto; + font-size: 40px; + width: auto; + text-align: center; +} + +#content .list li { + display: flex; + flex-flow: row; + align-items: center; +} +#content .list li .element { + display: inline-flex; + flex: 1; +} + +#content .list .element span { + margin-right: 20px; display: inline-block; } +#content .list .element span.name { + flex: 1; +} +#content .list .element span.date { + width: 120px; +} diff --git a/www/file/main.js b/www/file/main.js index 875d0c162..6bf1380a7 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -27,13 +27,39 @@ define([ var saveAs = window.saveAs; var $iframe = $('#pad-iframe').contents(); var ifrw = $('#pad-iframe')[0].contentWindow; + + var ROOT = "root"; + var ROOT_NAME = "My files"; + var FILES_DATA = "filesData"; + var FILES_DATA_NAME = "Unsorted files"; + var TRASH = "trash"; + var TRASH_NAME = "Trash"; + var TIME_BEFORE_RENAME = 1000; + var LOCALSTORAGE_LAST = "cryptpad-file-lastOpened"; + var LOCALSTORAGE_OPENED = "cryptpad-file-openedFolders"; + var LOCALSTORAGE_VIEWMODE = "cryptpad-file-viewMode"; + var FOLDER_CONTENT_ID = "folderContent"; + var files = module.files = { root: { "Directory 1": { "Dir A": { + "Dir D": { + "Dir E": {}, + }, "File a": "#hash_a", - "File b": "#hash_b" + "File b": "#hash_b", + "File c": "#hash_c", + "File d": "#hash_d", + "File e": "#hash_e", + "File f": "#hash_f", + "File g": "#hash_g", + "File h": "#hash_h", + "File i": "#hash_i", + "File j": "#hash_j", + "File k": "#hash_k" }, + "Dir C": {}, "Dir B": {}, "File A": "#hash_A" }, @@ -42,6 +68,83 @@ define([ "File C": "#hash_C" } }, + filesData: { + "#hash_a": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_b": { + ctime: "Mon Nov 07 2016 16:38:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:38:21 GMT+0100 (CET)", + title: "Pad B" + }, + "#hash_c": { + ctime: "Tue Nov 08 2016 16:34:21 GMT+0100 (CET)", + atime: "Sun Nov 06 2016 12:34:21 GMT+0100 (CET)", + title: "Pad C With A Long Title" + }, + "#hash_d": { + ctime: "Tue Nov 08 2016 16:30:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:30:21 GMT+0100 (CET)", + title: "Pad D" + }, + "#hash_e": { + ctime: "Tue Nov 08 2016 16:26:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:26:21 GMT+0100 (CET)", + title: "Pad E" + }, + "#hash_f": { + ctime: "Tue Nov 08 2016 16:22:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:22:21 GMT+0100 (CET)", + title: "Pad F" + }, + "#hash_g": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_h": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_i": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_j": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_k": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Pad A" + }, + "#hash_Z": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Code Z" + }, + "#hash_A": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Code A" + }, + "#hash_B": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Code B" + }, + "#hash_C": { + ctime: "Tue Nov 08 2016 16:42:21 GMT+0100 (CET)", + atime: "Tue Nov 08 2016 12:42:21 GMT+0100 (CET)", + title: "Code C" + } + }, trash: { "File Z": [{ element: "#hash_Z", @@ -49,15 +152,78 @@ define([ }] } }; + module.defaultFiles = JSON.parse(JSON.stringify(files)); // TODO translate // TODO translate contextmenu in inner.html - var ROOT = "root"; - var ROOT_NAME = "My files"; - var TRASH = "trash"; - var TRASH_NAME = "Trash"; - var TIME_BEFORE_RENAME = 1000; + var getLastOpenedFolder = function () { + var path; + try { + path = localStorage[LOCALSTORAGE_LAST] ? JSON.parse(localStorage[LOCALSTORAGE_LAST]) : [ROOT]; + } catch (e) { + path = [ROOT]; + } + return path; + }; + var setLastOpenedFolder = function (path) { + localStorage[LOCALSTORAGE_LAST] = JSON.stringify(path); + }; + + var initLSOpened = function () { + try { + var store = JSON.parse(localStorage[LOCALSTORAGE_OPENED]); + if (!$.isArray(store)) { + localStorage[LOCALSTORAGE_OPENED] = '[]'; + } + } catch (e) { + localStorage[LOCALSTORAGE_OPENED] = '[]'; + } + }; + initLSOpened(); + + var wasFolderOpened = function (path) { + var store = JSON.parse(localStorage[LOCALSTORAGE_OPENED]); + return store.indexOf(JSON.stringify(path)) !== -1; + }; + var setFolderOpened = function (path, opened) { + var s = JSON.stringify(path); + var store = JSON.parse(localStorage[LOCALSTORAGE_OPENED]); + if (opened && store.indexOf(s) === -1) { + store.push(s); + } + if (!opened) { + var idx = store.indexOf(s); + if (idx !== -1) { + store.splice(idx, 1); + } + } + localStorage[LOCALSTORAGE_OPENED] = JSON.stringify(store); + }; + + var getViewModeClass = function () { + var mode = localStorage[LOCALSTORAGE_VIEWMODE]; + if (mode === 'list') { return 'list'; } + return 'grid'; + }; + var getViewMode = function () { + return localStorage[LOCALSTORAGE_VIEWMODE] || 'grid'; + }; + var setViewMode = function (mode) { + if (typeof(mode) !== "string") { + console.error("Incorrect view mode: ", mode); + return; + } + localStorage[LOCALSTORAGE_VIEWMODE] = mode; + }; + - var currentPath = module.currentPath = [ROOT]; + var DEBUG = window.DEBUG = { + resetLocalStorage : function () { + delete localStorage[LOCALSTORAGE_OPENED]; + delete localStorage[LOCALSTORAGE_LAST]; + } + }; + + var currentPath = module.currentPath = getLastOpenedFolder(); var lastSelectTime; var selectedElement; @@ -76,6 +242,8 @@ define([ var $trashEmptyIcon = $('', {"class": "fa fa-trash-o"}); var $collapseIcon = $('', {"class": "fa fa-minus-square-o expcol"}); var $expandIcon = $('', {"class": "fa fa-plus-square-o expcol"}); + var $listIcon = $('', {"class": "fa fa-list"}); + var $gridIcon = $('', {"class": "fa fa-th"}); var removeSelected = function () { $iframe.find('.selected').removeClass("selected"); @@ -109,6 +277,63 @@ define([ return typeof(element) !== "string"; }; + var isFolderEmpty = function (element) { + if (typeof(element) !== "object") { return false; } + return Object.keys(element).length === 0; + }; + + var hasSubfolder = function (element) { + if (typeof(element) !== "object") { return false; } + var subfolder = false; + for (var f in element) { + subfolder = isFolder(element[f]); + if (subfolder) { break; } + } + return subfolder; + }; + + var isSubpath = function (path, parentPath) { + var pathA = parentPath.slice(); + var pathB = path.slice(0, pathA.length); + return comparePath(pathA, pathB); + }; + + var getAvailableName = function (parentEl, name) { + if (typeof(parentEl[name]) === "undefined") { return name; } + var newName = name; + var i = 1; + while (typeof(parentEl[newName]) !== "undefined") { + newName = name + "_" + i; + i++; + } + return newName; + }; + + var compareDays = function (date1, date2) { + var day1 = Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate()); + var day2 = Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate()); + var ms = Math.abs(day1-day2); + return Math.floor(ms/1000/60/60/24); + }; + + var getDate = function (sDate) { + var ret = sDate.toString(); + try { + var date = new Date(sDate); + var today = new Date(); + var diff = compareDays(date, today); + if (diff === 0) { + ret = date.toLocaleTimeString(); + } else { + ret = date.toLocaleDateString(); + } + } catch (e) { + // TODO + console.error("Unable to display that string to a date with .toLocaleString", sDate, e); + } + return ret; + }; + // Find an element in a object following a path, resursively var findElement = function (root, pathInput) { if (!pathInput) { @@ -148,7 +373,8 @@ define([ } else { parentPath = elementPath.slice(); name = parentPath.pop(); - newName = name; + // Automatically rename if we were in the trash since we can't rename from the trash + newName = elementPath[0] === TRASH ? getAvailableName(newParent, name) : name; } var parentEl = findElement(files, parentPath); @@ -159,7 +385,7 @@ define([ } newParent[newName] = element; delete parentEl[name]; - displayDirectory(newParentPath); + module.displayDirectory(newParentPath); }; // Move to trash @@ -184,9 +410,9 @@ define([ trashArray.push(trashElement); delete parentEl[name]; if (displayTrash) { - displayDirectory([TRASH]); + module.displayDirectory([TRASH]); } else { - displayDirectory(currentPath); + module.displayDirectory(currentPath); } }; Cryptpad.confirm("Are you sure you want to move " + name + " to the trash?", function(res) { @@ -195,17 +421,6 @@ define([ }); }; - var getAvailableName = function (parentEl, name) { - if (typeof(parentEl[name]) === "undefined") { return name; } - var newName = name; - var i = 1; - while (typeof(parentEl[newName]) !== "undefined") { - newName = name + "_" + i; - i++; - } - return newName; - }; - var removeFromTrashArray = function (element, name) { var array = files.trash[name]; if (!array || !$.isArray(array)) { return; } @@ -231,13 +446,12 @@ define([ var name = getAvailableName(newParentEl, path[1]); newParentEl[name] = element; removeFromTrashArray(parentEl, path[1]); - displayDirectory(currentPath); + module.displayDirectory(currentPath); }; var removeFromTrash = function (path) { if (!path || path.length < 4 || path[0] !== TRASH) { return; } // Remove the last element from the path to get the parent path and the element name - console.log(path); var parentPath = path.slice(); var name; if (path.length === 4) { // Trash root @@ -245,7 +459,7 @@ define([ parentPath.pop(); var parentElement = findElement(files, parentPath); removeFromTrashArray(parentElement, name); - displayDirectory(currentPath); + module.displayDirectory(currentPath); return; } name = parentPath.pop(); @@ -255,24 +469,25 @@ define([ return; } delete parentEl[name]; - displayDirectory(currentPath); + module.displayDirectory(currentPath); }; var emptyTrash = function () { files.trash = {}; - displayDirectory(currentPath); + module.displayDirectory(currentPath); }; var onDrag = function (ev, path) { var data = { 'path': path }; - ev.dataTransfer.setData("data", JSON.stringify(data)); + ev.dataTransfer.setData("text", JSON.stringify(data)); }; var onDrop = function (ev) { ev.preventDefault(); - var data = ev.dataTransfer.getData("data"); + $iframe.find('.droppable').removeClass('droppable'); + var data = ev.dataTransfer.getData("text"); var oldPath = JSON.parse(data).path; var newPath = $(ev.target).data('path') || $(ev.target).parent('li').data('path'); if (!oldPath || !newPath) { return; } @@ -313,8 +528,8 @@ define([ } parentEl[newName] = element; delete parentEl[oldName]; - resetTree(); - displayDirectory(currentPath); + module.resetTree(); + module.displayDirectory(currentPath); }; var displayRenameInput = function ($element, path) { @@ -379,7 +594,7 @@ define([ }; var openContextMenu = function (e) { - hideMenu(); + module.hideMenu(); onElementClick($(e.target)); e.stopPropagation(); var path = $(e.target).data('path') || $(e.target).parent('li').data('path'); @@ -408,7 +623,7 @@ define([ }; var openTrashTreeContextMenu = function (e) { - hideMenu(); + module.hideMenu(); onElementClick($(e.target)); e.stopPropagation(); var path = $(e.target).data('path') || $(e.target).parent('li').data('path'); @@ -424,7 +639,7 @@ define([ }; var openTrashContextMenu = function (e) { - hideMenu(); + module.hideMenu(); onElementClick($(e.target)); e.stopPropagation(); var path = $(e.target).data('path') || $(e.target).parent('li').data('path'); @@ -444,8 +659,16 @@ define([ }; var addDragAndDropHandlers = function ($element, path, isFolder, droppable) { + // "dragenter" is fired for an element and all its children + // "dragleave" may be fired when entering a child + // --> We store the number of enter/leave and the element entered and we remove the + // highlighting only when we have left everything + var counter = 0; + var dragenterList = []; $element.on('dragstart', function (e) { e.stopPropagation(); + counter = 0; + dragenterList = []; onDrag(e.originalEvent, path); }); @@ -458,28 +681,51 @@ define([ $element.on('drop', function (e) { onDrop(e.originalEvent); }); - var counter = 0; $element.on('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); + if (dragenterList.indexOf(e.target) !== -1) { return; } + dragenterList.push(e.target); counter++; $element.addClass('droppable'); }); $element.on('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); + var idx = dragenterList.indexOf(e.target); + dragenterList.splice(idx, 1); counter--; - if (counter === 0) { + if (counter <= 0) { $element.removeClass('droppable'); } }); }; + var addFileData = function (parentPath, key, $span) { + var parentEl = findElement(files, parentPath); + if (!parentEl || !parentEl[key] || !isFile(parentEl[key])) { return; } + var element = parentEl[key]; + if (typeof(files[FILES_DATA][element]) === "undefined") { + return; + } + var data = files[FILES_DATA][element]; + $span.html(''); + // The element with the class '.name' is underlined when the 'li' is hovered + $span.removeClass('name'); + var $name = $('', {'class': 'name'}).text(key); + var $title = $('', {'class': 'title'}).text(data.title); + var $adate = $('', {'class': 'date'}).text(getDate(data.atime)); + var $cdate = $('', {'class': 'date'}).text(getDate(data.ctime)); + $span.append($name).append($title).append($adate).append($cdate); + return; + }; + var createElement = function (path, elPath, root, isFolder) { // Forbid drag&drop inside the trash var isTrash = path[0] === TRASH; var newPath = path.slice(); + var key; if (isTrash && $.isArray(elPath)) { key = elPath[0]; @@ -490,20 +736,22 @@ define([ } var $icon = $fileIcon.clone(); - var spanClass = 'file-element element'; + var spanClass = 'file-element name element'; if (isFolder) { - spanClass = 'folder-element element'; - $icon = Object.keys(root[key]).length === 0 ? $folderEmptyIcon.clone() : $folderIcon.clone(); + spanClass = 'folder-element name element'; + $icon = isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone(); } var $name = $('', { 'class': spanClass }).text(key); + if(!isFolder && getViewMode() === 'list') { + addFileData(path, key, $name); + } var $element = $('
  • ', { draggable: true }).append($icon).append($name).dblclick(function () { if (isFolder) { - displayDirectory(newPath); + module.displayDirectory(newPath); return; } - // Prevent users from opening files from the trash TODO ?? if (isTrash) { return; } openFile(root[key]); }); @@ -536,7 +784,7 @@ define([ } } return title; - } + }; var createTitle = function (path) { var isTrash = path[0] === TRASH; @@ -556,16 +804,50 @@ define([ // --> parent is TRASH newPath = [TRASH]; } - displayDirectory(newPath); + module.displayDirectory(newPath); }); $title.append($parentFolder); } return $title; }; + var createViewModeButton = function () { + var $block = $('
    ', { + 'class': 'btn-group changeViewModeContainer' + }); + + var $listButton = $('