require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), '/bower_components/chainpad-listmap/chainpad-listmap.js', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/textpatcher/TextPatcher.amd.js', '/customize/messages.js?app=file', 'json.sortify', '/common/cryptpad-common.js', '/common/fileObject.js', '/common/toolbar.js', '/customize/application_config.js' ], function (Config, Listmap, Crypto, TextPatcher, Messages, JSONSortify, Cryptpad, FO, Toolbar, AppConfig) { var module = window.MODULE = {}; var $ = window.jQuery; var saveAs = window.saveAs; var $iframe = $('#pad-iframe').contents(); var ifrw = $('#pad-iframe')[0].contentWindow; Cryptpad.addLoadingScreen(); var onConnectError = function (info) { Cryptpad.errorLoadingScreen(Messages.websocketError); }; var APP = window.APP = { editable: false, Cryptpad: Cryptpad, loggedIn: Cryptpad.isLoggedIn() }; var stringify = APP.stringify = function (obj) { return JSONSortify(obj); }; var ROOT = "root"; var ROOT_NAME = Messages.fm_rootName; var UNSORTED = "unsorted"; var UNSORTED_NAME = Messages.fm_unsortedName; var FILES_DATA = Cryptpad.storageKey; var FILES_DATA_NAME = Messages.fm_filesDataName; var TEMPLATE = "template"; var TEMPLATE_NAME = Messages.fm_templateName; var TRASH = "trash"; var TRASH_NAME = Messages.fm_trashName; var LOCALSTORAGE_LAST = "cryptpad-file-lastOpened"; var LOCALSTORAGE_OPENED = "cryptpad-file-openedFolders"; var LOCALSTORAGE_VIEWMODE = "cryptpad-file-viewMode"; var FOLDER_CONTENT_ID = "folderContent"; var NEW_FOLDER_NAME = Messages.fm_newFolder; var config = {}; config.storageKey = FILES_DATA; var DEBUG = config.DEBUG = true; var debug = config.debug = DEBUG ? function () { console.log.apply(console, arguments); } : function () { return; }; var logError = config.logError = function () { console.error.apply(console, arguments); }; var log = config.log = Cryptpad.log; var getLastOpenedFolder = function () { var path; try { path = localStorage[LOCALSTORAGE_LAST] ? JSON.parse(localStorage[LOCALSTORAGE_LAST]) : [UNSORTED]; } catch (e) { path = [UNSORTED]; } return path; }; var setLastOpenedFolder = function (path) { localStorage[LOCALSTORAGE_LAST] = JSON.stringify(path); }; var initLocalStorage = function () { try { var store = JSON.parse(localStorage[LOCALSTORAGE_OPENED]); if (!$.isArray(store)) { localStorage[LOCALSTORAGE_OPENED] = '[]'; } } catch (e) { localStorage[LOCALSTORAGE_OPENED] = '[]'; } }; 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") { logError("Incorrect view mode: ", mode); return; } localStorage[LOCALSTORAGE_VIEWMODE] = mode; }; var now = function () { return new Date().getTime(); }; var setEditable = function (state) { APP.editable = state; if (!state) { $iframe.find('#content').addClass('readonly'); $iframe.find('[draggable="true"]').attr('draggable', false); } else { $iframe.find('#content').removeClass('readonly'); $iframe.find('[draggable="false"]').attr('draggable', true); } }; // Icons var $folderIcon = $('', {"class": "fa fa-folder folder icon"}); var $folderEmptyIcon = $folderIcon.clone(); var $folderOpenedIcon = $('', {"class": "fa fa-folder-open folder"}); var $folderOpenedEmptyIcon = $folderOpenedIcon.clone(); var $fileIcon = $('', {"class": "fa fa-file-text-o file icon"}); var $padIcon = $('', {"class": "fa fa-file-word-o file icon"}); var $codeIcon = $('', {"class": "fa fa-file-code-o file icon"}); var $slideIcon = $('', {"class": "fa fa-file-powerpoint-o file icon"}); var $pollIcon = $('', {"class": "fa fa-calendar file icon"}); var $upIcon = $('', {"class": "fa fa-arrow-circle-up"}); var $unsortedIcon = $('', {"class": "fa fa-files-o"}); var $templateIcon = $('', {"class": "fa fa-cubes"}); var $trashIcon = $('', {"class": "fa fa-trash"}); 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 $sortAscIcon = $('', {"class": "fa fa-angle-up sortasc"}); var $sortDescIcon = $('', {"class": "fa fa-angle-down sortdesc"}); var $closeIcon = $('', {"class": "fa fa-window-close"}); var $backupIcon = $('', {"class": "fa fa-life-ring"}); var init = function (proxy) { var files = proxy.drive; var isOwnDrive = function () { return Cryptpad.getUserHash() === APP.hash || localStorage.FS_hash === APP.hash; }; var isWorkgroup = function () { return files.workgroup === 1; }; config.workgroup = isWorkgroup(); var filesOp = FO.init(files, config); filesOp.fixFiles(); var error = filesOp.error; var $tree = $iframe.find("#tree"); var $content = $iframe.find("#content"); var $driveToolbar = $iframe.find("#driveToolbar"); var $contextMenu = $iframe.find("#treeContextMenu"); var $contentContextMenu = $iframe.find("#contentContextMenu"); var $defaultContextMenu = $iframe.find("#defaultContextMenu"); var $trashTreeContextMenu = $iframe.find("#trashTreeContextMenu"); var $trashContextMenu = $iframe.find("#trashContextMenu"); // TOOLBAR var getLastName = function (cb) { Cryptpad.getAttribute('username', function (err, userName) { cb(err, userName || ''); }); }; // Store the object sent for the "change username" button so that we can update the field value correctly var userNameButtonObject = APP.userName = {}; /* add a "change username" button */ if (!APP.readOnly) { getLastName(function (err, lastName) { APP.userName.lastName = lastName; APP.$displayName.text(lastName || Messages.anonymous); }); } else { APP.$displayName.html('' + Messages.readonly + ''); } // FILE MANAGER // _WORKGROUP_ and other people drive : display Documents as main page var currentPath = module.currentPath = isOwnDrive() ? getLastOpenedFolder() : [ROOT]; var lastSelectTime; var selectedElement; if (!APP.readOnly) { setEditable(true); } var appStatus = { isReady: true, _onReady: [], onReady: function (handler) { if (appStatus.isReady) { handler(); return; } appStatus._onReady.push(handler); }, ready: function (state) { appStatus.isReady = state; if (state) { appStatus._onReady.forEach(function (h) { h(); }); appStatus._onReady = []; } } }; var removeSelected = function () { $iframe.find('.selected').removeClass("selected"); }; var removeInput = function () { $iframe.find('li > span:hidden').removeAttr('style'); $iframe.find('li > input').remove(); }; 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) { error("Unable to format that string to a date with .toLocaleString", sDate, e); } return ret; }; var openFile = function (fileEl) { window.open(fileEl); }; var refresh = APP.refresh = function () { module.displayDirectory(currentPath); }; // Replace a file/folder name by an input to change its value var displayRenameInput = function ($element, path) { if (!APP.editable) { return; } if (!path || path.length < 2) { logError("Renaming a top level element (root, trash or filesData) is forbidden."); return; } removeInput(); removeSelected(); var $name = $element.find('.name'); if (!$name.length) { $name = $element.find('.element'); } $name.hide(); var name = path[path.length - 1]; var $input = $('', { placeholder: name, value: name }); $input.on('keyup', function (e) { if (e.which === 13) { removeInput(); filesOp.renameElement(path, $input.val(), function () { refresh(); }); } }); //$element.parent().append($input); $name.after($input); $input.focus(); $input.select(); // We don't want to open the file/folder when clicking on the input $input.on('click dblclick', function (e) { removeSelected(); e.stopPropagation(); }); // Remove the browser ability to drag text from the input to avoid // triggering our drag/drop event handlers $input.on('dragstart dragleave drag drop', function (e) { e.preventDefault(); e.stopPropagation(); }); // Make the parent element non-draggable when selecting text in the field // since it would remove the input $input.on('mousedown', function (e) { e.stopPropagation(); $input.parents('li').attr("draggable", false); }); $input.on('mouseup', function (e) { e.stopPropagation(); $input.parents('li').attr("draggable", true); }); }; // Add the "selected" class to the "li" corresponding to the clicked element var onElementClick = function (e, $element, path) { // If "Ctrl" is pressed, do not remove the current selection removeInput(); if (!e || !e.ctrlKey) { removeSelected(); } if (!$element.is('li')) { $element = $element.closest('li'); } if (!$element.length) { log(Messages.fm_selectError); return; } if (!$element.hasClass("selected")) { $element.addClass("selected"); lastSelectTime = now(); } else { $element.removeClass("selected"); } }; // Open the selected context menu on the closest "li" element var openContextMenu = function (e, $menu) { module.hideMenu(); e.stopPropagation(); var path = $(e.target).closest('li').data('path'); if (!path) { return false; } if (!APP.editable) { $menu.find('a.editable').parent('li').hide(); } if (!isOwnDrive()) { $menu.find('a.own').parent('li').hide(); } $menu.css({ display: "block", left: e.pageX, top: e.pageY }); if ($menu.find('li:visible').length === 0) { debug("No visible element in the context menu. Abort."); $menu.hide(); return true; } // $element should be the
  • var $element = $(e.target).closest('li'); onElementClick(undefined, $element); if (!$element.length) { logError("Unable to locate the .element tag", e.target); $menu.hide(); log(Messages.fm_contextMenuError); return false; } $menu.find('a').data('path', path); $menu.find('a').data('element', $element); return false; }; var openDirectoryContextMenu = function (e) { var $element = $(e.target).closest('li'); $contextMenu.find('li').show(); if ($element.find('.file-element').length) { $contextMenu.find('a.newfolder').parent('li').hide(); } else { $contextMenu.find('a.open_ro').parent('li').hide(); } openContextMenu(e, $contextMenu); return false; }; var openDefaultContextMenu = function (e) { var $element = $(e.target).closest('li'); $defaultContextMenu.find('li').show(); if ($element.find('.file-element').length) { $defaultContextMenu.find('a.newfolder').parent('li').hide(); } else { $defaultContextMenu.find('a.open_ro').parent('li').hide(); } openContextMenu(e, $defaultContextMenu); return false; }; var openTrashTreeContextMenu = function (e) { openContextMenu(e, $trashTreeContextMenu); return false; }; var openTrashContextMenu = function (e) { var path = $(e.target).closest('li').data('path'); if (!path) { return; } $trashContextMenu.find('li').show(); if (path.length > 4) { $trashContextMenu.find('a.restore').parent('li').hide(); $trashContextMenu.find('a.properties').parent('li').hide(); } openContextMenu(e, $trashContextMenu); return false; }; var openContentContextMenu = function (e) { module.hideMenu(); e.stopPropagation(); var path = $(e.target).closest('#' + FOLDER_CONTENT_ID).data('path'); if (!path) { return; } var $menu = $contentContextMenu; removeSelected(); if (!APP.editable) { $menu.find('a.editable').parent('li').hide(); } if (!isOwnDrive()) { $menu.find('a.own').parent('li').hide(); } $menu.find('[data-type]').each(function (idx, el) { if (AppConfig.availablePadTypes.indexOf($(el).attr('data-type')) === -1) { $(el).hide(); } }); $menu.css({ display: "block", left: e.pageX, top: e.pageY }); if ($menu.find('li:visible').length === 0) { debug("No visible element in the context menu. Abort."); $menu.hide(); return true; } $menu.find('a').data('path', path); return false; }; var getElementName = function (path) { // Trash root if (filesOp.isInTrashRoot(path)) { return path[0]; } // Root or trash if (filesOp.isPathInRoot(path) || filesOp.isPathInTrash(path)) { return path[path.length - 1]; } // Unsorted or template if (filesOp.isPathInUnsorted(path) || filesOp.isPathInTemplate(path)) { var file = filesOp.findElement(files, path); if (filesOp.isFile(file) && filesOp.getTitle(file)) { return filesOp.getTitle(file); } } // default return "???"; }; // 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) { if (!APP.editable) { return; } var andThen = function () { filesOp.moveElements(paths, newPath, cb); }; // "force" is currently unused but may be configurable by user 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 (res) { if (!res) { return; } 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')) { var $selected = $iframe.find('.selected'); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { var val = filesOp.findElement(files, ePath); if (!val) { return; } // Error? A ".selected" element in not in the object paths.push({ path: ePath, value: { name: getElementName(ePath), el: val } }); } }); } else { removeSelected(); $element.addClass('selected'); var val = filesOp.findElement(files, path); if (!val) { return; } // The element in not in the object paths = [{ path: path, value: { name: getElementName(path), el: val } }]; } var data = { 'path': paths }; ev.dataTransfer.setData("text", stringify(data)); }; var onDrop = function (ev) { ev.preventDefault(); $iframe.find('.droppable').removeClass('droppable'); var data = ev.dataTransfer.getData("text"); var oldPaths = JSON.parse(data).path; if (!oldPaths) { return; } // Dropped elements can be moved from the same file manager or imported from another one. // A moved element should be removed from its previous location var movedPaths = []; var importedElements = []; oldPaths.forEach(function (p) { var el = filesOp.findElement(files, p.path); if (el && (stringify(el) === stringify(p.value.el) || !p.value || !p.value.el)) { movedPaths.push(p.path); } else { importedElements.push(p.value); } }); var newPath = $(ev.target).data('path') || $(ev.target).parent('li').data('path'); if (!newPath) { return; } if (movedPaths && movedPaths.length) { moveElements(movedPaths, newPath, null, refresh); } if (importedElements && importedElements.length) { filesOp.importElements(importedElements, newPath, refresh); } }; var addDragAndDropHandlers = function ($element, path, isFolder, droppable) { if (!APP.editable) { return; } // "dragenter" is fired for an element and all its children // "dragleave" may be fired when entering a child // --> we use pointer-events: none in CSS, but we still need a counter to avoid some issues // --> 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; $element.on('dragstart', function (e) { e.stopPropagation(); counter = 0; onDrag(e.originalEvent, path); }); $element.on('mousedown', function (e) { e.stopPropagation(); }); // Add drop handlers if we are not in the trash and if the element is a folder if (!droppable || !isFolder) { return; } $element.on('dragover', function (e) { e.preventDefault(); }); $element.on('drop', function (e) { onDrop(e.originalEvent); }); $element.on('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); counter++; $element.addClass('droppable'); }); $element.on('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); counter--; if (counter <= 0) { counter = 0; $element.removeClass('droppable'); } }); }; // In list mode, display metadata from the filesData object // _WORKGROUP_ : Do not display title, atime and ctime columns since we don't have files data var addFileData = function (element, key, $span, displayTitle) { if (!filesOp.isFile(element)) { return; } // The element with the class '.name' is underlined when the 'li' is hovered var $name = $('', {'class': 'name', title: key}).text(key); $span.html(''); $span.append($name); if (!filesOp.getFileData(element)) { return; } var hrefData = Cryptpad.parsePadUrl(element); var data = filesOp.getFileData(element); var type = Messages.type[hrefData.type] || hrefData.type; var $title = $('', {'class': 'title listElement', title: data.title}).text(data.title); var $type = $('', {'class': 'type listElement', title: type}).text(type); var $adate = $('', {'class': 'atime listElement', title: getDate(data.atime)}).text(getDate(data.atime)); var $cdate = $('', {'class': 'ctime listElement', title: getDate(data.ctime)}).text(getDate(data.ctime)); if (displayTitle && !isWorkgroup()) { $span.append($title); } $span.append($type); if (!isWorkgroup()) { $span.append($adate).append($cdate); } }; var addFolderData = function (element, key, $span) { if (!element || !filesOp.isFolder(element)) { return; } $span.html(''); // The element with the class '.name' is underlined when the 'li' is hovered var sf = filesOp.hasSubfolder(element); var files = filesOp.hasFile(element); var $name = $('', {'class': 'name', title: key}).text(key); var $subfolders = $('', {'class': 'folders listElement', title: sf}).text(sf); var $files = $('', {'class': 'files listElement', title: files}).text(files); $span.append($name).append($subfolders).append($files); }; var getFileIcon = function (href) { var $icon = $fileIcon.clone(); if (href.indexOf('/pad/') !== -1) { $icon = $padIcon.clone(); } else if (href.indexOf('/code/') !== -1) { $icon = $codeIcon.clone(); } else if (href.indexOf('/slide/') !== -1) { $icon = $slideIcon.clone(); } else if (href.indexOf('/poll/') !== -1) { $icon = $pollIcon.clone(); } return $icon; }; // Create the "li" element corresponding to the file/folder located in "path" 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]; elPath.forEach(function (k) { newPath.push(k); }); } else { key = elPath; newPath.push(key); } var element = filesOp.findElement(files, newPath); var $icon = !isFolder ? getFileIcon(element) : undefined; var liClass = 'file-item file-element element'; if (isFolder) { liClass = 'folder-item folder-element element'; $icon = filesOp.isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone(); } var $element = $('
  • ', { draggable: true }); if (isFolder) { addFolderData(element, key, $element); } else { addFileData(element, key, $element, true); } $element.prepend($icon).dblclick(function () { if (isFolder) { module.displayDirectory(newPath); return; } if (isTrash) { return; } openFile(root[key]); }); $element.addClass(liClass); $element.data('path', newPath); addDragAndDropHandlers($element, newPath, isFolder, !isTrash); $element.click(function(e) { e.stopPropagation(); onElementClick(e, $element, newPath); }); if (!isTrash) { $element.contextmenu(openDirectoryContextMenu); } else { $element.contextmenu(openTrashContextMenu); } var isNewFolder = module.newFolder && filesOp.comparePath(newPath, module.newFolder); if (isNewFolder) { appStatus.onReady(function () { window.setTimeout(function () { displayRenameInput($element, newPath); }, 0); }); delete module.newFolder; } return $element; }; // Display the full path in the title when displaying a directory from the trash var getTrashTitle = function (path) { if (!path[0] || path[0] !== TRASH) { return; } var title = TRASH_NAME; for (var i=1; i', {'class': 'path unselectable'}); path.forEach(function (p, idx) { if (isTrash && [1,2].indexOf(idx) !== -1) { return; } var $span = $('', {'class': 'element'}); if (idx < path.length - 1) { $span.addClass('clickable'); $span.click(function (e) { module.displayDirectory(path.slice(0, idx + 1)); }); } var name = p; if (idx === 0) { name = getPrettyName(p); } else { $title.append(' > '); } $span.text(name).appendTo($title); }); return $title; }; var createInfoBox = function (path) { var $box = $('
    ', {'class': 'info-box'}); var msg; switch (path[0]) { case ROOT: msg = Messages.fm_info_root; break; case UNSORTED: msg = Messages.fm_info_unsorted; break; case TEMPLATE: msg = Messages.fm_info_template; break; case TRASH: msg = Messages.fm_info_trash; break; case FILES_DATA: msg = Messages.fm_info_allFiles; break; default: msg = undefined; } if (!msg || Cryptpad.getLSAttribute('hide-info-' + path[0]) === '1') { $box.hide(); } else { $box.text(msg); var $close = $closeIcon.clone().css({ 'cursor': 'pointer', 'margin-left': '10px', title: Messages.fm_closeInfoBox }).on('click', function () { $box.hide(); Cryptpad.setLSAttribute('hide-info-' + path[0], '1'); }); $box.prepend($close); } return $box; }; // Create the button allowing the user to switch from list to icons modes var createViewModeButton = function () { var $block = $('
    ', { 'class': 'dropdown-bar right changeViewModeContainer' }); var $listButton = $('