// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors // // SPDX-License-Identifier: AGPL-3.0-or-later define([ 'jquery', '/api/config', '/common/toolbar.js', 'json.sortify', '/common/common-util.js', '/common/common-hash.js', '/common/common-ui-elements.js', '/common/common-interface.js', '/common/common-constants.js', '/common/common-feedback.js', '/common/inner/share.js', '/common/inner/access.js', '/common/inner/properties.js', '/components/nthen/index.js', '/common/hyperscript.js', '/common/proxy-manager.js', '/customize/application_config.js', '/customize/messages.js', '/customize/pages.js', '/common/pad-types.js', '/common/onlyoffice/broken-formats.js', '/common/common-icons.js' ], function ( $, ApiConfig, Toolbar, JSONSortify, Util, Hash, UIElements, UI, Constants, Feedback, Share, Access, Properties, nThen, h, ProxyManager, AppConfig, Messages, Pages, PadTypes, BrokenFormats, Icons) { var APP = window.APP = { editable: false, online: false, mobile: function () { if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } else { return $('body').width() <= 600; } }, isMac: navigator.platform === "MacIntel", allowFolderUpload: File.prototype.hasOwnProperty("webkitRelativePath"), }; var onConnectEvt = Util.mkEvent(true); var stringify = function (obj) { return JSONSortify(obj); }; var E_OVER_LIMIT = 'E_OVER_LIMIT'; var ROOT = "root"; var ROOT_NAME = Messages.fm_rootName; var SEARCH = "search"; var SEARCH_NAME = Messages.fm_searchName; var TRASH = "trash"; var TRASH_NAME = Messages.fm_trashName; var FILES_DATA = Constants.storageKey; var FILES_DATA_NAME = Messages.fm_filesDataName; var TEMPLATE = "template"; var TEMPLATE_NAME = Messages.fm_templateName; var RECENT = "recent"; var RECENT_NAME = Messages.fm_recentPadsName; var OWNED = "owned"; var OWNED_NAME = Messages.fm_ownedPadsName; var TAGS = "tags"; var TAGS_NAME = Messages.fm_tagsName; var SHARED_FOLDER = 'sf'; var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName; var FILTER = "filter"; // Icons var folder = 'folder'; var folderOpen = 'folder-open'; var sharedFolder = 'drive-shared-folder'; var expandAll = 'expand'; var collapseAll = 'collapse'; var readOnly = 'read-only'; var preview = 'preview'; var rename = 'rename'; var palette = 'color-palette'; var trashFull = 'trash-full'; var copy = 'copy'; var destroy = 'destroy'; var access = 'access'; var properties = 'properties'; var tags = 'tag'; var uploadFile = 'drive-upload-file'; var uploadFolder = 'drive-upload-folder'; var trashEmpty = 'trash-empty'; var restore = 'restore'; var showParent = 'location'; var download = 'download'; var $folderIcon = $(Icons.get(folder,{"class": "cp-app-drive-icon-folder cp-app-drive-content-icon"})); var $fileMenuIcon = $(Icons.get('ellipsis-horizontal')); var $folderEmptyIcon = $folderIcon.clone(); var $folderOpenedIcon = $(Icons.get('folder-open', {"class": "cp-app-drive-icon-folder cp-app-drive-content-icon"})); var $folderOpenedEmptyIcon = $folderOpenedIcon.clone(); var $sharedFolderIcon = $(Icons.get('drive-shared-folder', {"class": "cp-app-drive-icon-folder"})); var $sharedFolderOpenedIcon = $(Icons.get('folder-open', {"class": "cp-app-drive-icon-folder"})); var $unsortedIcon = $(Icons.get('copy')); var $templateIcon = $(Icons.get('file-template')); var $recentIcon = $(Icons.get('drive-recent')); var $trashIcon = $(Icons.get('trash-full')); var $trashEmptyIcon = $(Icons.get('trash-empty')); var $sortAscIcon = $(Icons.get('sort-asc', {'class': 'sortasc'})); var $sortDescIcon = $(Icons.get('sort-desc', {'class': 'sortdesc'})); var $closeIcon = $(Icons.get('close')); var $searchIcon = $($(Icons.get('search', {'class': 'cp-app-drive-tree-search-icon'}))); var $addIcon = $(Icons.get('add')); var $renamedIcon = $(Icons.get('renamed')); var $readonlyIcon = $(Icons.get(readOnly)); var $ownedIcon = $(Icons.get('document-owner')); var $sharedIcon = $(Icons.get('share')); var $tagsIcon = $(Icons.get(tags)); var $passwordIcon = $(Icons.get('drive-password-document')); var $restrictedIcon = $(Icons.get('restricted')); var $expirableIcon = $(Icons.get('expire')); var $separator = $('
', {"class": "dropdown-divider"}); var LS_VIEWMODE = "app-drive-viewMode"; var FOLDER_CONTENT_ID = "cp-app-drive-content-folder"; var config = {}; var DEBUG = config.DEBUG = false; 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 = UI.log; var localStore = window.cryptpadStore; APP.store = {}; $(window).keydown(function (e) { if (e.which === 70 && e.ctrlKey) { e.preventDefault(); e.stopPropagation(); if (APP.displayDirectory) { APP.displayDirectory([SEARCH]); } return; } }); var makeLS = function (teamId) { var suffix = teamId ? ('-' + teamId) : ''; var LS_LAST = "app-drive-lastOpened" + suffix; var LS_OPENED = "app-drive-openedFolders" + suffix; var LS = {}; LS.getLastOpenedFolder = function () { var path; try { path = APP.store[LS_LAST] ? JSON.parse(APP.store[LS_LAST]) : [ROOT]; } catch (e) { path = [ROOT]; } return path; }; LS.setLastOpenedFolder = function (path) { if (path[0] === SEARCH) { return; } APP.store[LS_LAST] = JSON.stringify(path); localStore.put(LS_LAST, JSON.stringify(path)); }; LS.wasFolderOpened = function (path) { var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); return stored.indexOf(JSON.stringify(path)) !== -1; }; LS.getOpenedFolders = function () { var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); return stored.map(function(s) { try { return JSON.parse(s); } catch (e) { return null; } }).filter(function(p) { return p !== null; }); }; LS.setFolderOpened = function (path, opened) { var s = JSON.stringify(path); var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); if (opened && stored.indexOf(s) === -1) { stored.push(s); } if (!opened) { var idx = stored.indexOf(s); if (idx !== -1) { stored.splice(idx, 1); } } APP.store[LS_OPENED] = JSON.stringify(stored); localStore.put(LS_OPENED, JSON.stringify(stored)); }; LS.removeFoldersOpened = function (parentPath) { var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); var s = JSON.stringify(parentPath).slice(0, -1); for (var i = stored.length - 1 ; i >= 0 ; i--) { if (stored[i].indexOf(s) === 0) { stored.splice(i, 1); } } APP.store[LS_OPENED] = JSON.stringify(stored); localStore.put(LS_OPENED, JSON.stringify(stored)); }; LS.renameFoldersOpened = function (parentPath, newName) { var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); var s = JSON.stringify(parentPath).slice(0, -1); var newParentPath = parentPath.slice(); newParentPath[newParentPath.length - 1] = newName; var sNew = JSON.stringify(newParentPath).slice(0, -1); for (var i = 0 ; i < stored.length ; i++) { if (stored[i].indexOf(s) === 0) { stored[i] = stored[i].replace(s, sNew); } } APP.store[LS_OPENED] = JSON.stringify(stored); localStore.put(LS_OPENED, JSON.stringify(stored)); }; LS.moveFoldersOpened = function (previousPath, newPath) { var stored = JSON.parse(APP.store[LS_OPENED] || '[]'); var s = JSON.stringify(previousPath).slice(0, -1); var sNew = JSON.stringify(newPath).slice(0, -1); if (s === sNew ) { return; } // move to itself if (sNew.indexOf(s) === 0) { return; } // move to subfolder sNew = JSON.stringify(newPath.concat(previousPath[previousPath.length - 1])).slice(0, -1); for (var i = 0 ; i < stored.length ; i++) { if (stored[i].indexOf(s) === 0) { stored[i] = stored[i].replace(s, sNew); } } APP.store[LS_OPENED] = JSON.stringify(stored); localStore.put(LS_OPENED, JSON.stringify(stored)); }; return LS; }; var getViewModeClass = function (forceList) { var mode = APP.store[LS_VIEWMODE]; if (mode === 'list' || forceList) { return 'cp-app-drive-content-list'; } return 'cp-app-drive-content-grid'; }; var getViewMode = function () { return APP.store[LS_VIEWMODE] || 'grid'; }; var setViewMode = function (mode) { if (typeof(mode) !== "string") { logError("Incorrect view mode: ", mode); return; } APP.store[LS_VIEWMODE] = mode; localStore.put(LS_VIEWMODE, mode); }; // Handle disconnect/reconnect // If isHistory and isSf are both false, update the "APP.online" flag // If isHistory is true, update the "APP.history" flag // isSf is used to detect offline shared folders: setEditable is called on displayDirectory var setEditable = function (state, isHistory, isSf) { if (APP.closed || !APP.$content || !$.contains(document.documentElement, APP.$content[0])) { return; } if (isHistory) { APP.history = !state; } else if (!isSf) { APP.online = state; } state = APP.online && !APP.history && state; APP.editable = !APP.readOnly && state; if (APP.editable) { onConnectEvt.fire(); } if (!state) { APP.$content.addClass('cp-app-drive-readonly'); if (!APP.history || !APP.online) { $('#cp-app-drive-connection-state').show(); } else { $('#cp-app-drive-connection-state').hide(); } $('[draggable="true"]').attr('draggable', false); } else { APP.$content.removeClass('cp-app-drive-readonly'); $('#cp-app-drive-connection-state').hide(); $('[draggable="false"]').attr('draggable', true); } }; var copyObjectValue = function (objRef, objToCopy) { for (var k in objRef) { delete objRef[k]; } $.extend(true, objRef, objToCopy); }; APP.selectedFiles = []; var findElementId = function ($element) { var isTrashed = $element.data("path")[0] === TRASH; let elementId; if (isTrashed) { elementId = $element.data("path").join(','); } else { elementId = $element.data("path").slice(-1)[0]; } return elementId; }; var isElementSelected = function ($element) { var elementId = findElementId($element); return APP.selectedFiles.indexOf(elementId) !== -1; }; var selectElement = function ($element) { var elementId = findElementId($element); if (APP.selectedFiles.indexOf(elementId) === -1) { APP.selectedFiles.push(elementId); } $element.addClass("cp-app-drive-element-selected"); }; var unselectElement = function ($element) { var elementId = findElementId($element); var index = APP.selectedFiles.indexOf(elementId); if (index !== -1) { APP.selectedFiles.splice(index, 1); } $element.removeClass("cp-app-drive-element-selected"); }; var findSelectedElements = function () { return $(".cp-app-drive-element-selected"); }; var getNewPadTypes = function () { var arr = []; PadTypes.availableTypes.forEach(function (type) { if (AppConfig.hiddenTypes.indexOf(type) !== -1) { return; } if (!APP.loggedIn && AppConfig.registeredOnlyTypes && AppConfig.registeredOnlyTypes.indexOf(type) !== -1) { return; } arr.push(type); }); return arr; }; var createContextMenu = function (common) { var metadataMgr = common.getMetadataMgr(); var priv = metadataMgr.getPrivateData(); APP.premiumPlan = priv.plan; var getOpenIn = function (app) { var icon = AppConfig.applicationsIcon[app] || app; var iconHtml = Icons.get(icon).outerHTML; var html = iconHtml + Messages.type[app]; return Messages._getKey('fc_openIn', [html]); }; var restricted = {}; var enabled = []; var isAppEnabled = function (app) { return enabled.includes(app); }; getNewPadTypes().forEach(function (app) { if (!Array.isArray(AppConfig.availablePadTypes)) { return void enabled.push(app); } var registered = common.isLoggedIn() || !(AppConfig.registeredOnlyTypes || []).includes(app); restricted[app] = common.checkRestrictedApp(app); var e = PadTypes.isAvailable(app) && registered && restricted[app] >= 0; if (e) { enabled.push(app); } }); var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [ h('ul.dropdown-menu', { 'role': 'menu', 'aria-labelledby': 'dropdownMenu', 'style': 'display:block;position:static;margin-bottom:5px;' }, [ h('span.cp-app-drive-context-noAction.dropdown-item.disabled', Messages.fc_noAction || "No action possible"), h('li', h('a.cp-app-drive-context-preview.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(preview), Messages.pad_mediatagPreview ])), h('li', h('a.cp-app-drive-context-open.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(folderOpen), Messages.fc_open ] )), h('li', h('a.cp-app-drive-context-openfolder.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(folderOpen), Messages.fc_open ])), h('li', h('a.cp-app-drive-context-openro.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(readOnly), h('span.cp-text', Messages.fc_open_ro) ])), isAppEnabled('code') ? h('li', UI.setHTML(h('a.cp-app-drive-context-openincode.dropdown-item', { 'tabindex': '-1', }, [Icons.get('select')]), getOpenIn('code'))) : undefined, isAppEnabled('sheet') ? h('li', UI.setHTML(h('a.cp-app-drive-context-openinsheet.dropdown-item', { 'tabindex': '-1', }, [Icons.get('select')]), getOpenIn('sheet'))) : undefined, isAppEnabled('doc') ? h('li', UI.setHTML(h('a.cp-app-drive-context-openindoc.dropdown-item' + (restricted.doc === 0 ? '.cp-app-disabled' : ''), { 'tabindex': '-1', }, [Icons.get('select')]), getOpenIn('doc'))) : undefined, isAppEnabled('presentation') ? h('li', UI.setHTML(h('a.cp-app-drive-context-openinpresentation.dropdown-item' + (restricted.presentation === 0 ? '.cp-app-disabled' : ''), { 'tabindex': '-1', }, [Icons.get('select')]), getOpenIn('presentation'))) : undefined, h('li', h('a.cp-app-drive-context-savelocal.dropdown-item', { 'tabindex': '-1', }, [ Icons.get('cloud-upload'), Messages.pad_mediatagImport ])), // Save in your CryptDrive $separator.clone()[0], h('li', h('a.cp-app-drive-context-expandall.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(expandAll), Messages.fc_expandAll ])), h('li', h('a.cp-app-drive-context-collapseall.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(collapseAll), Messages.fc_collapseAll ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-openparent.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(showParent), Messages.fm_openParent ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-share.dropdown-item', { 'tabindex': '-1', }, [ Icons.get('share'), Messages.shareButton ])), h('li', h('a.cp-app-drive-context-access.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(access), Messages.accessButton ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-newfolder.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', },[ Icons.get(folder), Messages.fc_newfolder ])), h('li', h('a.cp-app-drive-context-newsharedfolder.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', },[ Icons.get(sharedFolder), Messages.fc_newsharedfolder ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-uploadfiles.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', },[ Icons.get(uploadFile), Messages.uploadButton ])), h('li', h('a.cp-app-drive-context-uploadfolder.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(uploadFolder), Messages.uploadFolderButton ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', },[ Icons.get('link'), Messages.fm_link_new ])), h('li.dropdown-submenu', [ h('a.cp-app-drive-context-newdocmenu.dropdown-item', { 'tabindex': '-1', },[ Icons.get('add'), Messages.fm_newFile ]), h("ul.dropdown-menu", getNewPadTypes().map(function (app) { return isAppEnabled(app) ? h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable' + (restricted[app] === 0 ? '.cp-app-disabled' : ''), { 'tabindex': '-1', 'data-type': app, },[ Icons.get(AppConfig.applicationsIcon[app] || 'file'), Messages.type[app] ])) : undefined; })), ]), $separator.clone()[0], h('li', h('a.cp-app-drive-context-empty.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(trashEmpty), Messages.fc_empty ])), h('li', h('a.cp-app-drive-context-restore.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', 'data-icon': restore, }, [ Icons.get(restore), Messages.fc_restore ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-rename.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(rename), Messages.fc_rename ])), h('li', h('a.cp-app-drive-context-color.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(palette), Messages.fc_color ])), h('li', h('a.cp-app-drive-context-hashtag.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(tags), Messages.fc_hashtag ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-makeacopy.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(copy), Messages.makeACopy ])), h('li', h('a.cp-app-drive-context-download.dropdown-item', { 'tabindex': '-1', }, [ Icons.get(download), Messages.download_mt_button ])), h('li', h('a.cp-app-drive-context-delete.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(trashFull), Messages.fc_delete ])), // "Move to trash" h('li', h('a.cp-app-drive-context-deleteowned.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(destroy), Messages.fc_delete_owned ])), h('li', h('a.cp-app-drive-context-remove.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(trashFull), Messages.fc_remove ] )), h('li', h('a.cp-app-drive-context-removesf.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', }, [ Icons.get(trashFull), Messages.fc_remove_sharedfolder ])), $separator.clone()[0], h('li', h('a.cp-app-drive-context-properties.dropdown-item', { 'tabindex': '-1', },[ Icons.get(properties), Messages.fc_prop ])) ]) ]); // add events handlers for the contextmenu submenus $(menu).find(".dropdown-submenu").each(function (i, el) { var $el = $(el); var $a = $el.children().filter("a"); var $sub = $el.find(".dropdown-menu").first(); var left, bottomOffset; var timeoutId; var showSubmenu = function () { clearTimeout(timeoutId); left = $el.offset().left + $el.outerWidth() + $sub.outerWidth() > $(window).width(); bottomOffset = $el.offset().top + $el.outerHeight() + $sub.outerHeight() - $(window).height(); $sub.css("left", left ? "0%": "100%"); $sub.css("transform", "translate(" + (left ? "-100%" : "0") + "," + (bottomOffset > 0 ? -(bottomOffset - $el.outerHeight()) : 0) + "px)"); $el.siblings().find(".dropdown-menu").hide(); $sub.show(); }; var hideSubmenu = function () { $sub.hide(); $sub.removeClass("left"); }; var mouseOutSubmenu = function () { // don't hide immediately the submenu timeoutId = setTimeout(hideSubmenu, 100); }; // Add submenu expand icon $a.append(h("span", [Icons.get('expand-menu')])); // Show / hide submenu $el.hover(function () { showSubmenu(); }, function () { mouseOutSubmenu(); }); // handle click event $el.click(function (e) { var targetItem = $(e.target).closest(".dropdown-item")[0]; // don't close contextmenu if open submenu var elTarget = $el.children(".dropdown-item")[0]; if (targetItem === elTarget) { e.stopPropagation(); } if ($el.children().filter(".dropdown-menu:visible").length !== 0) { $el.find(".dropdown-menu").hide(); hideSubmenu(); } else { showSubmenu(); } }); }); return $(menu); }; var create = function (common, driveConfig) { //proxy, folders) { var metadataMgr = common.getMetadataMgr(); var sframeChan = common.getSframeChannel(); var priv = metadataMgr.getPrivateData(); // Initialization Util.extend(APP, driveConfig.APP); APP.$limit = driveConfig.$limit; var proxy = driveConfig.proxy; var folders = driveConfig.folders; var files = proxy.drive; var history = driveConfig.history || {}; var edPublic = driveConfig.edPublic || priv.edPublic; config.editKey = driveConfig.editKey; APP.origin = priv.origin; APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']); APP.closed = false; APP.toolbar = driveConfig.toolbar; var $readOnly = $(h('div.cp-banner.cp-banner-info.cp-app-drive-content-info-box', Messages.readonly)); var updateObject = driveConfig.updateObject; var updateSharedFolders = driveConfig.updateSharedFolders; // manager config.loggedIn = APP.loggedIn; config.sframeChan = sframeChan; var manager = ProxyManager.createInner(files, sframeChan, edPublic, config); var LS = makeLS(APP.team); Object.keys(folders).forEach(function (id) { var f = folders[id]; var sfData = files.sharedFolders[id] || {}; var href = manager.user.userObject.getHref(sfData); var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); }); // UI containers var $tree = APP.$tree = $("#cp-app-drive-tree"); var $content = APP.$content = $("#cp-app-drive-content"); var $contentContainer = $("#cp-app-drive-content-container"); var $appContainer = $(".cp-app-drive-container"); var $driveToolbar = APP.toolbar.$bottom; var $contextMenu = createContextMenu(common).appendTo($appContainer); var $contentContextMenu = $("#cp-app-drive-context-content"); var $defaultContextMenu = $("#cp-app-drive-context-default"); var $trashTreeContextMenu = $("#cp-app-drive-context-trashtree"); var $trashContextMenu = $("#cp-app-drive-context-trash"); $content.attr("tabindex", "0"); if (APP.loggedIn) { var splitter = h('div.cp-splitter', [ Icons.get('ellipsis-vertical') ]); $contentContainer.append(splitter); APP.$splitter = $(splitter).on('mousedown touchstart', function (e) { e.preventDefault(); var x = e.type === 'touchstart' ? e.originalEvent.touches[0].pageX : e.pageX; var w = $tree.width(); var handler = function (evt) { if (evt.type === 'mouseup' || evt.type === 'touchend') { $(window).off('mouseup mousemove touchend touchmove', handler); return; } var pageX = evt.type === 'touchmove' ? evt.originalEvent.touches[0].pageX : evt.pageX; $tree.css('width', (w - x + pageX) + 'px'); }; $(window).off('mouseup mousemove touchend touchmove', handler, { passive: false }); $(window).on('mouseup mousemove touchend touchmove', handler); }); } // TOOLBAR // DRIVE var currentPath = APP.currentPath = LS.getLastOpenedFolder(); if (APP.newSharedFolder) { var newSFPaths = manager.findFile(Number(APP.newSharedFolder)); if (newSFPaths.length) { currentPath = newSFPaths[0]; } } // Categories dislayed in the menu var displayedCategories = [ROOT, TRASH, SEARCH, RECENT]; // PCS enabled: display owned pads //if (AppConfig.displayCreationScreen) { displayedCategories.push(OWNED); } // Templates enabled: display template category if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); } // Tags used: display Tags category if (Object.keys(manager.getTagsList()).length) { displayedCategories.push(TAGS); } var virtualCategories = [SEARCH, RECENT, OWNED, TAGS]; if (!APP.loggedIn) { $tree.hide(); if (APP.newSharedFolder) { // ANON_SHARED_FOLDER displayedCategories = [SHARED_FOLDER]; virtualCategories.push(SHARED_FOLDER); currentPath = [SHARED_FOLDER, ROOT]; } else { displayedCategories = [FILES_DATA]; currentPath = [FILES_DATA]; } } else if (priv.isEmbed && APP.newSharedFolder) { displayedCategories = [ROOT, TRASH]; } APP.editable = !APP.readOnly; 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 findDataHolder = function ($el) { return $el.is('.cp-app-drive-element-row') ? $el : $el.closest('.cp-app-drive-element-row'); }; // Selection var sel = {}; var removeSelected = function (keepObj) { APP.selectedFiles = []; findSelectedElements().removeClass("cp-app-drive-element-selected"); if (!keepObj) { delete sel.startSelected; delete sel.endSelected; delete sel.oldSelection; } }; sel.refresh = 50; sel.$selectBox = $('
', {'class': 'cp-app-drive-content-select-box'}).appendTo($content); var checkSelected = function () { if (!sel.down) { return; } var pos = sel.pos; var l = $content[0].querySelectorAll('.cp-app-drive-element:not(.cp-app-drive-element-selected):not(.cp-app-drive-element-header)'); var p, el; var offset = getViewMode() === "grid" ? 10 : 0; for (var i = 0; i < l.length; i++) { el = l[i]; p = $(el).position(); p.top += offset + $content.scrollTop(); p.left += offset; p.bottom = p.top + $(el).outerHeight(); p.right = p.left + $(el).outerWidth(); if (p.right < pos.left || p.left > pos.right || p.top > pos.bottom || p.bottom < pos.top) { $(el).removeClass('cp-app-drive-element-selected-tmp'); } else { $(el).addClass('cp-app-drive-element-selected-tmp'); } } }; $content.on('mousedown', function (e) { if (e.which !== 1) { return; } $content.focus(); sel.down = true; if (!e.ctrlKey) { removeSelected(); } var rect = e.currentTarget.getBoundingClientRect(); sel.startX = e.clientX - rect.left; sel.startY = e.clientY - rect.top + $content.scrollTop(); sel.$selectBox.show().css({ left: sel.startX + 'px', top: sel.startY + 'px', width: '0px', height: '0px' }); APP.hideMenu(e); if (sel.move) { return; } sel.move = function (ev) { var rectMove = ev.currentTarget.getBoundingClientRect(), offX = ev.clientX - rectMove.left, offY = ev.clientY - rectMove.top + $content.scrollTop(); var left = sel.startX, top = sel.startY; var width = offX - sel.startX; if (width < 0) { left = Math.max(0, offX); var diffX = left-offX; width = Math.abs(width) - diffX; } var height = offY - sel.startY; if (height < 0) { top = Math.max(0, offY); var diffY = top-offY; height = Math.abs(height) - diffY; } sel.$selectBox.css({ width: width + 'px', left: left + 'px', height: height + 'px', top: top + 'px' }); sel.pos = { top: top, left: left, bottom: top + height, right: left + width }; var diffT = sel.update ? +new Date() - sel.update : sel.refresh; if (diffT < sel.refresh) { if (!sel.to) { sel.to = window.setTimeout(function () { sel.update = +new Date(); checkSelected(); sel.to = undefined; }, (sel.refresh - diffT)); } return; } sel.update = +new Date(); checkSelected(); }; $content.mousemove(sel.move); }); var onWindowMouseUp = function (e) { if (!sel.down) { return; } if (e.which !== 1) { return; } sel.down = false; sel.$selectBox.hide(); $content.off('mousemove', sel.move); delete sel.move; $content.find('.cp-app-drive-element-selected-tmp') .removeClass('cp-app-drive-element-selected-tmp') .each(function (idx, element) { selectElement($(element)); }); e.stopPropagation(); }; var getSelectedPaths = function ($element) { var paths = []; if (!$element || $element.length === 0) { return paths; } if (findSelectedElements().length > 1) { var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { paths.push({ path: ePath, element: $(elmt) }); } }); } if (!paths.length) { var path = $element.data('path'); if (!path) { return false; } paths.push({ path: path, element: $element }); } return paths; }; var removeInput = function (cancel) { if (!cancel && $('.cp-app-drive-element-row > input').length === 1) { var $input = $('.cp-app-drive-element-row > input'); manager.rename($input.data('path'), $input.val(), function (err) { if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') { UI.alert(err.message || Messages.fo_unavailableName); return; } APP.refresh(); }); } $('.cp-app-drive-element-row > input').remove(); $('.cp-app-drive-element-row > span:hidden').removeAttr('style'); }; var getFileNameExtension = function (name) { var matched = /\.[^\. ]+$/.exec(name); if (matched && matched.length) { return matched[matched.length -1]; } return ''; }; // Replace a file/folder name by an input to change its value var displayRenameInput = function ($element, path) { // NOTE: setTimeout(f, 0) otherwise the "rename" button in the toolbar is not working var renameInput = function () { if (!APP.editable) { return; } if (!path || path.length < 2) { logError("Renaming a top level element (root, trash or filesData) is forbidden."); return; } removeInput(); var $name = $element.find('.cp-app-drive-element-name'); if (!$name.length) { $name = $element.find('> .cp-app-drive-element'); } $name.hide(); var isFolder = $element.is(".cp-app-drive-element-folder:not(.cp-app-drive-element-sharedf)"); var el = manager.find(path); var name = manager.isFile(el) ? manager.getTitle(el) : path[path.length - 1]; if (manager.isSharedFolder(el)) { name = manager.getSharedFolderData(el).title; } var $input = $('', { placeholder: name, value: name }).data('path', path); // Stop propagation on keydown to avoid issues with arrow keys $input.on('keydown', function (e) { e.stopPropagation(); }); $input.on('keyup', function (e) { e.stopPropagation(); if (e.which === 13) { removeInput(true); var newName = $input.val(); if (JSON.stringify(path) === JSON.stringify(currentPath)) { manager.rename(path, $input.val(), function (err) { if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') { UI.alert(err.message || Messages.fo_unavailableName); return; } if (isFolder) { LS.renameFoldersOpened(path, newName); path[path.length - 1] = newName; } APP.displayDirectory(path); }); } else { manager.rename(path, $input.val(), function (err) { if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') { UI.alert(err.message || Messages.fo_unavailableName); return; } if (isFolder) { LS.renameFoldersOpened(path, newName); unselectElement($element); $element.data("path", $element.data("path").slice(0, -1).concat(newName)); selectElement($element); } APP.refresh(); }); } return; } if (e.which === 27) { removeInput(true); } }).on('keypress', function (e) { e.stopPropagation(); }); //$element.parent().append($input); $name.after($input); $input.focus(); var extension = getFileNameExtension(name); var input = $input[0]; input.selectionStart = 0; input.selectionEnd = name.length - extension.length; // We don't want to open the file/folder when clicking on the input $input.on('click dblclick', function (e) { 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('.cp-app-drive-element-row').attr("draggable", false); }); $input.on('mouseup', function (e) { e.stopPropagation(); $input.parents('.cp-app-drive-element-row').attr("draggable", true); }); }; if (APP.mobile()) { renameInput(); } else { window.setTimeout(renameInput, 0); } }; // Arrow keys to modify the selection var onWindowKeydown = function (e) { if (!$content.is(':visible')) { return; } if ($('.cp-modal').is(':visible')) { return; } var $searchBar = $tree.find('#cp-app-drive-tree-search-input'); if (document.activeElement && document.activeElement.nodeName === 'INPUT') { return; } if ($searchBar.is(':focus') && $searchBar.val()) { return; } var $elements = $content.find('.cp-app-drive-element:not(.cp-app-drive-element-header)'); var ev = {}; if (e.ctrlKey) { ev.ctrlKey = true; } if (e.shiftKey) { ev.shiftKey = true; } // ESC if (e.which === 27) { return void APP.hideMenu(); } // Enter if (e.which === 13) { var $allSelected = $content.find('.cp-app-drive-element.cp-app-drive-element-selected'); if ($allSelected.length === 1) { // Open the folder or the file $allSelected.dblclick(); return; } // If more than one, open only the files var $select = $content.find('.cp-app-drive-element-file.cp-app-drive-element-selected'); $select.each(function (idx, el) { $(el).dblclick(); }); return; } // Ctrl+A select all if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) { e.preventDefault(); $content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)') .each(function (idx, element) { selectElement($(element)); }); return; } // F2: rename selected element if (e.which === 113) { var paths = getSelectedPaths(findSelectedElements().first()); if (paths.length !== 1) { return; } displayRenameInput(paths[0].element, paths[0].path); } // [Left, Up, Right, Down] if ([37, 38, 39, 40].indexOf(e.which) === -1) { return; } e.preventDefault(); // If the arrow keys aren't caught by another listener before, it means we can // use them to select content in the drive. If that's the case, we'll also // focus the drive container to avoid conflicts with other focused elements $content.focus(); var click = function (el) { if (!el) { return; } APP.onElementClick(ev, $(el)); }; var $selection = findSelectedElements(); if ($selection.length === 0) { return void click($elements.first()[0]); } var lastIndex = typeof sel.endSelected === "number" ? sel.endSelected : typeof sel.startSelected === "number" ? sel.startSelected : $elements.index($selection.last()[0]); var length = $elements.length; if (length === 0) { return; } // List mode if (getViewMode() === "list") { if (e.which === 40) { click($elements.get(Math.min(lastIndex+1, length -1))); } if (e.which === 38) { click($elements.get(Math.max(lastIndex-1, 0))); } return; } // Icon mode // Get the vertical and horizontal position of lastIndex // Filter all the elements to get those in the same line/column var pos = $($elements.get(0)).position(); var $line = $elements.filter(function (idx, el) { return $(el).position().top === pos.top; }); var cols = $line.length; var lines = Math.ceil(length/cols); var lastPos = { l : Math.floor(lastIndex/cols), c : lastIndex - Math.floor(lastIndex/cols)*cols }; if (e.which === 37) { if (lastPos.c === 0) { return; } click($elements.get(Math.max(lastIndex-1, 0))); return; } if (e.which === 38) { if (lastPos.l === 0) { return; } click($elements.get(Math.max(lastIndex-cols, 0))); return; } if (e.which === 39) { if (lastPos.c === cols-1) { return; } click($elements.get(Math.min(lastIndex+1, length-1))); return; } if (e.which === 40) { if (lastPos.l === lines-1) { return; } click($elements.get(Math.min(lastIndex+cols, length-1))); return; } }; 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) { if (!sDate) { return ''; } 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) { console.error("Unable to format that string to a date with .toLocaleString", sDate, e); } return ret; }; var previewMediaTag = function (data) { var mts = []; $content.find('.cp-app-drive-element.cp-border-color-file').each(function (i, el) { var path = $(el).data('path'); var id = manager.find(path); if (!id) { return; } var _data = manager.getFileData(id); if (!_data || _data.channel < 48) { return; } mts.push({ channel: _data.channel, href: _data.href, password: _data.password }); }); // Find initial position var idx = -1; mts.some(function (obj, i) { if (obj.channel === data.channel) { idx = i; return true; } }); if (idx === -1) { mts.unshift({ href: data.href, password: data.password }); idx = 0; } common.getMediaTagPreview(mts, idx); }; var FILTER_BY = "filterBy"; var refreshDeprecated = function () { if (!APP.passwordModal) { return; } var deprecated = files.sharedFoldersTemp; if (JSONSortify(deprecated) === APP.deprecatedSF) { return; } APP.deprecatedSF = JSONSortify(deprecated); if (typeof (deprecated) === "object" && Object.keys(deprecated).length) { var nt = nThen; Object.keys(deprecated).forEach(function (fId) { var data = deprecated[fId]; var sfId = manager.user.userObject.getSFIdFromHref(data.href); if (folders[fId] || sfId) { // This shared folder is already stored in the drive... return void manager.delete([['sharedFoldersTemp', fId]], function () { }); } nt = nt(function (waitFor) { UI.openCustomModal(APP.passwordModal(fId, data, waitFor())); }).nThen; }); nt(function () { APP.refresh(); }); } }; var refresh = APP.refresh = function (cb, opt) { var type = APP.store[FILTER_BY]; var path = type ? [FILTER, type, currentPath] : currentPath; APP.displayDirectory(path, undefined, () => { refreshDeprecated(); if (typeof(cb) === "function") { cb(); } }, opt); }; // `app`: true (force open with the app), false (force open in preview), // falsy (open in preview if default is not using the app) var defaultInApp = ['application/pdf']; var openFile = function (el, isRo, app) { // In anonymous drives, `el` already contains file data var data = el.channel ? el : manager.getFileData(el); if (data.static) { if (data.href) { common.openUnsafeURL(data.href); manager.updateStaticAccess(el, refresh); } return; } if (!data || (!data.href && !data.roHref)) { return void logError("Missing data for the file", el, data); } var href = isRo ? data.roHref : (data.href || data.roHref); var parsed = Hash.parsePadUrl(href); if (parsed.hashData && parsed.hashData.type === 'file' && !app && (defaultInApp.indexOf(data.fileType) === -1 || app === false)) { return void previewMediaTag(data); } var obj = { t: APP.team }; var priv = metadataMgr.getPrivateData(); var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']); if (useUnsafe === true || APP.newSharedFolder) { return void common.openURL(Hash.getNewPadURL(href, obj)); } // Get hidden hash var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); var opts = {}; if (isRo) { opts.view = true; } var hash = Hash.getHiddenHashFromKeys(parsed.type, secret, opts); var hiddenHref = Hash.hashToHref(hash, parsed.type); common.openURL(Hash.getNewPadURL(hiddenHref, obj)); }; var openIn = function (type, path, team, fData) { var obj = { p: path, t: team, d: fData }; var href = Hash.hashToHref('', type); common.openURL(Hash.getNewPadURL(href, obj)); }; var pickFolderColor = function ($element, currentColor, cb) { var colors = ["", "#f23c38", "#ff0073", "#da0eba", "#9d00ac", "#6c19b3", "#4a42b1", "#3d8af0", "#30a0f1", "#1fb9d1", "#009686", "#45b354", "#84c750", "#c6e144", "#faf147", "#fbc423", "#fc9819", "#fd5227", "#775549", "#9c9c9c", "#607a89"]; var colorsElements = []; var currentElement = null; const icon_classes = 'cp-app-drive-icon-folder cp-app-drive-content-icon'; colors.forEach(function (color, i) { var element = h("span.cp-app-drive-color-picker-color", [ (i === 0 ? Icons.get('folder-nocolor', {'class': icon_classes + ' folder-no-color'}): Icons.get('folder', {'class': icon_classes})), ]); $(element).css("color", colors[i]); if (colors[i] === currentColor) { currentElement = element; $(element).find('.cp-app-drive-icon-folder').replaceWith( Icons.get('folder-check', {'class': icon_classes}) ); if (i === 0) { $(element).addClass("folder-no-color"); } $(element).addClass("cp-app-drive-current-color"); } $(element).on("click", function () { if (currentElement) { $(currentElement).removeClass("cp-app-drive-current-color"); var currentIndex = colorsElements.indexOf(currentElement); var originalIcon = currentIndex === 0 ? Icons.get('folder-nocolor', {'class': icon_classes + ' folder-no-color'}) : Icons.get('folder', {'class': icon_classes}); $(currentElement).find('.cp-app-drive-icon-folder').replaceWith(originalIcon); } currentElement = element; $(element).addClass("cp-app-drive-current-color"); $(element).find('.cp-app-drive-icon-folder').replaceWith( Icons.get('folder-check', {'class': icon_classes}) ); cb(color); }); colorsElements.push(element); }); var content = h("div.cp-app-drive-color-picker", colorsElements); UI.alert(content); }; var getFolderColor = function (path) { if (path.length === 0) { return; } return manager.getFolderData(path).color || ""; }; var setFolderColor = function ($element, path, color) { if ($element.length === 0) { return; } $element.find(".cp-app-drive-icon-folder").css("color", color); manager.setFolderData({ path: path, key: "color", value: color }, function () {}); }; var filterContextMenu = function (type, paths) { if (!paths || paths.length === 0) { logError('no paths'); } $contextMenu.find('li').hide(); var show = []; var filter; var editable = true; if (type === "content") { if (APP.$content.data('readOnlyFolder')) { editable = false; } // Return true in filter to hide filter = function ($el, className) { if (className === 'newfolder') { return; } if (className === 'newsharedfolder') { // Hide the new shared folder menu if we're already in a shared folder return manager.isInSharedFolder(currentPath) || APP.disableSF; } if (className === 'uploadfiles') { return; } if (className === 'uploadfolder') { return !APP.allowFolderUpload; } if (className === 'newdoc') { return; } }; } else { // In case of multiple selection, we must hide the option if at least one element // is not compatible var containsFolder = false; var hide = []; if (!APP.team) { hide.push('savelocal'); } paths.forEach(function (p) { var path = p.path; var $element = p.element; if (APP.$content.data('readOnlyFolder') && manager.isSubpath(path, currentPath)) { editable = false; } if (!$element.closest("#cp-app-drive-tree").length) { hide.push('expandall'); hide.push('collapseall'); } if (path.length === 1) { // Can't rename, share, delete, or change the color of categories hide.push('delete'); hide.push('rename'); hide.push('share'); hide.push('savelocal'); hide.push('color'); } if ($element.is('.cp-app-drive-element-folder')) { hide.push('open'); } if (!$element.is('.cp-app-drive-element-owned')) { hide.push('deleteowned'); } if ($element.is('.cp-app-drive-element-restricted')) { hide.push('rename', 'download', 'share', 'access', 'color'); } if ($element.is('.cp-app-drive-element-notrash')) { // We can't delete elements in virtual categories hide.push('delete'); } if (!$element.is('.cp-border-color-file')) { //hide.push('download'); hide.push('openincode'); hide.push('preview'); } if ($element.is('.cp-app-drive-static')) { hide.push('access', 'hashtag', 'properties', 'download'); } if ($element.is('.cp-app-drive-element-file')) { // No folder in files hide.push('openfolder'); hide.push('color'); hide.push('newfolder'); if ($element.is('.cp-app-drive-element-readonly')) { hide.push('open'); // Remove open 'edit' mode } else if ($element.is('.cp-app-drive-element-noreadonly')) { hide.push('openro'); // Remove open 'view' mode } var metadata = manager.getFileData(manager.find(path)); if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { hide.push('openincode'); } if (metadata && /\/(doc|presentation|sheet)\//.test(metadata.href)) { hide.push('openinsheet'); hide.push('openindoc'); hide.push('openinpresentation'); } if (!metadata || !Util.isSpreadsheet(metadata.fileType, metadata.title) || !priv.supportsWasm) { hide.push('openinsheet'); } if (!metadata || !Util.isOfficeDoc(metadata.fileType, metadata.title) || !priv.supportsWasm) { hide.push('openindoc'); } if (!metadata || !Util.isPresentation(metadata.fileType, metadata.title) || !priv.supportsWasm) { hide.push('openinpresentation'); } if (metadata.channel && metadata.channel.length < 48) { hide.push('preview'); } if (!metadata.channel || metadata.channel.length > 32) { hide.push('makeacopy'); // Not for blobs } } else if ($element.is('.cp-app-drive-element-sharedf')) { if (containsFolder) { // More than 1 folder selected: cannot create a new subfolder hide.push('newfolder'); hide.push('expandall'); hide.push('collapseall'); } containsFolder = true; hide.push('openro'); hide.push('openincode'); hide.push('openinsheet'); hide.push('openindoc'); hide.push('openinpresentation'); hide.push('hashtag'); //hide.push('delete'); hide.push('makeacopy'); //hide.push('deleteowned'); } else { // it's a folder or a file in a guest drive if (containsFolder) { // More than 1 folder selected: cannot create a new subfolder hide.push('newfolder'); hide.push('expandall'); hide.push('collapseall'); } containsFolder = true; hide.push('savelocal'); if ($element.is('.cp-app-drive-element-folder')) { hide.push('openro'); } hide.push('openincode'); hide.push('openinsheet'); hide.push('openindoc'); hide.push('openinpresentation'); hide.push('properties', 'access'); hide.push('hashtag'); hide.push('makeacopy'); } // If we're in the trash, hide restore and properties for non-root elements if (type === "trash" && path && path.length > 4) { hide.push('restore'); hide.push('properties'); } // If we're not in the trash nor in a shared folder, hide "remove" if (!manager.isInSharedFolder(path) && !$element.is('.cp-app-drive-element-sharedf')) { // This isn't a shared folder: can't delete shared folder hide.push('removesf'); } else if (type === "tree") { // This is a shared folder or an element inside a shsared folder // ==> can't move to trash hide.push('delete'); } if ($element.closest('[data-ro]').length) { editable = false; } }); if (paths.length > 1) { hide.push('properties', 'access'); hide.push('rename'); hide.push('openparent'); hide.push('download'); hide.push('share'); hide.push('savelocal'); //hide.push('openincode'); // can't because of race condition //hide.push('openinsheet'); // can't because of race condition hide.push('makeacopy'); hide.push('preview'); } if (containsFolder && paths.length > 1) { // Cannot open multiple folders hide.push('open'); } if (!APP.loggedIn) { hide.push('openparent'); hide.push('rename'); hide.push('openinsheet'); hide.push('openindoc'); hide.push('openinpresentation'); } filter = function ($el, className) { if (hide.indexOf(className) !== -1) { return true; } }; } switch(type) { case 'content': show = ['newfolder', 'newsharedfolder', 'uploadfiles', 'uploadfolder', 'newdoc']; break; case 'tree': show = ['open', 'openfolder', 'openro', 'preview', 'openincode', 'expandall', 'collapseall', 'color', 'download', 'share', 'savelocal', 'rename', 'delete', 'makeacopy', 'openinsheet', 'openindoc', 'openinpresentation', 'deleteowned', 'removesf', 'access', 'properties', 'hashtag']; break; case 'default': show = ['open', 'openro', 'preview', 'openincode', 'openinsheet', 'openindoc', 'openinpresentation', 'share', 'download', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag', 'makeacopy', 'savelocal', 'rename']; break; case 'trashtree': { show = ['empty']; break; } case 'trash': { show = ['remove', 'deleteowned', 'restore', 'properties']; } } var filtered = []; show.forEach(function (className) { var $el = $contextMenu.find('.cp-app-drive-context-' + className); if ((!APP.editable || !editable) && $el.is('.cp-app-drive-context-editable')) { return; } if ((!APP.editable || !editable) && $el.is('.cp-app-drive-context-editable')) { return; } if (filter($el, className)) { return; } $el.parent('li').show(); filtered.push('.cp-app-drive-context-' + className); }); return filtered; }; var scrollTo = function ($element) { // Current scroll position var st = $content.scrollTop(); // Block height var h = $content.height(); // Current top position of the element relative to the scroll position var pos = Math.round($element.offset().top - $content.position().top); // Element height var eh = $element.outerHeight(); // New scroll value var v = st + pos + eh - h; // If the element is completely visile, don't change the scroll position if (pos+eh <= h && pos >= 0) { return; } $content.scrollTop(v); }; // Add the "selected" class to the "li" corresponding to the clicked element var onElementClick = APP.onElementClick = function (e, $element) { // If "Ctrl" is pressed, do not remove the current selection removeInput(); $element = findDataHolder($element); // If we're selecting a new element with the left click, hide the menu if (e) { APP.hideMenu(); } // Remove the selection if we don't hold ctrl key or if we are right-clicking if (!e || !e.ctrlKey) { removeSelected(e && e.shiftKey); } if (!$element.length) { log(Messages.fm_selectError); return; } scrollTo($element); // Add the selected class to the clicked / right-clicked element // Remove the class if it already has it // If ctrlKey, add to the selection // If shiftKey, select a range of elements var $elements = $content.find('.cp-app-drive-element:not(.cp-app-drive-element-header)'); var $selection = $elements.filter('.cp-app-drive-element-selected'); if (typeof sel.startSelected !== "number" || !e || (e.ctrlKey && !e.shiftKey)) { sel.startSelected = $elements.index($element[0]); sel.oldSelection = []; $selection.each(function (idx, el) { sel.oldSelection.push(el); }); delete sel.endSelected; } if (e && e.shiftKey) { var end = $elements.index($element[0]); sel.endSelected = end; var $el; removeSelected(true); sel.oldSelection.forEach(function (el) { if (!isElementSelected($(el))) { selectElement($(el)); } }); for (var i = Math.min(sel.startSelected, sel.endSelected); i <= Math.max(sel.startSelected, sel.endSelected); i++) { $el = $($elements.get(i)); if (!isElementSelected($el)) { selectElement($el); } } } else { if (!isElementSelected($element)) { selectElement($element); } else { unselectElement($element); } } }; // show / hide dropdown separators var hideSeparators = function ($menu) { var showSep = false; var $lastVisibleSep = null; $menu.children().each(function (i, el) { var $el = $(el); if ($el.is(".dropdown-divider")) { $el.css("display", showSep ? "list-item" : "none"); if (showSep) { $lastVisibleSep = $el; } showSep = false; } else if ($el.is("li") && $el.css("display") !== "none") { showSep = true; } }); if (!showSep && $lastVisibleSep) { $lastVisibleSep.css("display", "none"); } // remove last divider if no options after }; // prepare and display contextmenu var displayMenu = function (e) { var $menu = $contextMenu; // show / hide submenus $menu.find(".dropdown-submenu").each(function (i, el) { var $el = $(el); $el.children(".dropdown-menu").css("display", "none"); $el.find("li").each(function (i, li) { if ($(li).css("display") !== "none") { $el.css("display", "block"); return; } }); }); // show / hide separators $menu.find(".dropdown-menu").each(function (i, menu) { hideSeparators($(menu)); }); // show contextmenu at cursor position $menu.css({ display: "block" }); if (APP.mobile()) { let menuPositionTop; let menuPositionLeft; if (($(e.target).offset().top + $(e.target).height()) > ($(window).height()-$menu.height())) { if ( $(e.target).offset().top < $menu.height()) { menuPositionTop = 0; } else { menuPositionTop = ($(e.target).offset().top - $menu.height()); } } else { menuPositionTop = $(e.target).offset().top + $(e.target).outerHeight(); } if (($(e.target).offset().left + $(e.target).width()) < $menu.width()) { menuPositionLeft = $(e.target).offset().left; } else { menuPositionLeft = $(e.target).offset().left - ($menu.width() - $(e.target).width()) + 10; } $menu.css({ top: menuPositionTop + 'px', left: menuPositionLeft + 'px', }); return; } var h = $menu.outerHeight(); var w = $menu.outerWidth(); var wH = window.innerHeight; var wW = window.innerWidth; if (h > wH) { $menu.css({ top: '0px', bottom: '' }); } else if (e.pageY + h <= wH) { $menu.css({ top: e.pageY+'px', bottom: '' }); } else { $menu.css({ bottom: '0px', top: '' }); } if(w > wW) { $menu.css({ left: '0px', right: '' }); } else if (e.pageX + w <= wW) { $menu.css({ left: e.pageX+'px', right: '' }); } else { $menu.css({ left: '', right: '0px', }); } }; // Open the selected context menu on the closest "li" element var openContextMenu = function (type) { return function (e) { APP.hideMenu(); e.stopPropagation(); var paths; if (type === 'content') { paths = [{path: $(e.target).closest('#' + FOLDER_CONTENT_ID).data('path')}]; if (!paths) { return; } removeSelected(); } else { var $element = findDataHolder($(e.target)); // if clicked from tree var fromTree = $element.closest("#cp-app-drive-tree").length; if (fromTree) { removeSelected(); } // if clicked on non selected element if (!isElementSelected($element)) { removeSelected(); } if (type === 'trash' && !$element.data('path')) { return; } if (!$element.length) { logError("Unable to locate the .element tag", e.target); log(Messages.fm_contextMenuError); return false; } if (!isElementSelected($element)) { selectElement($element); } paths = getSelectedPaths($element); $('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_ro); if (paths.length === 1) { var metadata = manager.getFileData(manager.find(paths[0].path)); if (metadata.roHref || manager.find(paths[0].path).roHref) { var parsed = metadata.roHref ? Hash.parsePadUrl(metadata.roHref) : Hash.parsePadUrl(manager.find(paths[0].path).roHref); if (parsed.type === "form") { if (parsed.hashData.auditorKey) { $('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_formaud); } else { $('.cp-app-drive-context-openro .cp-text').text(Messages.fc_open_formro); } } } } } $contextMenu.attr('data-menu-type', type); filterContextMenu(type, paths); displayMenu(e); $(".cp-app-drive-context-noAction").toggle($contextMenu.find('li:visible').length === 0); $contextMenu.data('paths', paths); return false; }; }; var getElementName = function (path) { var file = manager.find(path); if (!file) { return; } if (manager.isSharedFolder(file)) { return manager.getSharedFolderData(file).title; } return manager.getTitle(file); }; // moveElements is able to move several paths to a new location var moveElements = function (paths, newPath, copy, cb) { if (!APP.editable) { return; } // Cancel drag&drop from TRASH to TRASH if (manager.isPathIn(newPath, [TRASH]) && paths.length && paths[0][0] === TRASH) { return; } var newCb = function (err) { if (err && err.error === 'E_DUPLICATE_FOLDER_NAME') { UI.alert(err.message || Messages.fo_unavailableName); paths.forEach(function (path) { LS.moveFoldersOpened(path, newPath); }); cb(); return; } paths.forEach(function (path) { LS.moveFoldersOpened(path, newPath); }); cb(); }; if (paths.some(function (p) { return manager.comparePath(newPath, p); })) { return void cb(); } manager.move(paths, newPath, newCb, copy); }; // Delete paths from the drive and/or shared folders (without moving them to the trash) var deletePaths = function (paths, pathsList) { pathsList = pathsList || []; if (paths) { paths.forEach(function (p) { pathsList.push(p.path); }); } var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [pathsList.length]); if (pathsList.length === 1) { msg = Messages.fm_removePermanentlyDialog; } UI.confirm(msg, function(res) { $(window).focus(); if (!res) { return; } manager.delete(pathsList, function () { pathsList.forEach(LS.removeFoldersOpened); removeSelected(); refresh(); }); }); }; // 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 = findDataHolder($(ev.target)); var getRefPath = function(navPath) { if (navPath && navPath.length > 1 && navPath[navPath.length - 1] === ROOT) { return navPath.slice(0, -1); } return navPath; }; if ($element.hasClass('cp-app-drive-element-selected')) { var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var navPath = $(elmt).data('path'); var ePath = getRefPath(navPath); if (ePath) { var val = manager.find(ePath); if (!val) { return; } // Error? A ".selected" element is not in the object paths.push({ path: ePath, value: { name: getElementName(ePath), el: val } }); } }); } else { removeSelected(); selectElement($element); var dragPath = getRefPath(path); var val = manager.find(dragPath); if (!val) { return; } // The element is not in the object paths = [{ path: dragPath, value: { name: getElementName(dragPath), el: val } }]; } var data = { 'path': paths }; ev.dataTransfer.setData("text", stringify(data)); }; var findDropPath = function (target) { var $target = $(target); var $el; if ($target.is(".cp-app-drive-path-element")) { $el = $target; } else { $el = findDataHolder($target); } var newPath = $el.data('path'); var dropEl = newPath && manager.find(newPath); if (newPath && manager.isSharedFolder(dropEl)) { newPath.push(manager.user.userObject.ROOT); } else if ((!newPath || manager.isFile(dropEl)) && $target.parents('#cp-app-drive-content')) { newPath = currentPath; } return newPath; }; var onFileDrop = APP.onFileDrop = function (file, e) { var ev = { target: e.target, path: findDropPath(e.target) }; // Make sure we can only upload files in the Documents tree var p = ev.path; if (!Array.isArray(p) || !p.length || p[0] !== ROOT) { return void UI.warn(Messages.fm_cantUploadHere); } APP.FM.onFileDrop(file, ev); }; var traverseFileTree = function (item, path, w, files) { path = path || ""; if (item.isFile) { // Get file item.file(w(function(file) { file.fix_path = path + file.name; files.push(file); })); } else if (item.isDirectory) { // Get folder contents var dirReader = item.createReader(); // this API is not supported in Opera or IE // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry // all other browsers will recurse over subfolders and upload everything dirReader.readEntries(w(function(entries) { for (var i=0; i= uploadSteps.length) { return; } nThen(function (waitFor) { // add folders uploadSteps[i].folders.forEach(function (folder) { var folderPath = folder.split("/"); var parentFolder = currentFolder.concat(folderPath.slice(0, -1)); var folderName = folderPath.slice(-1); manager.addFolder(parentFolder, folderName, waitFor(refresh)); }); // upload files uploadSteps[i].files.forEach(function (file) { var ev = { target: $content[0], path: currentFolder.concat(file.path.slice(0, -1)), }; APP.FM.handleFile(file.file, ev, folderUploadOptions); }); }).nThen(function () { stepByStep(uploadSteps, i + 1); }); }; stepByStep(uploadSteps, 0); }); }; var onDrop = function (ev) { ev.preventDefault(); $('.cp-app-drive-element-droppable').removeClass('cp-app-drive-element-droppable'); var data = ev.dataTransfer.getData("text"); var newPath = findDropPath(ev.target); if (!newPath) { return; } var sfId = manager.isInSharedFolder(newPath); if (sfId && folders[sfId] && folders[sfId].readOnly) { return void UI.warn(Messages.fm_forbidden); } if (newPath[0] === TEMPLATE) { var oldPaths = data && JSON.parse(data).path; if (oldPaths && oldPaths.length) { var hasFolder = oldPaths.some(function(p) { var el = manager.find(p.path); return manager.isFolder(el); }); if (hasFolder) { return void UI.warn(Messages.fo_moveUnsortedError); } } } var fileDrop = ev.dataTransfer.items; if (fileDrop.length) { // Filter out all the folders and use the correct function to upload them fileDrop = Array.prototype.slice.call(fileDrop).map(function (file) { if (file.kind !== "file") { return; } var f = file.getAsFile(); if (file.webkitGetAsEntry) { let entry = file.webkitGetAsEntry(); if (entry.isFile) { return f; } // It's a folder var files = []; nThen(function (w) { traverseFileTree(entry, "", w, files); }).nThen(function () { uploadFolder(files, f.name); }); return; } else { // Old browsers: unreliable hack to detect folders if (!file.type && file.size%4096 === 0) { return; // Folder upload not supported } } return f; }).filter(Boolean); // Continue only with the files // if there are no files, fall through to other handlers if (fileDrop.length) { return void onFileDrop(fileDrop, ev); } } oldPaths = data && JSON.parse(data).path; if (!oldPaths) { return; } // A moved element should be removed from its previous location var movedPaths = []; var sharedF = false; var draggedSharedFolder = false; var folderContainsSharedFolder = false; oldPaths.forEach(function (p) { movedPaths.push(p.path); if (!sharedF && manager.isInSharedFolder(p.path)) { sharedF = true; } if (!draggedSharedFolder && p.value && p.value.el && manager.isSharedFolder(p.value.el)) { draggedSharedFolder = true; } if (!folderContainsSharedFolder && p.value && p.value.el && manager.hasSubSharedFolder(p.value.el)) { folderContainsSharedFolder = true; } }); if (draggedSharedFolder && manager.isInSharedFolder(newPath)) { return void UI.warn(Messages.fo_sharedFolder_move_error); } if (manager.isPathIn(newPath, [SHARED_FOLDER])) { return void UI.warn(Messages.fo_sharedFolder_move_error); } if (sharedF && manager.isPathIn(newPath, [TRASH])) { // TODO create a key here? // You can't move to YOUR trash documents stored in a shared folder // TODO or keep deletePaths: trigger the "Remove from cryptdrive" modal return void UI.warn(Messages.fo_trash_move_error); //return void deletePaths(null, movedPaths); } if (folderContainsSharedFolder) { var destEl = manager.find(newPath); if (manager.isInSharedFolder(newPath) || (destEl && manager.hasSubSharedFolder(destEl))) { return void UI.warn(Messages.fo_sharedFolder_move_error); } } var copy = false; if (ev.ctrlKey || (ev.metaKey && APP.isMac)) { copy = true; } if (movedPaths && movedPaths.length) { moveElements(movedPaths, newPath, copy, 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) { e.preventDefault(); e.stopPropagation(); onDrop(e.originalEvent); }); $element.on('dragenter', function (e) { e.preventDefault(); e.stopPropagation(); counter++; $element.addClass('cp-app-drive-element-droppable'); }); $element.on('dragleave', function (e) { e.preventDefault(); e.stopPropagation(); counter--; if (counter <= 0) { counter = 0; $element.removeClass('cp-app-drive-element-droppable'); } }); }; addDragAndDropHandlers($content, null, true, true); $tree.on('drop dragover', function (e) { e.preventDefault(); e.stopPropagation(); }); $driveToolbar.on('drop dragover', function (e) { e.preventDefault(); e.stopPropagation(); }); // In list mode, display metadata from the filesData object var addStaticData = function (element, $element, data) { $element.addClass('cp-border-color-drive'); var name = data.name; var $name = $('', {'class': 'cp-app-drive-element-name'}).text(name); $element.append($name); if (getViewMode() === 'grid') { //console.error(name, Util.fixHTML(name)); // this is only safe because our build of tippy sets titles as // 'textContent' instead of innerHTML, otherwise // we would need to use Util.fixHTML $element.attr('title', name); } var type = Messages.fm_link_type; var $type = $('', { 'class': 'cp-app-drive-element-type cp-app-drive-element-list' }).text(type); var $adate = $('', { 'class': 'cp-app-drive-element-atime cp-app-drive-element-list' }).text(getDate(data.atime)); var $cdate = $('', { 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' }).text(getDate(data.ctime)); $element.append($type).append($adate).append($cdate); }; var _addOwnership = function ($span, $state, data) { if (data && Array.isArray(data.owners) && data.owners.indexOf(edPublic) !== -1) { var $owned = $ownedIcon.clone().appendTo($state); $owned.attr('title', Messages.fm_padIsOwned); $span.addClass('cp-app-drive-element-owned'); } /* else if (data.owners && data.owners.length) { var $owner = $ownerIcon.clone().appendTo($state); $owner.attr('title', Messages.fm_padIsOwnedOther); } */ }; var thumbsUrls = {}; // This is duplicated in cryptpad-common, it should be unified var getFileIcon = function (id) { var data = manager.getFileData(id); return UI.getFileIcon(data); }; var getIcon = UI.getIcon; var addTitleIcon = function (element, $name) { var icon = getFileIcon(element); $(icon).addClass('cp-app-drive-element-icon'); $name.addClass('cp-app-drive-element-name-icon'); $name.prepend($(icon)); // Clean up setTimeout(function() { $name.closest('li').children('svg').remove(); }, 0); }; var addFileData = function (element, $element) { if (!manager.isFile(element)) { return; } var data = manager.getFileData(element); if (data.static) { return addStaticData(element, $element, data); } if (!Object.keys(data).length) { return true; } var href = data.href || data.roHref; if (!data) { return void logError("No data for the file", element); } var hrefData = Hash.parsePadUrl(href); if (hrefData.type) { $element.addClass('cp-border-color-'+hrefData.type); } var $state = $('', {'class': 'cp-app-drive-element-state'}); if (hrefData.hashData && hrefData.hashData.mode === 'view') { var $ro = $readonlyIcon.clone().appendTo($state); $ro.attr('title', Messages.readonly); } if (data.filename && data.filename !== data.title) { var $renamed = $renamedIcon.clone().appendTo($state); $renamed.attr('data-cptippy-html', 'true'); $renamed.attr('title', Messages._getKey('fm_renamedPad', [Util.fixHTML(data.title)])); } if (hrefData.hashData && hrefData.hashData.password) { var $password = $passwordIcon.clone().appendTo($state); $password.attr('title', Messages.fm_passwordProtected || ''); } if (data.expire) { var $expire = $expirableIcon.clone().appendTo($state); $expire.attr('title', Messages._getKey('fm_expirablePad', [new Date(data.expire).toLocaleString()])); } _addOwnership($element, $state, data); var $menu = $('', {'class': 'cp-app-drive-element-menu'}); $menu.click(function (e) { e.preventDefault(); e.stopPropagation(); $element.contextmenu(); }); $fileMenuIcon.clone().appendTo($menu); var name = manager.getTitle(element); // The element with the class '.name' is underlined when the 'li' is hovered var $name = $(h('span.cp-app-drive-element-name', [ h('span.cp-app-drive-element-name-text', name) ])); $element.append($name); $element.append($state); if (APP.mobile()) { $element.append($menu); } if (getViewMode() === 'grid') { $element.attr('title', name); } // display the thumbnail // if the thumbnail has already been displayed once, do not reload it, keep the same url if (thumbsUrls[element]) { var img = new Image(); img.src = thumbsUrls[element]; $element.prepend(img); $(img).addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); $(img).attr("draggable", false); $(img).attr("role", "presentation"); addTitleIcon(element, $name); } else { common.displayThumbnail(href || data.roHref, data.channel, data.password, $element, function ($thumb) { // Called only if the thumbnail exists // Remove the .hide() added by displayThumnail() because it hides the icon in list mode too $element.find('.cp-icon').removeAttr('style'); $thumb.addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); $thumb.attr("draggable", false); thumbsUrls[element] = $thumb[0].src; addTitleIcon(element, $name); }); } var type = Messages.type[hrefData.type] || hrefData.type; var $type = $('', { 'class': 'cp-app-drive-element-type cp-app-drive-element-list' }).text(type); var $adate = $('', { 'class': 'cp-app-drive-element-atime cp-app-drive-element-list' }).text(getDate(data.atime)); var $cdate = $('', { 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' }).text(getDate(data.ctime)); $element.append($type).append($adate).append($cdate); }; var addFolderData = function (element, key, $span) { if (!element || !manager.isFolder(element)) { return; } // The element with the class '.name' is underlined when the 'li' is hovered var $state = $('', {'class': 'cp-app-drive-element-state'}); var $ro; var $menu = $('', {'class': 'cp-app-drive-element-menu'}); $menu.click(function (e) { e.preventDefault(); e.stopPropagation(); $span.contextmenu(); }); $fileMenuIcon.clone().appendTo($menu); if (manager.isSharedFolder(element)) { var data = manager.getSharedFolderData(element); var fId = element; key = data.title || data.lastTitle || Messages.fm_deletedFolder; element = Util.find(manager, ['folders', element, 'proxy', manager.user.userObject.ROOT]) || {}; $span.addClass('cp-app-drive-element-sharedf'); _addOwnership($span, $state, data); var hrefData = Hash.parsePadUrl(data.href || data.roHref); if (hrefData.hashData && hrefData.hashData.password) { var $password = $passwordIcon.clone().appendTo($state); $password.attr('title', Messages.fm_passwordProtected || ''); } if (hrefData.hashData && hrefData.hashData.mode === 'view') { $ro = $readonlyIcon.clone().appendTo($state); $ro.attr('title', Messages.readonly); } if (files.restrictedFolders[fId]) { var $restricted = $restrictedIcon.clone().appendTo($state); $restricted.attr('title', Messages.fm_restricted); } var $shared = $sharedIcon.clone().appendTo($state); $shared.attr('title', Messages.fm_canBeShared); } else if ($content.data('readOnlyFolder') || APP.readOnly) { $ro = $readonlyIcon.clone().appendTo($state); $ro.attr('title', Messages.readonly); } var sf = manager.hasSubfolder(element); var hasFiles = manager.hasFile(element); var $name = $(h('span.cp-app-drive-element-name', [ h('span.cp-app-drive-element-name-text', key) ])); var $subfolders = $('', { 'class': 'cp-app-drive-element-folders cp-app-drive-element-list' }).text(sf); var $files = $('', { 'class': 'cp-app-drive-element-files cp-app-drive-element-list' }).text(hasFiles); var $filler = $('', { 'class': 'cp-app-drive-element-filler cp-app-drive-element-list' }); if (getViewMode() === 'grid') { $span.attr('title', key); } $span.append($name).append($state).append($subfolders).append($files).append($filler); if (APP.mobile()) { $span.append($menu); } }; var createShareButton = function (id, $container) { var $shareBlock = $('