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', '/file/fileObject.js', '/common/toolbar.js', '/customize/pad.js' ], function (Config, Listmap, Crypto, TextPatcher, Messages, JSONSortify, Cryptpad, FO, Toolbar) { var module = window.MODULE = {}; var $ = window.jQuery; var saveAs = window.saveAs; var $iframe = $('#pad-iframe').contents(); var ifrw = $('#pad-iframe')[0].contentWindow; var APP = window.APP = { $bar: $iframe.find('#toolbar'), editable: false }; 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 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 DEBUG_LS = APP.DEBUG_LS = { resetLocalStorage : function () { delete localStorage[LOCALSTORAGE_OPENED]; delete localStorage[LOCALSTORAGE_LAST]; } }; 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 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('[draggable="true"]').attr('draggable', false); } else { $iframe.find('[draggable="false"]').attr('draggable', true); } }; var keyPressed = []; var pressKey = function (key, state) { if (state) { if (keyPressed.indexOf(key) === -1) { keyPressed.push(key); } return; } var idx = keyPressed.indexOf(key); if (idx !== -1) { keyPressed.splice(idx, 1); } }; var init = function (files) { var filesOp = FO.init(files, config); filesOp.fixFiles(); var error = filesOp.error; // TOOLBAR var getLastName = function (cb) { cb(null, files['cryptpad.username'] || ''); }; var setName = APP.setName = function (newName) { if (typeof(newName) !== 'string') { return; } var myUserNameTemp = Cryptpad.fixHTML(newName.trim()); if(myUserNameTemp.length > 32) { myUserNameTemp = myUserNameTemp.substr(0, 32); } var myUserName = myUserNameTemp; files['cryptpad.username'] = myUserName; APP.userName.lastName = myUserName; var $button = APP.$userNameButton; var $span = $('
').append($button.find('span').clone()).html(); $button.html($span + myUserName); }; var $userBlock = APP.$bar.find('.' + Toolbar.constants.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 */ getLastName(function (err, lastName) { userNameButtonObject.lastName = lastName; var $username = APP.$userNameButton = Cryptpad.createButton('username', false, userNameButtonObject, setName).hide(); $userBlock.append($username); $username.append(lastName); $username.show(); }); // FILE MANAGER var currentPath = module.currentPath = getLastOpenedFolder(); var lastSelectTime; var selectedElement; var $tree = $iframe.find("#tree"); var $content = $iframe.find("#content"); var $contextMenu = $iframe.find("#contextMenu"); var $contentContextMenu = $iframe.find("#contentContextMenu"); var $trashTreeContextMenu = $iframe.find("#trashTreeContextMenu"); var $trashContextMenu = $iframe.find("#trashContextMenu"); // Icons var $folderIcon = $('', {"class": "fa fa-folder folder", style:"color:#FEDE8B;text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;"}); var $folderEmptyIcon = $folderIcon.clone(); var $folderOpenedIcon = $('', {"class": "fa fa-folder-open folder", style:"color:#FEDE8B;text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;"}); var $folderOpenedEmptyIcon = $folderOpenedIcon.clone(); var $fileIcon = $('', {"class": "fa fa-file-text-o file"}); var $upIcon = $('', {"class": "fa fa-arrow-circle-up"}); var $unsortedIcon = $('', {"class": "fa fa-files-o"}); 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"}); var $sortDescIcon = $('', {"class": "fa fa-angle-down"}); 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(); }); _onReady = []; } } }; var ownFileManager = function () { return localStorage.FS_hash === APP.hash; }; var removeSelected = function () { $iframe.find('.selected').removeClass("selected"); }; var removeInput = function () { $iframe.find('li > span:hidden').show(); $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 = function () { module.displayDirectory(currentPath); }; // Replace a file/folder name by an input to change its value var displayRenameInput = function ($element, path) { if (!APP.editable) { debug("Read-only mode"); return; } if (!path || path.length < 2) { logError("Renaming a top level element (root, trash or filesData) is forbidden."); return; } removeInput(); removeSelected(); $element.hide(); var name = path[path.length - 1]; var $input = $('', { placeholder: name, value: name }); $input.on('keyup', function (e) { if (e.which === 13) { filesOp.renameElement(path, $input.val(), function () { refresh(); }); removeInput(); } }); //$element.parent().append($input); $element.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 () { $input.parents('li').attr("draggable", false); }); $input.on('mouseup', function () { $input.parents('li').attr("draggable", true); }); }; // Add the "selected" class to the "li" corresponding to the clicked element var onElementClick = function ($element, path) { // If "Ctrl" is pressed, do not remove the current selection if (keyPressed.indexOf(17) === -1) { 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(); var path = $(e.target).closest('li').data('path'); if (!path) { return; } if (!APP.editable) { $menu.find('a.editable').parent('li').hide(); } if (!ownFileManager()) { $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 , find it if it's not the case var $element = $(e.target).closest('li').children('span.element'); onElementClick($element); if (!$element.length) { logError("Unable to locate the .element tag", e.target); $menu.hide(); log(Messages.fm_contextMenuError); return; } $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.hasClass('file-element')) { $contextMenu.find('a.newfolder').parent('li').hide(); } openContextMenu(e, $contextMenu); 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 (!ownFileManager()) { $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; } $menu.find('a').data('path', path); 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) { if (!APP.editable) { debug("Read-only mode"); return; } 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 (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) { if ($(elmt).data('path')) { paths.push($(elmt).data('path')); } }); } else { removeSelected(); $element.addClass('selected'); paths = [path]; } var data = { 'path': paths }; ev.dataTransfer.setData("text", JSON.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; var newPath = $(ev.target).data('path') || $(ev.target).parent('li').data('path'); if (!oldPaths || !oldPaths.length || !newPath) { return; } moveElements(oldPaths, newPath, null, refresh); }; var addDragAndDropHandlers = function ($element, path, isFolder, droppable) { if (!APP.editable) { debug("Read-only mode"); 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); }); // 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 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) { $span.append($title); } $span.append($type).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); }; // 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 = $fileIcon.clone(); var spanClass = 'file-element element'; var liClass = 'file-item'; if (isFolder) { spanClass = 'folder-element element'; liClass = 'folder-item'; $icon = filesOp.isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone(); } var $name = $('', { 'class': spanClass }).text(key); if (isFolder) { addFolderData(element, key, $name); } else { addFileData(element, key, $name, true); } var $element = $('
  • ', { draggable: true }).append($icon).append($name).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($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($name, 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').text(name); if (path.length > 1) { var $parentFolder = $upIcon.clone().addClass("parentFolder") .click(function() { var newPath = path.slice(); newPath.pop(); if (isTrash && path.length === 4) { // path = [TRASH, "{DirName}", 0, 'element'] // --> parent is TRASH newPath = [TRASH]; } module.displayDirectory(newPath); }); $title.append($parentFolder); } return $title; }; // Create the button allowing the user to switch from list to icons modes var createViewModeButton = function () { var $block = $('
    ', { 'class': 'btn-group topButtonContainer changeViewModeContainer' }); var $listButton = $('