diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 2525d80e1..979a24d0c 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -173,15 +173,18 @@ define(function () { out.fm_creation = "Création"; out.fm_forbidden = "Action interdite"; out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?"; + out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de manière permanente ?"; out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer {0} de la corbeille de manière permanente ?"; out.fm_restoreDialog = "Êtes-vous sûr de vouloir restaurer {0} à son emplacement précédent ?"; - out.fm_removeSeveralDialog = "Êtes-vous sûr de vouloir déplacer ces {0} éléments vers la corbeille ?" + out.fm_removeSeveralDialog = "Êtes-vous sûr de vouloir déplacer ces {0} éléments vers la corbeille ?"; + out.fm_removeDialog = "Êtes-vous sûr de vouloir déplacer {0} vers la corbeille ?"; out.fm_unknownFolderError = "Le dossier sélectionné ou le dernier dossier visité n'existe plus. Ouverture du dossier parent..."; out.fm_contextMenuError = "Impossible d'ouvrir le menu contextuel pour cet élément. Si le problème persiste, essayez de rechercher la page."; out.fm_selectError = "Impossible de sélectionner l'élément ciblé. Si le problème persiste, essayez de recharger la page."; out.fo_moveUnsortedError = "La liste des éléments non triés ne peut pas contenir de dossiers."; out.fo_existingNameError = "Ce nom est déjà utilisé dans ce répertoire. Veuillez en choisir un autre."; + out.fo_moveFolderToChildError = "Vous ne pouvez pas déplacer un dossier dans un de ses descendants"; // index.html diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 9d5571e8d..31ee969f1 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -173,8 +173,10 @@ define(function () { out.fm_creation = "Creation"; out.fm_forbidden = "Forbidden action"; out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?"; + out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from the trash permanently?"; out.fm_removePermanentlyDialog = "Are you sure you want to remove {0} from the trash permanently?"; - out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?" + out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?"; + out.fm_removeDialog = "Are you sure you want to move {0} to the trash?"; out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?"; out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder..."; out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page."; @@ -182,6 +184,7 @@ define(function () { out.fo_moveUnsortedError = "You can't move a folder to the list of unsorted pads"; out.fo_existingNameError = "Name already used in that directory. Please choose another one."; + out.fo_moveFolderToChildError = "You can't move a folder into one of its descendants"; // index.html diff --git a/www/file/fileObject.js b/www/file/fileObject.js index e92969266..623bea8bd 100644 --- a/www/file/fileObject.js +++ b/www/file/fileObject.js @@ -23,7 +23,7 @@ define([ var error = exp.error = function() { exp.fixFiles(); - console.error.apply(arguments); + console.error.apply(null, arguments); }; var comparePath = exp.comparePath = function (a, b) { @@ -41,7 +41,7 @@ define([ var deleteFromObject = function (path) { var parentPath = path.slice(); var key = parentPath.pop(); - var parentEl = findElement(files, parentPath); + var parentEl = exp.findElement(files, parentPath); if (path.length === 4 && path[0] === TRASH) { files[TRASH][path[1]].splice(path[2], 1); } else if (path[0] === UNSORTED) { @@ -77,12 +77,13 @@ define([ var hasSubfolder = exp.hasSubfolder = function (element, trashRoot) { if (typeof(element) !== "object") { return false; } var subfolder = 0; + var addSubfolder = function (el, idx) { + subfolder += isFolder(el.element) ? 1 : 0; + }; for (var f in element) { if (trashRoot) { if ($.isArray(element[f])) { - element[f].forEach(function (el, idx) { - subfolder += isFolder(el.element) ? 1 : 0; - }); + element[f].forEach(addSubfolder); } } else { subfolder += isFolder(element[f]) ? 1 : 0; @@ -94,12 +95,13 @@ define([ var hasFile = exp.hasFile = function (element, trashRoot) { if (typeof(element) !== "object") { return false; } var file = 0; + var addFile = function (el, idx) { + file += isFile(el.element) ? 1 : 0; + }; for (var f in element) { if (trashRoot) { if ($.isArray(element[f])) { - element[f].forEach(function (el, idx) { - file += isFile(el.element) ? 1 : 0; - }); + element[f].forEach(addFile); } } else { file += isFile(element[f]) ? 1 : 0; @@ -130,36 +132,83 @@ define([ return fileA === fileB; }; - var isInTrashRoot = function (path) { - return path[0] === TRASH && path.length === 4; + var isFileInTree = function (file, root) { + if (isFile(root)) { + return compareFiles(file, root); + } + var inTree = false; + for (var e in root) { + inTree = isFileInTree(file, root[e]); + if (inTree) { break; } + } + return inTree; }; - var getTrashElementData = exp.getTrashElementData = function (trashPath) { - if (!isInTrashRoot) { - debug("Called getTrashElementData on a element not in trash root: ", trashpath); - return; + var isFileInTrash = function (file) { + var inTrash = false; + var root = files[TRASH]; + var filter = function (trashEl, idx) { + inTrash = isFileInTree(file, trashEl.element); + return inTrash; + }; + for (var e in root) { + if (!$.isArray(root[e])) { + error("Trash contains a non-array element"); + return; + } + root[e].some(filter); + if (inTrash) { break; } } - var parentPath = trashPath.slice(); - parentPath.pop(); - return findElement(files, parentPath); + return inTrash; + }; + + var isFileInUnsorted = function (file) { + return files[UNSORTED].indexOf(file) !== -1; }; var getUnsortedFiles = exp.getUnsortedFiles = function () { return files[UNSORTED]; }; + var getFilesRecursively = function (root, arr) { + for (var e in root) { + if (isFile(root[e])) { + if(arr.indexOf(root[e]) === -1) { arr.push(root[e]); } + } else { + getFilesRecursively(root[e], arr); + } + } + }; + var getRootFiles = function () { + var ret = []; + getFilesRecursively(files[ROOT], ret); + return ret; + }; - - // Data from filesData - var getTitle = function (href) { - if (!files[FILES_DATA][href]) { - error("getTitle called with a non-existing href: ", href); - return; + var getTrashFiles = exp.getTrashFiles = function () { + var root = files[TRASH]; + var ret = []; + var addFiles = function (el, idx) { + if (isFile(el.element)) { + if(ret.indexOf(el.element) === -1) { ret.push(el.element); } + } else { + getFilesRecursively(el.element, ret); + } + }; + for (var e in root) { + if (!$.isArray(root[e])) { + error("Trash contains a non-array element"); + return; + } + root[e].forEach(addFiles); } - return files[FILES_DATA][href].title; + return ret; }; + var isInTrashRoot = exp.isInTrashRoot = function (path) { + return path[0] === TRASH && path.length === 4; + }; // Find an element in a object following a path, resursively var findElement = exp.findElement = function (root, pathInput) { @@ -177,16 +226,70 @@ define([ return findElement(root[key], path); }; - var moveElement = exp.moveElement = function (elementPath, newParentPath, cb) { + var getTrashElementData = exp.getTrashElementData = function (trashPath) { + if (!isInTrashRoot) { + debug("Called getTrashElementData on a element not in trash root: ", trashPath); + return; + } + var parentPath = trashPath.slice(); + parentPath.pop(); + return findElement(files, parentPath); + }; + + // Data from filesData + var getTitle = function (href) { + if (!href || !files[FILES_DATA][href]) { + error("getTitle called with a non-existing href: ", href); + return; + } + return files[FILES_DATA][href].title; + }; + + var pushToTrash = function (name, element, path) { + var trash = findElement(files, [TRASH]); + + if (typeof(trash[name]) === "undefined") { + trash[name] = []; + } + var trashArray = trash[name]; + var trashElement = { + element: element, + path: path + }; + trashArray.push(trashElement); + }; + + // Move to trash + var removeElement = exp.removeElement = function (path, cb, keepOld) { + if (!path || path.length < 2 || path[0] === TRASH) { + debug("Calling removeElement from a wrong path: ", path); + return; + } + var element = findElement(files, path); + var key = path[path.length - 1]; + var name = isPathInUnsorted(path) ? getTitle(element) : key; + var parentPath = path.slice(); + parentPath.pop(); + pushToTrash(name, element, parentPath); + if (!keepOld) { deleteFromObject(path); } + if(cb) { cb(); } + }; + + var moveElement = exp.moveElement = function (elementPath, newParentPath, cb, keepOld) { if (comparePath(elementPath, newParentPath)) { return; } // Nothing to do... - if (newParentPath[0] && newParentPath[0] === TRASH) { - debug("Moving to trash is forbidden. You have to use the removeElement function"); + if (isPathInTrash(newParentPath)) { + removeElement(elementPath, cb, keepOld); return; } var element = findElement(files, elementPath); var newParent = findElement(files, newParentPath); + if (isFolder(element) && isSubpath(newParentPath, elementPath)) { + log(Messages.fo_moveFolderToChildError); + return; + } + if (isPathInUnsorted(newParentPath)) { if (isFolder(element)) { log(Messages.fo_moveUnsortedError); @@ -195,8 +298,8 @@ define([ if (files[UNSORTED].indexOf(element) === -1) { files[UNSORTED].push(element); } - deleteFromObject(elementPath); - cb && cb(); + if (!keepOld) { deleteFromObject(elementPath); } + if(cb) { cb(); } return; } } @@ -205,7 +308,7 @@ define([ if (isPathInUnsorted(elementPath)) { name = getTitle(element); - } else if (elementPath.length === 4 && elementPath[0] === TRASH) { + } else if (isInTrashRoot(elementPath)) { // Element from the trash root: elementPath = [TRASH, "{dirName}", 0, 'element'] name = elementPath[1]; } else { @@ -219,8 +322,47 @@ define([ } newParent[newName] = element; - deleteFromObject(elementPath); - cb && cb(); + if (!keepOld) { deleteFromObject(elementPath); } + if(cb) { cb(); } + }; + + // "Unsorted" is an array of href: we can't move several of them using "moveElement" in a + // loop because moveElement removes the href from the array and it changes the path for all + // the other elements. We have to move them all and then remove them from unsorted + var moveUnsortedElements = exp.moveUnsortedElements = function (paths, newParentPath, cb) { + if (!paths || paths.length === 0) { return; } + var elements = {}; + // Get the elements + paths.forEach(function (p) { + if (!isPathInUnsorted(p)) { return; } + var el = findElement(files, p); + if (el) { elements[el] = p; } + }); + // Copy the elements to their new location + Object.keys(elements).forEach(function (el) { + moveElement(elements[el], newParentPath, null, true); + }); + // Remove the elements from their old location + Object.keys(elements).forEach(function (el) { + var idx = files[UNSORTED].indexOf(el); + if (idx !== -1) { + files[UNSORTED].splice(idx, 1); + } + }); + if(cb) { cb(); } + }; + + var moveElements = exp.moveElements = function (paths, newParentPath, cb) { + var unsortedPaths = paths.filter(function (p) { + return p[0] === UNSORTED; + }); + moveUnsortedElements(unsortedPaths, newParentPath); + // Copy the elements to their new location + paths.forEach(function (p) { + if (isPathInUnsorted(p)) { return; } + moveElement(p, newParentPath, null); + }); + if(cb) { cb(); } }; var createNewFolder = exp.createNewFolder = function (folderPath, name, cb) { @@ -234,45 +376,26 @@ define([ }); }; - var pushToTrash = function (name, element, path) { - var trash = findElement(files, [TRASH]); - if (typeof(trash[name]) === "undefined") { - trash[name] = []; - } - var trashArray = trash[name]; - var trashElement = { - element: element, - path: path - }; - trashArray.push(trashElement); - }; - // Move to trash - var removeElement = exp.removeElement = function (path, cb, force) { - if (!path || path.length < 2 || path[0] === TRASH) { - debug("Calling removeElement from a wrong path: ", path); - return; - } - var element = findElement(files, path); - var key = path[path.length - 1]; - var name = isPathInUnsorted(path) ? getTitle(element) : key; - var andThen = function () { - var parentPath = path.slice(); - parentPath.pop(); - pushToTrash(name, element, parentPath); - deleteFromObject(path); - cb && cb(); - }; - if (force) { - andThen(); - return; - } - Cryptpad.confirm("Are you sure you want to move " + name + " to the trash?", function(res) { - if (!res) { return; } - andThen(); + var checkDeletedFiles = function () { + var rootFiles = getRootFiles().slice(); + var unsortedFiles = getUnsortedFiles().slice(); + var trashFiles = getTrashFiles().slice(); + var toRemove = []; + Object.keys(files[FILES_DATA]).forEach(function (f) { + if (rootFiles.indexOf(f) === -1 + && unsortedFiles.indexOf(f) === -1 + && trashFiles.indexOf(f) === -1) { + toRemove.push(f); + } + }); + toRemove.forEach(function (f) { + debug("Removing", f, "from filesData"); + delete files[FILES_DATA][f]; }); }; + // Remove an element from the trash root var removeFromTrashArray = function (element, name) { var array = files[TRASH][name]; if (!array || !$.isArray(array)) { return; } @@ -287,6 +410,7 @@ define([ } }; + // Restore an element (copy it elsewhere and remove from the trash root) var restoreTrash = exp.restoreTrash = function (path, cb) { if (!path || path.length !== 4 || path[0] !== TRASH) { debug("restoreTrash was called from an element not in the trash root: ", path); @@ -312,33 +436,34 @@ define([ cb(); }; - // Delete permanently + // Delete permanently (remove from the trash root and from filesData) var removeFromTrash = exp.removeFromTrash = function (path, cb) { 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 var parentPath = path.slice(); var name; + var element = findElement(files, path); if (path.length === 4) { // Trash root name = path[1]; parentPath.pop(); var parentElement = findElement(files, parentPath); removeFromTrashArray(parentElement, name); - cb(); - return; - } - name = parentPath.pop(); - var parentEl = findElement(files, parentPath); - if (typeof(parentEl[name]) === "undefined") { - logError("Unable to locate the element to remove from trash: ", path); - return; + } else { + name = parentPath.pop(); + var parentEl = findElement(files, parentPath); + if (typeof(parentEl[name]) === "undefined") { + logError("Unable to locate the element to remove from trash: ", path); + return; + } + delete parentEl[name]; } - delete parentEl[name]; - cb(); + checkDeletedFiles(); + if(cb) { cb(); } }; var emptyTrash = exp.emptyTrash = function (cb) { files[TRASH] = {}; - cb(); + if(cb) { cb(); } }; @@ -373,6 +498,7 @@ define([ // * Trash root contains only arrays, each element of the array is an object {element:.., path:..} // * Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. // * Dates (adate, cdate) can be parsed/formatted + // * All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' debug("Cleaning file system..."); // Create a backup @@ -398,17 +524,18 @@ define([ fixRoot(files[ROOT]); var fixTrashRoot = function (tr) { var toClean; + var addToClean = function (obj, idx) { + if (typeof(obj) !== "object") { toClean.push(idx); return; } + if (!isFile(obj.element) && !isFolder(obj.element)) { toClean.push(idx); return; } + if (!$.isArray(obj.path)) { toClean.push(idx); return; } + }; for (var el in tr) { if (!$.isArray(tr[el])) { debug("An element in TRASH root is not an array. ", tr[el]); delete tr[el]; } else { toClean = []; - tr[el].forEach(function (obj, idx) { - if (typeof(obj) !== "object") { toClean.push(idx); return; } - if (!isFile(obj.element) && !isFolder(obj.element)) { toClean.push(idx); return; } - if (!$.isArray(obj.path)) { toClean.push(idx); return; } - }); + tr[el].forEach(addToClean); for (var i = toClean.length-1; i>=0; i--) { tr[el].splice(toClean[i], 1); } @@ -416,15 +543,7 @@ define([ } }; fixTrashRoot(files[TRASH]); - var fixFilesData = function (fd) { - for (var el in fd) { - if (typeof(fd[el]) !== "object") { - debug("An element in filesData was not an object. ", fd[el]); - delete fd[el]; - } - }; - }; - fixFilesData(files[FILES_DATA]); + var fixUnsorted = function (us) { var toClean = []; us.forEach(function (el, idx) { @@ -435,6 +554,26 @@ define([ }; fixUnsorted(files[UNSORTED]); + var rootFiles = getRootFiles().slice(); + var unsortedFiles = getUnsortedFiles().slice(); + var trashFiles = getTrashFiles().slice(); + var fixFilesData = function (fd) { + for (var el in fd) { + if (typeof(fd[el]) !== "object") { + debug("An element in filesData was not an object.", fd[el]); + delete fd[el]; + } else { + if (rootFiles.indexOf(el) === -1 + && unsortedFiles.indexOf(el) === -1 + && trashFiles.indexOf(el) === -1) { + debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el); + files[UNSORTED].push(el); + } + } + } + }; + fixFilesData(files[FILES_DATA]); + if (JSON.stringify(files) !== before) { var backup = JSON.parse(localStorage.oldFileSystem); backup.push(files); diff --git a/www/file/main.js b/www/file/main.js index 185c64b5c..3702c5df1 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -36,6 +36,12 @@ define([ var debug = config.debug = DEBUG ? console.log : function() {return;}; var logError = config.logError = console.error; var log = config.log = Cryptpad.log; + var DEBUG_LS = module.DEBUG_LS = { + resetLocalStorage : function () { + delete localStorage[LOCALSTORAGE_OPENED]; + delete localStorage[LOCALSTORAGE_LAST]; + } + }; var filesObject = module.files = { root: { @@ -245,13 +251,6 @@ define([ return new Date().getTime(); }; - var DEBUG = window.DEBUG = { - resetLocalStorage : function () { - delete localStorage[LOCALSTORAGE_OPENED]; - delete localStorage[LOCALSTORAGE_LAST]; - } - }; - var keyPressed = []; var pressKey = function (key, state) { if (state) { @@ -448,13 +447,33 @@ define([ return false; }; + // filesOp.moveElements is able to move several paths to a new location, including + // the Trash or the "Unsorted files" folder + var moveElements = function (paths, newPath, force, cb) { + var andThen = function () { + filesOp.moveElements(paths, newPath, cb); + }; + if (newPath[0] !== TRASH || force) { + andThen(); + return; + } + var msg = Messages._getKey('fm_removeSeveralDialog', [paths.length]); + if (paths.length === 1) { + var path = paths[0]; + var name = path[0] === UNSORTED ? filesOp.getTitle(filesOp.findElement(files, path)) : path[path.length - 1]; + msg = Messages._getKey('fm_removeDialog', [name]); + } + Cryptpad.confirm(msg, function () { + andThen(); + }); + }; // Drag & drop: // The data transferred is a stringified JSON containing the path of the dragged element var onDrag = function (ev, path) { var paths = []; var $element = $(ev.target).closest('li'); if ($element.hasClass('selected')) { - $selected = $iframe.find('.selected'); + var $selected = $iframe.find('.selected'); $selected.each(function (idx, elmt) { if ($(elmt).data('path')) { paths.push($(elmt).data('path')); @@ -476,28 +495,9 @@ define([ $iframe.find('.droppable').removeClass('droppable'); var data = ev.dataTransfer.getData("text"); var oldPaths = JSON.parse(data).path; - console.log(oldPaths); var newPath = $(ev.target).data('path') || $(ev.target).parent('li').data('path'); if (!oldPaths || !oldPaths.length || !newPath) { return; } - // Call removeElement when trying to move something into the trash - if (newPath[0] === TRASH) { - if (oldPaths.length === 1) { - filesOp.removeElement(oldPaths[0]); - module.displayDirectory([TRASH]); - } else { - Cryptpad.confirm(Messages._getKey('fm_removeSeveralDialog', [oldPaths.length]), function () { - oldPaths.forEach(function (oldPath) { - filesOp.removeElement(oldPath, null, true); - }); - module.displayDirectory([TRASH]); - }); - } - return; - } - oldPaths.forEach(function (oldPath) { - filesOp.moveElement(oldPath, newPath, null); - }); - refresh(); + moveElements(oldPaths, newPath, null, refresh); }; var addDragAndDropHandlers = function ($element, path, isFolder, droppable) { @@ -631,7 +631,7 @@ define([ displayRenameInput($name, newPath); }, 500); delete module.newFolder; - }; + } return $element; }; @@ -758,6 +758,10 @@ define([ return $fileHeader; }; + var allFilesSorted = function () { + return filesOp.getUnsortedFiles().length === 0; + }; + // Unsorted element are represented by "href" in an array: they don't have a filename // and they don't hav a hierarchical structure (folder/subfolders) var displayUnsorted = function ($container, $fileHeader) { @@ -824,7 +828,6 @@ define([ // NOTE: Elements in the trash are not using the same storage structure as the others var displayDirectory = module.displayDirectory = function (path) { currentPath = path; - module.resetTree(); $content.html(""); if (!path || path.length === 0) { path = [ROOT]; @@ -842,6 +845,8 @@ define([ return; } + module.resetTree(); + setLastOpenedFolder(path); var $title = createTitle(path); @@ -993,12 +998,7 @@ define([ }); }; - var allFilesSorted = function () { - return filesOp.getUnsortedFiles().length === 0; - }; - var createUnsorted = function ($container, path) { - if (allFilesSorted()) { return; } var $icon = $unsortedIcon.clone(); var isOpened = filesOp.comparePath(path, currentPath); var $unsortedElement = createTreeElement(UNSORTED_NAME, $icon, [UNSORTED], false, false, isOpened); @@ -1055,7 +1055,7 @@ define([ displayRenameInput($element, path); } else if($(this).hasClass("delete")) { - filesOp.removeElement(path, refresh); + moveElements([path], [TRASH], false, refresh); } else if ($(this).hasClass('open')) { $element.dblclick(); @@ -1067,7 +1067,7 @@ define([ e.stopPropagation(); var path = $(this).data('path'); var $element = $(this).data('element'); - if (!$element || !comparePath(path, [TRASH])) { + if (!$element || !filesOp.comparePath(path, [TRASH])) { log(Messages.fm_forbidden); debug("Trash tree context menu on a forbidden or unexisting element. ", $element, path); return; @@ -1128,17 +1128,31 @@ define([ pressKey(e.which, false); }); $(ifrw).on('keypress', function (e) { - console.log(e.which); if (e.which === 0) { var $selected = $iframe.find('.selected'); if (!$selected.length) { return; } - Cryptpad.confirm(Messages._getKey('fm_removeSeveralDialog', [$selected.length]), function () { - $selected.each(function (idx, elmt) { - if (!$(elmt).data('path')) { return; } - filesOp.removeElement($(elmt).data('path'), null, true); - }); - refresh(); + var paths = []; + $selected.each(function (idx, elmt) { + if (!$(elmt).data('path')) { return; } + paths.push($(elmt).data('path')); }); + if (filesOp.isPathInTrash(currentPath)) { + // If we are already in the trash, delete the elements permanently + var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); + if (paths.length === 1) { + var path = paths[0]; + var name = filesOp.isInTrashRoot(path) ? path[1] : path[path.length - 1]; + msg = Messages._getKey("fm_removePermanentlyDialog", [name]); + } + Cryptpad.confirm(msg, function(res) { + paths.forEach(function(p) { + filesOp.removeFromTrash(p); + }); + refresh(); + }); + return; + } + moveElements(paths, [TRASH], false, refresh); } }); };