You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/drive/main.js

3028 lines
124 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

define([
'jquery',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/textpatcher/TextPatcher.amd.js',
'json.sortify',
'/common/cryptpad-common.js',
'/common/userObject.js',
'/common/toolbar2.js',
'/customize/application_config.js',
'/common/cryptget.js',
'/common/mergeDrive.js',
'less!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/customize/src/less/cryptpad.less',
], function ($, Listmap, Crypto, TextPatcher, JSONSortify, Cryptpad, FO, Toolbar, AppConfig, Get, Merge) {
var module = window.MODULE = {};
var Messages = Cryptpad.Messages;
//var saveAs = window.saveAs;
// Use `$(function () {});` to make sure the html is loaded before doing anything else
$(function () {
var ifrw = $('#pad-iframe')[0].contentWindow;
Cryptpad.addLoadingScreen();
var onConnectError = function () {
Cryptpad.errorLoadingScreen(Messages.websocketError);
};
var APP = window.APP = {
editable: false,
Cryptpad: Cryptpad,
loggedIn: Cryptpad.isLoggedIn(),
mobile: function () { return $('body').width() <= 600; } // Menu and content area are not inline-block anymore for mobiles
};
var stringify = APP.stringify = function (obj) {
return JSONSortify(obj);
};
var E_OVER_LIMIT = 'E_OVER_LIMIT';
var SEARCH = "search";
var SEARCH_NAME = Messages.fm_searchName;
var ROOT = "root";
var ROOT_NAME = Messages.fm_rootName;
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 RECENT = "recent";
var RECENT_NAME = Messages.fm_recentPadsName;
var LOCALSTORAGE_LAST = "cryptpad-file-lastOpened";
var LOCALSTORAGE_OPENED = "cryptpad-file-openedFolders";
var LOCALSTORAGE_VIEWMODE = "cryptpad-file-viewMode";
var FOLDER_CONTENT_ID = "folderContent";
var config = {};
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]) : [ROOT];
} catch (e) {
path = [ROOT];
}
return path;
};
var setLastOpenedFolder = function (path) {
if (path[0] === SEARCH) { return; }
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 setSearchCursor = function () {
var $input = APP.$iframe.find('#searchInput');
localStorage.searchCursor = $input[0].selectionStart;
};
var getSearchCursor = function () {
return localStorage.searchCursor || 0;
};
/* var now = function () {
return new Date().getTime();
}; */
var setEditable = function (state) {
APP.editable = state;
if (!state) {
APP.$iframe.find('#content').addClass('readonly');
APP.$iframe.find('[draggable="true"]').attr('draggable', false);
}
else {
APP.$iframe.find('#content').removeClass('readonly');
APP.$iframe.find('[draggable="false"]').attr('draggable', true);
}
};
// Icons
var $folderIcon = $('<span>', {"class": "fa fa-folder folder icon"});
//var $folderIcon = $('<img>', {src: "/customize/images/icons/folder.svg", "class": "folder icon"});
var $folderEmptyIcon = $folderIcon.clone();
var $folderOpenedIcon = $('<span>', {"class": "fa fa-folder-open folder"});
//var $folderOpenedIcon = $('<img>', {src: "/customize/images/icons/folderOpen.svg", "class": "folder icon"});
var $folderOpenedEmptyIcon = $folderOpenedIcon.clone();
//var $upIcon = $('<span>', {"class": "fa fa-arrow-circle-up"});
var $unsortedIcon = $('<span>', {"class": "fa fa-files-o"});
var $templateIcon = $('<span>', {"class": "fa fa-cubes"});
var $recentIcon = $('<span>', {"class": "fa fa-clock-o"});
var $trashIcon = $('<span>', {"class": "fa fa-trash"});
var $trashEmptyIcon = $('<span>', {"class": "fa fa-trash-o"});
//var $collapseIcon = $('<span>', {"class": "fa fa-minus-square-o expcol"});
var $expandIcon = $('<span>', {"class": "fa fa-plus-square-o expcol"});
var $emptyTrashIcon = $('<button>', {"class": "fa fa-ban"});
var $listIcon = $('<button>', {"class": "fa fa-list"});
var $gridIcon = $('<button>', {"class": "fa fa-th-large"});
var $sortAscIcon = $('<span>', {"class": "fa fa-angle-up sortasc"});
var $sortDescIcon = $('<span>', {"class": "fa fa-angle-down sortdesc"});
var $closeIcon = $('<span>', {"class": "fa fa-window-close"});
var $backupIcon = $('<span>', {"class": "fa fa-life-ring"});
var $searchIcon = $('<span>', {"class": "fa fa-search searchIcon"});
var $addIcon = $('<span>', {"class": "fa fa-plus"});
var $renamedIcon = $('<span>', {"class": "fa fa-flag"});
var $readonlyIcon = $('<span>', {"class": "fa fa-eye"});
var history = {
isHistoryMode: false,
};
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();
config.Cryptpad = Cryptpad;
var $iframe = APP.$iframe;
var filesOp = FO.init(files, config);
filesOp.fixFiles();
var error = filesOp.error;
var $tree = $iframe.find("#tree");
var $content = $iframe.find("#content");
var $appContainer = $iframe.find(".app-container");
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");
$tree.on('drop dragover', function (e) {
e.preventDefault();
e.stopPropagation();
});
$driveToolbar.on('drop dragover', function (e) {
e.preventDefault();
e.stopPropagation();
});
// TOOLBAR
/* add a "change username" button */
if (!APP.readOnly) {
Cryptpad.getLastName(function (err, lastName) {
APP.$displayName.text(lastName || Messages.anonymous);
});
} else {
APP.$displayName.html('<span class="' + Toolbar.constants.readonly + '">' + Messages.readonly + '</span>');
}
// FILE MANAGER
// _WORKGROUP_ and other people drive : display Documents as main page
var currentPath = module.currentPath = isOwnDrive() ? getLastOpenedFolder() : [ROOT];
// Categories dislayed in the menu
// _WORKGROUP_ : do not display unsorted
var displayedCategories = [ROOT, TRASH, SEARCH, RECENT];
if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); }
if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; }
var virtualCategories = [SEARCH, RECENT];
if (!APP.loggedIn) {
displayedCategories = [FILES_DATA];
currentPath = [FILES_DATA];
$tree.hide();
if (Object.keys(files.root).length && !proxy.anonymousAlert) {
Cryptpad.alert(Messages.fm_alert_anonymous, null, true);
proxy.anonymousAlert = true;
}
}
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 findDataHolder = function ($el) {
return $el.is('.element-row') ? $el : $el.closest('.element-row');
};
// Selection
var sel = {};
var removeSelected = function (keepObj) {
$iframe.find('.selected').removeClass("selected");
var $container = $driveToolbar.find('#contextButtonsContainer');
if (!$container.length) { return; }
$container.html('');
if (!keepObj) {
delete sel.startSelected;
delete sel.endSelected;
delete sel.oldSelection;
}
};
sel.refresh = 200;
sel.$selectBox = $('<div>', {'class': 'selectBox'}).appendTo($content);
var checkSelected = function () {
if (!sel.down) { return; }
var pos = sel.pos;
var l = $content[0].querySelectorAll('.element:not(.selected):not(.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('selectedTmp');
} else {
$(el).addClass('selectedTmp');
}
}
};
$content.on('mousedown', function (e) {
if (currentPath[0] === SEARCH) { return; }
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'
});
module.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);
});
$(ifrw).on('mouseup', 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('.selectedTmp').removeClass('selectedTmp').addClass('selected');
e.stopPropagation();
});
// Arrow keys to modify the selection
$(ifrw).keydown(function (e) {
var $searchBar = $tree.find('#searchInput');
if ($searchBar.is(':focus') && $searchBar.val()) { return; }
var $elements = $content.find('.element:not(.header)');
var ev = {};
if (e.ctrlKey) { ev.ctrlKey = true; }
if (e.shiftKey) { ev.shiftKey = true; }
var click = function (el) {
if (!el) { return; }
module.onElementClick(ev, $(el));
};
// Enter
if (e.which === 13) {
var $allSelected = $content.find('.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('.file-element.selected');
$select.each(function (idx, el) {
$(el).dblclick();
});
return;
}
// [Left, Up, Right, Down]
if ([37, 38, 39, 40].indexOf(e.which) === -1) { return; }
e.preventDefault();
var $selection = $content.find('.element.selected');
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 removeInput = function (cancel) {
if (!cancel && $iframe.find('.element-row > input').length === 1) {
var $input = $iframe.find('.element-row > input');
filesOp.rename($input.data('path'), $input.val(), APP.refresh);
}
$iframe.find('.element-row > input').remove();
$iframe.find('.element-row > span:hidden').removeAttr('style');
};
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) {
error("Unable to format that string to a date with .toLocaleString", sDate, e);
}
return ret;
};
var openFile = function (el, href) {
if (!href) {
var data = filesOp.getFileData(el);
if (!data || !data.href) {
return void logError("Missing data for the file", el, data);
}
href = data.href;
}
window.open(href);
};
var refresh = APP.refresh = function () {
module.displayDirectory(currentPath);
};
var getFileNameExtension = function (name) {
var matched = /\.\S+$/.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
window.setTimeout(function () {
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 el = filesOp.find(path);
var name = filesOp.isFile(el) ? filesOp.getTitle(el) : path[path.length - 1];
var $input = $('<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) {
if (e.which === 13) {
removeInput(true);
filesOp.rename(path, $input.val(), refresh);
return;
}
if (e.which === 27) {
removeInput(true);
}
});
//$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) {
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('.element-row').attr("draggable", false);
});
$input.on('mouseup', function (e) {
e.stopPropagation();
$input.parents('.element-row').attr("draggable", true);
});
},0);
};
var filterContextMenu = function ($menu, paths) {
//var path = $element.data('path');
if (!paths || paths.length === 0) { logError('no paths'); }
var hide = [];
var hasFolder = false;
paths.forEach(function (p) {
var path = p.path;
var $element = p.element;
if (path.length === 1) {
hide.push($menu.find('a.rename'));
hide.push($menu.find('a.delete'));
}
if (!APP.editable) {
hide.push($menu.find('a.editable'));
}
if (!isOwnDrive()) {
hide.push($menu.find('a.own'));
}
if ($element.is('.file-element')) {
// No folder in files
hide.push($menu.find('a.newfolder'));
if ($element.is('.readonly')) {
// Keep only open readonly
hide.push($menu.find('a.open'));
} else if ($element.is('.noreadonly')) {
// Keep only open readonly
hide.push($menu.find('a.open_ro'));
}
} else {
if (hasFolder) {
// More than 1 folder selected: cannot create a new subfolder
hide.push($menu.find('a.newfolder'));
}
hasFolder = true;
hide.push($menu.find('a.open_ro'));
hide.push($menu.find('a.properties'));
}
// If we're in the trash, hide restore and properties for non-root elements
if ($menu.find('a.restore').length && path && path.length > 4) {
hide.push($menu.find('a.restore'));
hide.push($menu.find('a.properties'));
}
});
if (paths.length > 1) {
hide.push($menu.find('a.restore'));
hide.push($menu.find('a.properties'));
hide.push($menu.find('a.rename'));
}
if (hasFolder && paths.length > 1) {
// Cannot open multiple folders
hide.push($menu.find('a.open'));
}
return hide;
};
var getSelectedPaths = function ($element) {
var paths = [];
if ($iframe.find('.selected').length > 1) {
var $selected = $iframe.find('.selected');
$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 updateContextButton = function () {
if (filesOp.isPathIn(currentPath, [TRASH])) {
$driveToolbar.find('cp-app-drive-toolbar-emptytrash').show();
} else {
$driveToolbar.find('cp-app-drive-toolbar-emptytrash').hide();
}
var $li = $content.find('.selected');
if ($li.length === 0) {
$li = findDataHolder($tree.find('.active'));
}
var $button = $driveToolbar.find('#contextButton');
if ($button.length) { // mobile
if ($li.length !== 1
|| !$._data($li[0], 'events').contextmenu
|| $._data($li[0], 'events').contextmenu.length === 0) {
$button.hide();
return;
}
$button.show();
$button.css({
background: '#000'
});
window.setTimeout(function () {
$button.css({
background: ''
});
}, 500);
return;
}
// Non mobile
var $container = $driveToolbar.find('#contextButtonsContainer');
if (!$container.length) { return; }
$container.html('');
var $element = $li.length === 1 ? $li : $($li[0]);
var paths = getSelectedPaths($element);
var $menu = $element.data('context');
if (!$menu) { return; }
//var actions = [];
var $actions = $menu.find('a');
var toHide = filterContextMenu($menu, paths);
$actions = $actions.filter(function (i, el) {
for (var j = 0; j < toHide.length; j++) {
if ($(el).is(toHide[j])) { return false; }
}
return true;
});
$actions.each(function (i, el) {
var $a = $('<button>', {'class': 'element'});
if ($(el).attr('data-icon')) {
$a.addClass('fa').addClass($(el).attr('data-icon'));
$a.attr('title', $(el).text());
} else {
$a.text($(el).text());
}
$(el).data('paths', paths);
//$(el).data('path', path);
//:$(el).data('element', $element);
$container.append($a);
$a.click(function() { $(el).click(); });
});
};
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 = module.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) { module.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('.element:not(.header)');
var $selection = $elements.filter('.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 (!$(el).hasClass("selected")) { $(el).addClass("selected"); }
});
for (var i = Math.min(sel.startSelected, sel.endSelected);
i <= Math.max(sel.startSelected, sel.endSelected);
i++) {
$el = $($elements.get(i));
if (!$el.hasClass("selected")) { $el.addClass("selected"); }
}
} else {
if (!$element.hasClass("selected")) {
$element.addClass("selected");
} else {
$element.removeClass("selected");
}
}
updateContextButton();
};
var displayMenu = function (e, $menu) {
$menu.css({ display: "block" });
if (APP.mobile()) { 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 (e, $menu) {
module.hideMenu();
e.stopPropagation();
var $element = findDataHolder($(e.target));
if (!$element.length) {
logError("Unable to locate the .element tag", e.target);
$menu.hide();
log(Messages.fm_contextMenuError);
return false;
}
if (!$element.hasClass('selected')) { //paths.length === 1) {
onElementClick(undefined, $element);
}
var paths = getSelectedPaths($element);
var toHide = filterContextMenu($menu, paths);
toHide.forEach(function ($a) {
$a.parent('li').hide();
});
displayMenu(e, $menu);
if ($menu.find('li:visible').length === 0) {
debug("No visible element in the context menu. Abort.");
$menu.hide();
return true;
}
$menu.find('a').data('paths', paths);
//$menu.find('a').data('path', path);
//$menu.find('a').data('element', $element);
return false;
};
var openDirectoryContextMenu = function (e) {
$contextMenu.find('li').show();
openContextMenu(e, $contextMenu);
return false;
};
var openDefaultContextMenu = function (e) {
$defaultContextMenu.find('li').show();
openContextMenu(e, $defaultContextMenu);
return false;
};
var openTrashTreeContextMenu = function (e) {
removeSelected();
$trashTreeContextMenu.find('li').show();
openContextMenu(e, $trashTreeContextMenu);
return false;
};
var openTrashContextMenu = function (e) {
var path = findDataHolder($(e.target)).data('path');
if (!path) { return; }
$trashContextMenu.find('li').show();
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();
}
});
displayMenu(e, $menu);
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) {
var file = filesOp.find(path);
if (!file || !filesOp.isFile(file)) { return '???'; }
return filesOp.getTitle(file);
};
// 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.move(paths, newPath, cb);
};
// Cancel drag&drop from TRASH to TRASH
if (filesOp.isPathIn(newPath, [TRASH]) && paths.length && paths[0][0] === TRASH) {
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 = findDataHolder($(ev.target));
if ($element.hasClass('selected')) {
var $selected = $iframe.find('.selected');
$selected.each(function (idx, elmt) {
var ePath = $(elmt).data('path');
if (ePath) {
var val = filesOp.find(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.find(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 onFileDrop = APP.onFileDrop = function (file, e) {
APP.FM.onFileDrop(file, e);
};
var findDropPath = function (target) {
var $target = $(target);
var $el = findDataHolder($target);
var newPath = $el.data('path');
if ((!newPath || filesOp.isFile(filesOp.find(newPath)))
&& $target.parents('#content')) {
newPath = currentPath;
}
return newPath;
};
var onDrop = function (ev) {
ev.preventDefault();
$iframe.find('.droppable').removeClass('droppable');
var data = ev.dataTransfer.getData("text");
// Don't the the normal drop handler for file upload
var fileDrop = ev.dataTransfer.files;
if (fileDrop.length) { return void onFileDrop(fileDrop, ev); }
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.find(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 = findDropPath(ev.target);
if (!newPath) { return; }
if (movedPaths && movedPaths.length) {
moveElements(movedPaths, newPath, null, refresh);
}
if (importedElements && importedElements.length) {
// TODO workgroup
//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) {
e.preventDefault();
e.stopPropagation();
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');
}
});
};
addDragAndDropHandlers($content, null, true, true);
// 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, $span) {
if (!filesOp.isFile(element)) { return; }
var data = filesOp.getFileData(element);
if (!data) { return void logError("No data for the file", element); }
var hrefData = Cryptpad.parsePadUrl(data.href);
var $state = $('<span>', {'class': '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('title', Messages._getKey('fm_renamedPad', [data.title]));
}
var name = filesOp.getTitle(element);
// The element with the class '.name' is underlined when the 'li' is hovered
var $name = $('<span>', {'class': 'name'}).text(name);
$span.html('');
$span.append($name);
$span.append($state);
var type = Messages.type[hrefData.type] || hrefData.type;
var $type = $('<span>', {'class': 'type listElement'}).text(type);
var $adate = $('<span>', {'class': 'atime listElement'}).text(getDate(data.atime));
var $cdate = $('<span>', {'class': 'ctime listElement'}).text(getDate(data.ctime));
$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 = $('<span>', {'class': 'name'}).text(key);
var $state = $('<span>', {'class': 'state'});
var $subfolders = $('<span>', {'class': 'folders listElement'}).text(sf);
var $files = $('<span>', {'class': 'files listElement'}).text(files);
$span.append($name).append($state).append($subfolders).append($files);
};
// This is duplicated in cryptpad-common, it should be unified
var getFileIcon = function (id) {
var data = filesOp.getFileData(id);
return Cryptpad.getFileIcon(data);
};
var getIcon = Cryptpad.getIcon;
// 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 && Array.isArray(elPath)) {
key = elPath[0];
elPath.forEach(function (k) { newPath.push(k); });
} else {
key = elPath;
newPath.push(key);
}
var element = filesOp.find(newPath);
var $icon = !isFolder ? getFileIcon(element) : undefined;
var ro = filesOp.isReadOnlyFile(element);
// ro undefined means it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ? ' noreadonly' : ro ? ' readonly' : '';
var liClass = 'file-item file-element element' + roClass;
if (isFolder) {
liClass = 'folder-item folder-element element';
$icon = filesOp.isFolderEmpty(root[key]) ? $folderEmptyIcon.clone() : $folderIcon.clone();
}
var $element = $('<li>', {
draggable: true,
'class': 'element-row'
});
if (!isFolder && Array.isArray(APP.selectedFiles)) {
var idx = APP.selectedFiles.indexOf(element);
if (idx !== -1) {
$element.addClass('selected');
APP.selectedFiles.splice(idx, 1);
}
}
if (isFolder) {
addFolderData(element, key, $element);
} else {
addFileData(element, $element);
}
$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);
$element.data('context', $contextMenu);
} else {
$element.contextmenu(openTrashContextMenu);
$element.data('context', $trashContextMenu);
}
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<path.length; i++) {
if (i === 3 && path[i] === 'element') {}
else if (i === 2 && parseInt(path[i]) === path[i]) {
if (path[i] !== 0) {
title += " [" + path[i] + "]";
}
} else {
title += " / " + path[i];
}
}
return title;
}; */
var getPrettyName = function (name) {
var pName;
switch (name) {
case ROOT: pName = ROOT_NAME; break;
case TRASH: pName = TRASH_NAME; break;
case TEMPLATE: pName = TEMPLATE_NAME; break;
case FILES_DATA: pName = FILES_DATA_NAME; break;
case SEARCH: pName = SEARCH_NAME; break;
case RECENT: pName = RECENT_NAME; break;
default: pName = name;
}
return pName;
};
// Create the title block with the "parent folder" button
var createTitle = function ($container, path, noStyle) {
if (!path || path.length === 0) { return; }
var isTrash = filesOp.isPathIn(path, [TRASH]);
if (APP.mobile() && !noStyle) { // noStyle means title in search result
return $container;
}
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var el = isVirtual ? undefined : filesOp.find(path);
path = path[0] === SEARCH ? path.slice(0,1) : path;
path.forEach(function (p, idx) {
if (isTrash && [2,3].indexOf(idx) !== -1) { return; }
var name = p;
var $span = $('<span>', {'class': 'element'});
if (idx < path.length - 1) {
if (!noStyle) {
$span.addClass('clickable');
$span.click(function () {
var sliceEnd = idx + 1;
if (isTrash && idx === 1) { sliceEnd = 4; } // Make sure we don't show the index or 'element' and 'path'
module.displayDirectory(path.slice(0, sliceEnd));
});
}
} else if (idx > 0 && filesOp.isFile(el)) {
name = getElementName(path);
}
if (idx === 0) { name = getPrettyName(p); }
else {
var $span2 = $('<span>', {'class': 'element separator'}).text(' / ');
$container.prepend($span2);
}
$span.text(name).prependTo($container);
});
};
var createInfoBox = function (path) {
var $box = $('<div>', {'class': 'info-box'});
var msg;
switch (path[0]) {
case ROOT:
msg = Messages.fm_info_root;
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;
case RECENT:
msg = Messages.fm_info_recent;
break;
default:
msg = undefined;
}
if (!APP.loggedIn) {
msg = Messages.fm_info_anonymous;
$box.html(msg);
$box.addClass('noclose');
return $box;
}
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 ($container) {
var $listButton = $listIcon.clone().addClass('element');
var $gridButton = $gridIcon.clone().addClass('element');
$listButton.click(function () {
$gridButton.removeClass('active');
$listButton.addClass('active');
setViewMode('list');
$iframe.find('#' + FOLDER_CONTENT_ID).removeClass('grid');
$iframe.find('#' + FOLDER_CONTENT_ID).addClass('list');
Cryptpad.feedback('DRIVE_LIST_MODE');
});
$gridButton.click(function () {
$listButton.removeClass('active');
$gridButton.addClass('active');
setViewMode('grid');
$iframe.find('#' + FOLDER_CONTENT_ID).addClass('grid');
$iframe.find('#' + FOLDER_CONTENT_ID).removeClass('list');
Cryptpad.feedback('DRIVE_GRID_MODE');
});
if (getViewMode() === 'list') {
$listButton.addClass('active');
} else {
$gridButton.addClass('active');
}
$listButton.attr('title', Messages.fm_viewListButton);
$gridButton.attr('title', Messages.fm_viewGridButton);
$container.append($listButton).append($gridButton);
};
var createEmptyTrashButton = function ($container) {
var $button = $emptyTrashIcon.clone().addClass('element');
$button.addClass('cp-app-drive-toolbar-emptytrash');
$button.attr('title', Messages.fc_empty);
$button.click(function () {
Cryptpad.confirm(Messages.fm_emptyTrashDialog, function(res) {
if (!res) { return; }
filesOp.emptyTrash(refresh);
});
});
$container.append($button);
};
var getNewPadTypes = function () {
var arr = [];
AppConfig.availablePadTypes.forEach(function (type) {
if (type === 'drive') { return; }
if (type === 'contacts') { return; }
if (type === 'todo') { return; }
if (type === 'file') { return; }
if (!Cryptpad.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(type) !== -1) {
return;
}
arr.push(type);
});
return arr;
};
var addNewPadHandlers = function ($block, isInRoot) {
// Handlers
if (isInRoot) {
var onCreated = function (err, info) {
if (err) {
if (err === E_OVER_LIMIT) {
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
}
return void Cryptpad.alert(Messages.fm_error_cantPin);
}
module.newFolder = info.newPath;
refresh();
};
$block.find('a.newFolder, li.newFolder').click(function () {
filesOp.addFolder(currentPath, null, onCreated);
});
$block.find('a.uploadFile, li.uploadFile').click(function () {
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;'
}).on('change', function (e) {
var file = e.target.files[0];
var ev = {
target: $content[0]
};
APP.FM.handleFile(file, ev);
});
$input.click();
});
}
$block.find('a.newdoc, li.newdoc').click(function () {
var type = $(this).attr('data-type') || 'pad';
sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
window.open('/' + type + '/');
});
};
var createNewButton = function (isInRoot, $container) {
if (!APP.editable) { return; }
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
if (!filesOp.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
// Create dropdown
var options = [];
if (isInRoot) {
options.push({
tag: 'a',
attributes: {'class': 'newFolder'},
content: $('<div>').append($folderIcon.clone()).html() + Messages.fm_folder
});
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'uploadFile'},
content: $('<div>').append(getIcon('file')).html() + Messages.uploadButton
});
options.push({tag: 'hr'});
}
getNewPadTypes().forEach(function (type) {
var attributes = {
'class': 'newdoc',
'data-type': type,
'href': '#'
};
options.push({
tag: 'a',
attributes: attributes,
content: $('<div>').append(getIcon(type)).html() + Messages.type[type]
});
});
var $plusIcon = $('<div>').append($('<span>', {'class': 'fa fa-plus'}));
var dropdownConfig = {
text: $plusIcon.html() + '<span>'+Messages.fm_newButton+'</span>',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
};
var $block = Cryptpad.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('new');
$block.find('button').attr('title', Messages.fm_newButtonTitle);
addNewPadHandlers($block, isInRoot);
$container.append($block);
};
var hideNewButton = function () {
$iframe.find('.cp-dropdown-content').hide();
};
var SORT_FOLDER_DESC = 'sortFoldersDesc';
var SORT_FILE_BY = 'sortFilesBy';
var SORT_FILE_DESC = 'sortFilesDesc';
var getSortFileDesc = function () {
return Cryptpad.getLSAttribute(SORT_FILE_DESC) === "true";
};
var getSortFolderDesc = function () {
return Cryptpad.getLSAttribute(SORT_FOLDER_DESC) === "true";
};
var onSortByClick = function () {
var $span = $(this);
var value;
if ($span.hasClass('foldername')) {
value = getSortFolderDesc();
Cryptpad.setLSAttribute(SORT_FOLDER_DESC, value ? false : true);
refresh();
return;
}
value = Cryptpad.getLSAttribute(SORT_FILE_BY);
var descValue = getSortFileDesc();
if ($span.hasClass('filename')) {
if (value === '') {
descValue = descValue ? false : true;
} else {
descValue = false;
value = '';
}
} else {
var found = false;
['title', 'type', 'atime', 'ctime'].forEach(function (c) {
if (!found && $span.hasClass(c)) {
found = true;
if (value === c) { descValue = descValue ? false : true; }
else {
// atime and ctime should be ordered in a desc order at the first click
descValue = c !== 'title';
value = c;
}
}
});
}
Cryptpad.setLSAttribute(SORT_FILE_BY, value);
Cryptpad.setLSAttribute(SORT_FILE_DESC, descValue);
refresh();
};
var addFolderSortIcon = function ($list) {
var $icon = $sortAscIcon.clone();
if (getSortFolderDesc()) {
$icon = $sortDescIcon.clone();
}
if (typeof(Cryptpad.getLSAttribute(SORT_FOLDER_DESC)) !== "undefined") {
$list.find('.foldername').addClass('active').prepend($icon);
}
};
var getFolderListHeader = function () {
var $fohElement = $('<li>', {'class': 'header listElement'});
//var $fohElement = $('<span>', {'class': 'element'}).appendTo($folderHeader);
var $fhIcon = $('<span>', {'class': 'icon'});
var $name = $('<span>', {'class': 'name foldername clickable'}).text(Messages.fm_folderName).click(onSortByClick);
var $state = $('<span>', {'class': 'state'});
var $subfolders = $('<span>', {'class': 'folders listElement'}).text(Messages.fm_numberOfFolders);
var $files = $('<span>', {'class': 'files listElement'}).text(Messages.fm_numberOfFiles);
$fohElement.append($fhIcon).append($name).append($state)
.append($subfolders).append($files);
addFolderSortIcon($fohElement);
return $fohElement;
};
var addFileSortIcon = function ($list) {
var $icon = $sortAscIcon.clone();
if (getSortFileDesc()) {
$icon = $sortDescIcon.clone();
}
var classSorted;
if (Cryptpad.getLSAttribute(SORT_FILE_BY) === '') { classSorted = 'filename'; }
else if (Cryptpad.getLSAttribute(SORT_FILE_BY)) { classSorted = Cryptpad.getLSAttribute(SORT_FILE_BY); }
if (classSorted) {
$list.find('.' + classSorted).addClass('active').prepend($icon);
}
};
// _WORKGROUP_ : do not display title, atime and ctime in workgroups since we don't have files data
var getFileListHeader = function () {
var $fihElement = $('<li>', {'class': 'file-header header listElement element'});
//var $fihElement = $('<span>', {'class': 'element'}).appendTo($fileHeader);
var $fhIcon = $('<span>', {'class': 'icon'});
var $fhName = $('<span>', {'class': 'name filename clickable'}).text(Messages.fm_fileName).click(onSortByClick);
var $fhState = $('<span>', {'class': 'state'});
var $fhType = $('<span>', {'class': 'type clickable'}).text(Messages.fm_type).click(onSortByClick);
var $fhAdate = $('<span>', {'class': 'atime clickable'}).text(Messages.fm_lastAccess).click(onSortByClick);
var $fhCdate = $('<span>', {'class': 'ctime clickable'}).text(Messages.fm_creation).click(onSortByClick);
// If displayTitle is false, it means the "name" is the title, so do not display the "name" header
$fihElement.append($fhIcon).append($fhName).append($fhState).append($fhType);
if (!isWorkgroup()) {
$fihElement.append($fhAdate).append($fhCdate);
}
addFileSortIcon($fihElement);
return $fihElement;
};
var sortElements = function (folder, path, oldkeys, prop, asc, useId) {
var root = filesOp.find(path);
var test = folder ? filesOp.isFolder : filesOp.isFile;
var keys = oldkeys.filter(function (e) {
return useId ? test(e) : test(root[e]);
});
if (keys.length < 2) { return keys; }
var mult = asc ? 1 : -1;
var getProp = function (el, prop) {
if (folder) { return el.toLowerCase(); }
var id = useId ? el : root[el];
var data = filesOp.getFileData(id);
if (!data) { return ''; }
if (prop === 'type') {
var hrefData = Cryptpad.parsePadUrl(data.href);
return hrefData.type;
}
if (prop === 'atime' || prop === 'ctime') {
return new Date(data[prop]);
}
return (filesOp.getTitle(id) || "").toLowerCase();
};
keys.sort(function(a, b) {
if (getProp(a, prop) < getProp(b, prop)) { return mult * -1; }
if (getProp(a, prop) > getProp(b, prop)) { return mult * 1; }
return 0;
});
return keys;
};
var sortTrashElements = function (folder, oldkeys, prop, asc) {
var test = folder ? filesOp.isFolder : filesOp.isFile;
var keys = oldkeys.filter(function (e) {
return test(e.element);
});
if (keys.length < 2) { return keys; }
var mult = asc ? 1 : -1;
var getProp = function (el, prop) {
if (prop && !folder) {
var element = el.element;
var e = filesOp.getFileData(element);
if (!e) {
e = {
href : el,
title : Messages.fm_noname,
atime : 0,
ctime : 0
};
}
if (prop === 'type') {
var hrefData = Cryptpad.parsePadUrl(e.href);
return hrefData.type;
}
if (prop === 'atime' || prop === 'ctime') {
return new Date(e[prop]);
}
}
return (el.name || "").toLowerCase();
};
keys.sort(function(a, b) {
if (getProp(a, prop) < getProp(b, prop)) { return mult * -1; }
if (getProp(a, prop) > getProp(b, prop)) { return mult * 1; }
return 0;
});
return keys;
};
// Create the ghost icon to add pads/folders
var createNewPadIcons = function ($block, isInRoot) {
var $container = $('<div>');
if (isInRoot) {
// Folder
var $element1 = $('<li>', {
'class': 'newFolder element-row grid-element'
}).prepend($folderIcon.clone()).appendTo($container);
$element1.append($('<span>', {'class': 'name'}).text(Messages.fm_folder));
// File
var $element2 = $('<li>', {
'class': 'uploadFile element-row grid-element'
}).prepend(getIcon('file')).appendTo($container);
$element2.append($('<span>', {'class': 'name'}).text(Messages.uploadButton));
}
// Pads
getNewPadTypes().forEach(function (type) {
var $element = $('<li>', {
'class': 'newdoc element-row grid-element'
}).prepend(getIcon(type)).appendTo($container);
$element.append($('<span>', {'class': 'name'}).text(Messages.type[type]));
$element.attr('data-type', type);
});
$container.find('.element-row').click(function () {
$block.hide();
});
return $container;
};
var createGhostIcon = function ($list) {
var isInRoot = currentPath[0] === ROOT;
var $element = $('<li>', {
'class': 'element-row grid-element addpad'
}).prepend($addIcon.clone()).appendTo($list);
$element.append($('<span>', {'class': 'name'}).text(Messages.fm_newFile));
$element.attr('title', Messages.fm_newFile);
$element.click(function () {
var $modal = Cryptpad.createModal({
id: 'addPadDialog',
$body: $iframe.find('body')
});
var $title = $('<h3>').text(Messages.fm_newFile);
var $description = $('<p>').text(Messages.fm_newButtonTitle);
$modal.find('.cp-modal').append($title);
$modal.find('.cp-modal').append($description);
var $content = createNewPadIcons($modal, isInRoot);
$modal.find('.cp-modal').append($content);
window.setTimeout(function () { $modal.show(); });
addNewPadHandlers($modal, isInRoot);
});
};
// Drive content toolbar
var createToolbar = function () {
var $toolbar = $driveToolbar;
$toolbar.html('');
$('<div>', {'class': 'leftside'}).appendTo($toolbar);
$('<div>', {'class': 'path unselectable'}).appendTo($toolbar);
var $rightside = $('<div>', {'class': 'rightside'}).appendTo($toolbar);
var $hist = Cryptpad.createButton('history', true, {histConfig: APP.histConfig});
$hist.addClass('element');
$rightside.append($hist);
return $toolbar;
};
// Unsorted element are represented by "href" in an array: they don't have a filename
// and they don't hav a hierarchical structure (folder/subfolders)
var displayHrefArray = function ($container, rootName, draggable) {
var unsorted = files[rootName];
if (unsorted.length) {
var $fileHeader = getFileListHeader(false);
$container.append($fileHeader);
}
var keys = unsorted;
var sortBy = Cryptpad.getLSAttribute(SORT_FILE_BY);
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
sortedFiles.forEach(function (id) {
var file = filesOp.getFileData(id);
if (!file) {
//debug("Unsorted or template returns an element not present in filesData: ", href);
file = { title: Messages.fm_noname };
//return;
}
var idx = files[rootName].indexOf(id);
var $icon = getFileIcon(id);
var ro = filesOp.isReadOnlyFile(id);
// ro undefined mens it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ? ' noreadonly' : ro ? ' readonly' : '';
var $element = $('<li>', {
'class': 'file-element element element-row' + roClass,
draggable: draggable
});
if (Array.isArray(APP.selectedFiles)) {
var sidx = APP.selectedFiles.indexOf(id);
if (sidx !== -1) {
$element.addClass('selected');
APP.selectedFiles.splice(sidx, 1);
}
}
addFileData(id, $element);
$element.prepend($icon).dblclick(function () {
openFile(id);
});
var path = [rootName, idx];
$element.data('path', path);
$element.click(function(e) {
e.stopPropagation();
onElementClick(e, $element, path);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
if (draggable) {
addDragAndDropHandlers($element, path, false, false);
}
$container.append($element);
});
createGhostIcon($container);
};
var displayAllFiles = function ($container) {
var allfiles = files[FILES_DATA];
if (allfiles.length === 0) { return; }
var $fileHeader = getFileListHeader(false);
$container.append($fileHeader);
var keys = filesOp.getFiles([FILES_DATA]);
var sortedFiles = sortElements(false, [FILES_DATA], keys, Cryptpad.getLSAttribute(SORT_FILE_BY), !getSortFileDesc(), true);
sortedFiles.forEach(function (id) {
var $icon = getFileIcon(id);
var ro = filesOp.isReadOnlyFile(id);
// ro undefined maens it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ? ' noreadonly' : ro ? ' readonly' : '';
var $element = $('<li>', { 'class': 'file-element element element-row' + roClass });
addFileData(id, $element);
$element.data('path', [FILES_DATA, id]);
$element.data('element', id);
$element.prepend($icon).dblclick(function () {
openFile(id);
});
$element.click(function(e) {
e.stopPropagation();
onElementClick(e, $element);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$container.append($element);
});
};
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
var filesList = [];
var root = files[TRASH];
// Elements in the trash are JS arrays (several elements can have the same name)
Object.keys(root).forEach(function (key) {
if (!Array.isArray(root[key])) {
logError("Trash element has a wrong type", root[key]);
return;
}
root[key].forEach(function (el, idx) {
if (!filesOp.isFile(el.element) && !filesOp.isFolder(el.element)) { return; }
var spath = [key, idx, 'element'];
filesList.push({
element: el.element,
spath: spath,
name: key
});
});
});
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, Cryptpad.getLSAttribute(SORT_FILE_BY), !getSortFileDesc());
if (filesOp.hasSubfolder(root, true)) { $list.append($folderHeader); }
sortedFolders.forEach(function (f) {
var $element = createElement([TRASH], f.spath, root, true);
$list.append($element);
});
if (filesOp.hasFile(root, true)) { $list.append($fileHeader); }
sortedFiles.forEach(function (f) {
var $element = createElement([TRASH], f.spath, root, false);
$list.append($element);
});
};
var displaySearch = function ($list, value) {
var filesList = filesOp.search(value);
filesList.forEach(function (r) {
r.paths.forEach(function (path) {
var href = r.data.href;
var parsed = Cryptpad.parsePadUrl(href);
var $table = $('<table>');
var $icon = $('<td>', {'rowspan': '3', 'class': 'icon'}).append(getFileIcon(href));
var $title = $('<td>', {'class': 'col1 title'}).text(r.data.title)
.click(function () {
openFile(null, r.data.href);
});
var $typeName = $('<td>', {'class': 'label2'}).text(Messages.fm_type);
var $type = $('<td>', {'class': 'col2'}).text(Messages.type[parsed.type] || parsed.type);
var $atimeName = $('<td>', {'class': 'label2'}).text(Messages.fm_lastAccess);
var $atime = $('<td>', {'class': 'col2'}).text(new Date(r.data.atime).toLocaleString());
var $ctimeName = $('<td>', {'class': 'label2'}).text(Messages.fm_creation);
var $ctime = $('<td>', {'class': 'col2'}).text(new Date(r.data.ctime).toLocaleString());
if (filesOp.isPathIn(path, ['hrefArray'])) {
path.pop();
path.push(r.data.title);
}
var $path = $('<td>', {'class': 'col1 path'});
createTitle($path, path, true);
var parentPath = path.slice();
var $a;
if (parentPath) {
$a = $('<a>').text(Messages.fm_openParent).click(function (e) {
e.preventDefault();
if (filesOp.isInTrashRoot(parentPath)) { parentPath = [TRASH]; }
else { parentPath.pop(); }
APP.selectedFiles = [r.id];
module.displayDirectory(parentPath);
});
}
var $openDir = $('<td>', {'class': 'openDir'}).append($a);
// rows 1-3
$('<tr>').append($icon).append($title).append($typeName).append($type).appendTo($table);
$('<tr>').append($path).append($atimeName).append($atime).appendTo($table);
$('<tr>').append($openDir).append($ctimeName).append($ctime).appendTo($table);
$('<li>', {'class':'searchResult'}).append($table).appendTo($list);
});
});
};
var displayRecent = function ($list) {
var filesList = filesOp.getRecentPads();
var limit = 20;
var i = 0;
filesList.forEach(function (id) {
if (i >= limit) { return; }
// Check path (pad exists and not in trash)
var paths = filesOp.findFile(id);
if (!paths.length) { return; }
var path = paths[0];
if (filesOp.isPathIn(path, [TRASH])) { return; }
// Display the pad
var file = filesOp.getFileData(id);
if (!file) {
//debug("Unsorted or template returns an element not present in filesData: ", href);
file = { title: Messages.fm_noname };
//return;
}
var $icon = getFileIcon(id);
var ro = filesOp.isReadOnlyFile(id);
// ro undefined mens it's an old hash which doesn't support read-only
var roClass = typeof(ro) === 'undefined' ? ' noreadonly' : ro ? ' readonly' : '';
var $element = $('<li>', {
'class': 'file-element element element-row' + roClass,
});
addFileData(id, $element);
$element.prepend($icon).dblclick(function () {
openFile(id);
});
$element.data('path', path);
$element.click(function(e) {
e.stopPropagation();
onElementClick(e, $element, path);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
/*if (draggable) {
addDragAndDropHandlers($element, path, false, false);
}*/
$list.append($element);
i++;
});
};
// Display the selected directory into the content part (rightside)
// NOTE: Elements in the trash are not using the same storage structure as the others
// _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage
var displayDirectory = module.displayDirectory = function (path, force) {
module.hideMenu();
if (!APP.editable) { debug("Read-only mode"); }
if (!appStatus.isReady && !force) { return; }
// Only Trash and Root are available in not-owned files manager
if (displayedCategories.indexOf(path[0]) === -1) {
log(Messages.categoryError);
currentPath = [ROOT];
displayDirectory(currentPath);
return;
}
appStatus.ready(false);
currentPath = path;
var s = $content.scrollTop() || 0;
$content.html("");
sel.$selectBox = $('<div>', {'class': 'selectBox'}).appendTo($content);
if (!path || path.length === 0) {
path = [ROOT];
}
var isInRoot = filesOp.isPathIn(path, [ROOT]);
var inTrash = filesOp.isPathIn(path, [TRASH]);
var isTrashRoot = filesOp.comparePath(path, [TRASH]);
var isTemplate = filesOp.comparePath(path, [TEMPLATE]);
var isAllFiles = filesOp.comparePath(path, [FILES_DATA]);
var isSearch = path[0] === SEARCH;
var isRecent = path[0] === RECENT;
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var root = isVirtual ? undefined : filesOp.find(path);
if (!isVirtual && typeof(root) === "undefined") {
log(Messages.fm_unknownFolderError);
debug("Unable to locate the selected directory: ", path);
var parentPath = path.slice();
parentPath.pop();
displayDirectory(parentPath, true);
return;
}
if (!isSearch) { delete APP.Search.oldLocation; }
module.resetTree();
if (displayedCategories.indexOf(SEARCH) !== -1 && $tree.find('#searchInput').length) {
// in history mode we want to focus the version number input
if (!history.isHistoryMode && !APP.mobile()) {
var st = $tree.scrollTop() || 0;
$tree.find('#searchInput').focus();
$tree.scrollTop(st);
}
$tree.find('#searchInput')[0].selectionStart = getSearchCursor();
$tree.find('#searchInput')[0].selectionEnd = getSearchCursor();
}
if (!isWorkgroup()) {
setLastOpenedFolder(path);
}
var $toolbar = createToolbar(path);
var $info = createInfoBox(path);
var $dirContent = $('<div>', {id: FOLDER_CONTENT_ID});
$dirContent.data('path', path);
if (!isSearch) {
var mode = getViewMode();
if (mode) {
$dirContent.addClass(getViewModeClass());
}
createViewModeButton($toolbar.find('.rightside'));
}
if (inTrash) {
createEmptyTrashButton($toolbar.find('.rightside'));
}
var $list = $('<ul>').appendTo($dirContent);
// NewButton can be undefined if we're in read only mode
createNewButton(isInRoot, $toolbar.find('.leftside'));
createTitle($toolbar.find('.path'), path);
if (APP.mobile()) {
var $context = $('<button>', {'class': 'element right cp-dropdown-container', id: 'contextButton'});
$context.append($('<span>', {'class': 'fa fa-caret-down'}));
$context.appendTo($toolbar.find('.rightside'));
$context.click(function (e) {
e.preventDefault();
e.stopPropagation();
var $li = $content.find('.selected');
if ($li.length !== 1) {
$li = findDataHolder($tree.find('.active'));
}
// Close if already opened
if ($iframe.find('.contextMenu:visible').length) {
module.hideMenu();
return;
}
// Open the menu
$iframe.find('.contextMenu').css({
top: ($context.offset().top + 32) + 'px',
right: '0px',
left: ''
});
$li.contextmenu();
});
} else {
var $contextButtons = $('<span>', {'id' : 'contextButtonsContainer'});
$contextButtons.appendTo($toolbar.find('.rightside'));
}
updateContextButton();
var $folderHeader = getFolderListHeader();
var $fileHeader = getFileListHeader(true);
if (isTemplate) {
displayHrefArray($list, path[0], true);
} else if (isAllFiles) {
displayAllFiles($list);
} else if (isTrashRoot) {
displayTrashRoot($list, $folderHeader, $fileHeader);
} else if (isSearch) {
displaySearch($list, path[1]);
} else if (isRecent) {
displayRecent($list);
} else {
$dirContent.contextmenu(openContentContextMenu);
if (filesOp.hasSubfolder(root)) { $list.append($folderHeader); }
// display sub directories
var keys = Object.keys(root);
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFiles = sortElements(false, path, keys, Cryptpad.getLSAttribute(SORT_FILE_BY), !getSortFileDesc());
sortedFolders.forEach(function (key) {
if (filesOp.isFile(root[key])) { return; }
var $element = createElement(path, key, root, true);
$element.appendTo($list);
});
if (filesOp.hasFile(root)) { $list.append($fileHeader); }
// display files
sortedFiles.forEach(function (key) {
if (filesOp.isFolder(root[key])) { return; }
var $element = createElement(path, key, root, false);
$element.appendTo($list);
});
createGhostIcon($list);
}
$content.append($info).append($dirContent);
var $truncated = $('<span>', {'class': 'truncated'}).text('...');
$content.find('.element').each(function (idx, el) {
var $name = $(el).find('.name');
if ($name.length === 0) { return; }
if ($name[0].scrollHeight > $name[0].clientHeight) {
var $tr = $truncated.clone();
$tr.attr('title', $name.text());
$(el).append($tr);
}
});
$content.scrollTop(s);
appStatus.ready(true);
};
/* var refreshFilesData = function () {
$content.find('.element-row').each(function (i, e) {
var $el = $(e);
if ($el.data('path')) {
var path = $el.data('path');
var element = filesOp.find(path);
if (!filesOp.isFile(element)) { return; }
var data = filesOp.getFileData(element);
if (!data) { return; }
if (filesOp.isPathIn(path, ['hrefArray'])) { $el.find('.name').attr('title', data.title).text(data.title); }
$el.find('.title').attr('title', data.title).text(data.title);
$el.find('.atime').attr('title', getDate(data.atime)).text(getDate(data.atime));
$el.find('.ctime').attr('title', getDate(data.ctime)).text(getDate(data.ctime));
}
});
}; */
var createTreeElement = function (name, $icon, path, draggable, droppable, collapsable, active) {
var $name = $('<span>', { 'class': 'folder-element element' }).text(name);
var $collapse;
if (collapsable) {
$collapse = $expandIcon.clone();
}
var $elementRow = $('<span>', {'class': 'element-row'}).append($collapse).append($icon).append($name).click(function (e) {
e.stopPropagation();
module.displayDirectory(path);
});
var $element = $('<li>').append($elementRow);
if (draggable) { $elementRow.attr('draggable', true); }
if (collapsable) {
$element.addClass('collapsed');
$collapse.click(function(e) {
e.stopPropagation();
if ($element.hasClass('collapsed')) {
// It is closed, open it
$element.removeClass('collapsed');
setFolderOpened(path, true);
$collapse.removeClass('fa-plus-square-o');
$collapse.addClass('fa-minus-square-o');
} else {
// Collapse the folder
$element.addClass('collapsed');
setFolderOpened(path, false);
$collapse.removeClass('fa-minus-square-o');
$collapse.addClass('fa-plus-square-o');
// Change the current opened folder if it was collapsed
if (filesOp.isSubpath(currentPath, path)) {
displayDirectory(path);
}
}
});
if (wasFolderOpened(path) ||
(filesOp.isSubpath(currentPath, path) && path.length < currentPath.length)) {
$collapse.click();
}
}
$elementRow.data('path', path);
addDragAndDropHandlers($elementRow, path, true, droppable);
if (active) {
$elementRow.addClass('active');
}
return $element;
};
var createTree = function ($container, path) {
var root = filesOp.find(path);
// don't try to display what doesn't exist
if (!root) { return; }
// Display the root element in the tree
var displayingRoot = filesOp.comparePath([ROOT], path);
if (displayingRoot) {
var isRootOpened = filesOp.comparePath([ROOT], currentPath);
var $rootIcon = filesOp.isFolderEmpty(files[ROOT]) ?
(isRootOpened ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
(isRootOpened ? $folderOpenedIcon : $folderIcon);
var $rootElement = createTreeElement(ROOT_NAME, $rootIcon.clone(), [ROOT], false, true, true, isRootOpened);
if (!filesOp.hasSubfolder(root)) {
$rootElement.find('.expcol').css('visibility', 'hidden');
}
$rootElement.addClass('root');
$rootElement.find('>.element-row').contextmenu(openDirectoryContextMenu);
$('<ul>', {'class': 'docTree'}).append($rootElement).appendTo($container);
$container = $rootElement;
} else if (filesOp.isFolderEmpty(root)) { return; }
// Display root content
var $list = $('<ul>').appendTo($container);
var keys = Object.keys(root).sort();
keys.forEach(function (key) {
// Do not display files in the menu
if (!filesOp.isFolder(root[key])) { return; }
var newPath = path.slice();
newPath.push(key);
var isCurrentFolder = filesOp.comparePath(newPath, currentPath);
var isEmpty = filesOp.isFolderEmpty(root[key]);
var subfolder = filesOp.hasSubfolder(root[key]);
var $icon = isEmpty ?
(isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
(isCurrentFolder ? $folderOpenedIcon : $folderIcon);
var $element = createTreeElement(key, $icon.clone(), newPath, true, true, subfolder, isCurrentFolder);
$element.appendTo($list);
$element.find('>.element-row').contextmenu(openDirectoryContextMenu);
createTree($element, newPath);
});
};
var createTemplate = function ($container, path) {
var $icon = $templateIcon.clone();
var isOpened = filesOp.comparePath(path, currentPath);
var $element = createTreeElement(TEMPLATE_NAME, $icon, [TEMPLATE], false, true, false, isOpened);
$element.addClass('root');
var $list = $('<ul>', { id: 'templateTree', 'class': 'category' }).append($element);
$container.append($list);
};
var createAllFiles = function ($container, path) {
var $icon = $unsortedIcon.clone();
var isOpened = filesOp.comparePath(path, currentPath);
var $allfilesElement = createTreeElement(FILES_DATA_NAME, $icon, [FILES_DATA], false, false, false, isOpened);
$allfilesElement.addClass('root');
var $allfilesList = $('<ul>', { id: 'allfilesTree', 'class': 'category' }).append($allfilesElement);
$container.append($allfilesList);
};
var createTrash = function ($container, path) {
var $icon = filesOp.isFolderEmpty(files[TRASH]) ? $trashEmptyIcon.clone() : $trashIcon.clone();
var isOpened = filesOp.comparePath(path, currentPath);
var $trashElement = createTreeElement(TRASH_NAME, $icon, [TRASH], false, true, false, isOpened);
$trashElement.addClass('root');
$trashElement.find('>.element-row').contextmenu(openTrashTreeContextMenu);
var $trashList = $('<ul>', { id: 'trashTree', 'class': 'category' }).append($trashElement);
$container.append($trashList);
};
var createRecent = function ($container, path) {
var $icon = $recentIcon.clone();
var isOpened = filesOp.comparePath(path, currentPath);
var $element = createTreeElement(RECENT_NAME, $icon, [RECENT], false, false, false, isOpened);
$element.addClass('root');
var $list = $('<ul>', { id: 'recentTree', 'class': 'category' }).append($element);
$container.append($list);
};
var search = APP.Search = {};
var createSearch = function ($container) {
var isInSearch = currentPath[0] === SEARCH;
var $div = $('<div>', {'id': 'searchContainer', 'class': 'unselectable'});
var $input = $('<input>', {
id: 'searchInput',
type: 'text',
draggable: false,
tabindex: 1,
placeholder: Messages.fm_searchPlaceholder
}).keyup(function (e) {
if (search.to) { window.clearTimeout(search.to); }
if ([38, 39, 40, 41].indexOf(e.which) !== -1) {
if (!$input.val()) {
$input.blur();
$content.focus();
return;
} else {
e.stopPropagation();
}
}
var isInSearchTmp = currentPath[0] === SEARCH;
if ($input.val().trim() === "") {
setSearchCursor(0);
if (search.oldLocation && search.oldLocation.length) { displayDirectory(search.oldLocation); }
return;
}
if (e.which === 13) {
if (!isInSearchTmp) { search.oldLocation = currentPath.slice(); }
var newLocation = [SEARCH, $input.val()];
setSearchCursor();
if (!filesOp.comparePath(newLocation, currentPath.slice())) { displayDirectory(newLocation); }
return;
}
if (APP.mobile()) { return; }
search.to = window.setTimeout(function () {
if (!isInSearchTmp) { search.oldLocation = currentPath.slice(); }
var newLocation = [SEARCH, $input.val()];
setSearchCursor();
if (!filesOp.comparePath(newLocation, currentPath.slice())) { displayDirectory(newLocation); }
}, 500);
}).appendTo($div);
$searchIcon.clone().appendTo($div);
if (isInSearch) { $input.val(currentPath[1] || ''); }
$container.append($div);
};
module.resetTree = function () {
var $categories = $tree.find('.categories-container');
var s = $categories.scrollTop() || 0;
$tree.html('');
if (displayedCategories.indexOf(SEARCH) !== -1) { createSearch($tree); }
var $div = $('<div>', {'class': 'categories-container'}).appendTo($tree);
if (displayedCategories.indexOf(RECENT) !== -1) { createRecent($div, [RECENT]); }
if (displayedCategories.indexOf(ROOT) !== -1) { createTree($div, [ROOT]); }
if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($div, [TEMPLATE]); }
if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($div, [FILES_DATA]); }
if (displayedCategories.indexOf(TRASH) !== -1) { createTrash($div, [TRASH]); }
$tree.append(APP.$limit);
$categories = $tree.find('.categories-container');
$categories.scrollTop(s);
};
module.hideMenu = function () {
$contextMenu.hide();
$trashTreeContextMenu.hide();
$trashContextMenu.hide();
$contentContextMenu.hide();
$defaultContextMenu.hide();
$iframe.find('.cp-dropdown-content').hide();
};
var stringifyPath = function (path) {
if (!Array.isArray(path)) { return; }
var $div = $('<div>');
var i = 0;
var space = 10;
path.forEach(function (s) {
if (i === 0) { s = getPrettyName(s); }
$div.append($('<span>', {'style': 'margin: 0 0 0 ' + i * space + 'px;'}).text(s));
$div.append($('<br>'));
i++;
});
return $div.html();
};
var getReadOnlyUrl = APP.getRO = function (id) {
if (!filesOp.isFile(id)) { return; }
var data = filesOp.getFileData(id);
if (!data) { return; }
var parsed = Cryptpad.parsePadUrl(data.href);
if (parsed.hashData.type !== "pad") { return; }
var origin = window.location.origin;
var i = data.href.indexOf('#') + 1;
var base = origin + data.href.slice(0, i);
var hrefsecret = Cryptpad.getSecrets(parsed.type, parsed.hash);
if (!hrefsecret.keys) { return; }
var viewHash = Cryptpad.getViewHashFromKeys(hrefsecret.channel, hrefsecret.keys);
return base + viewHash;
};
// Disable middle click in the context menu to avoid opening /drive/inner.html# in new tabs
$(ifrw).click(function (e) {
if (!e.target || !$(e.target).parents('.cp-dropdown-content').length) { return; }
if (e.which !== 1) {
e.stopPropagation();
return false;
}
});
var getProperties = function (el, cb) {
if (!filesOp.isFile(el)) {
return void cb('NOT_FILE');
}
var ro = filesOp.isReadOnlyFile(el);
var base = window.location.origin;
var $d = $('<div>');
$('<strong>').text(Messages.fc_prop).appendTo($d);
var data = filesOp.getFileData(el);
if (!data || !data.href) { return void cb(void 0, $d); }
$('<br>').appendTo($d);
if (!ro) {
$('<label>', {'for': 'propLink'}).text(Messages.editShare).appendTo($d);
$d.append(Cryptpad.dialog.selectable(base + data.href, {
id: 'propLink',
}));
}
var parsed = Cryptpad.parsePadUrl(data.href);
if (parsed.hashData && parsed.hashData.type === 'pad') {
var roLink = ro ? base + data.href : getReadOnlyUrl(el);
if (roLink) {
$('<label>', {'for': 'propROLink'}).text(Messages.viewShare).appendTo($d);
$d.append(Cryptpad.dialog.selectable(roLink, {
id: 'propROLink',
}));
}
}
if (data.tags && Array.isArray(data.tags)) {
$('<label>', {'for': 'cp-drive-tags'}).text(Messages.fm_prop_tagsList).appendTo($d);
$d.append(Cryptpad.dialog.selectable(data.tags.join(', '), {
id: 'cp-drive-tags',
}));
}
if (APP.loggedIn && AppConfig.enablePinning) {
// check the size of this file...
console.log(data.href);
Cryptpad.getFileSize(data.href, function (e, bytes) {
if (e) {
// there was a problem with the RPC
logError(e);
// but we don't want to break the interface.
// continue as if there was no RPC
return void cb(void 0, $d);
}
var KB = Cryptpad.bytesToKilobytes(bytes);
var formatted = Messages._getKey('formattedKB', [KB]);
$('<br>').appendTo($d);
$('<label>', {
'for': 'size'
}).text(Messages.fc_sizeInKilobytes).appendTo($d);
$d.append(Cryptpad.dialog.selectable(formatted, {
id: 'size',
}));
cb(void 0, $d);
});
} else {
cb(void 0, $d);
}
};
$contextMenu.on("click", "a", function(e) {
e.stopPropagation();
var paths = $(this).data('paths');
//var path = $(this).data('path');
//var $element = $(this).data('element');
if (paths.length === 0) {
log(Messages.fm_forbidden);
debug("Directory context menu on a forbidden or unexisting element. ", paths);
return;
}
if ($(this).hasClass("rename")) {
if (paths.length !== 1) { return; }
displayRenameInput(paths[0].element, paths[0].path);
}
else if($(this).hasClass("delete")) {
var pathsList = [];
paths.forEach(function (p) { pathsList.push(p.path); });
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass('open')) {
paths.forEach(function (p) {
var $element = p.element;
$element.click();
$element.dblclick();
});
}
else if ($(this).hasClass('open_ro')) {
paths.forEach(function (p) {
var el = filesOp.find(p.path);
if (filesOp.isFolder(el)) { return; }
var roUrl = getReadOnlyUrl(el);
openFile(null, roUrl);
});
}
else if ($(this).hasClass('newfolder')) {
if (paths.length !== 1) { return; }
var onCreated = function (err, info) {
if (err) { return void logError(err); }
module.newFolder = info.newPath;
module.displayDirectory(paths[0].path);
};
filesOp.addFolder(paths[0].path, null, onCreated);
}
else if ($(this).hasClass("properties")) {
if (paths.length !== 1) { return; }
var el = filesOp.find(paths[0].path);
getProperties(el, function (e, $prop) {
if (e) { return void logError(e); }
Cryptpad.alert($prop[0], undefined, true);
});
}
module.hideMenu();
});
$defaultContextMenu.on("click", "a", function(e) {
e.stopPropagation();
var paths = $(this).data('paths');
if (paths.length === 0) {
log(Messages.fm_forbidden);
debug("Context menu on a forbidden or unexisting element. ", paths);
return;
}
if ($(this).hasClass('open')) {
paths.forEach(function (p) {
var $element = p.element;
$element.dblclick();
});
}
else if ($(this).hasClass('open_ro')) {
paths.forEach(function (p) {
var el = filesOp.find(p.path);
if (filesOp.isPathIn(p.path, [FILES_DATA])) { el = el.href; }
if (!el || filesOp.isFolder(el)) { return; }
var roUrl = getReadOnlyUrl(el);
openFile(null, roUrl);
});
}
else if ($(this).hasClass('delete')) {
var pathsList = [];
paths.forEach(function (p) { pathsList.push(p.path); });
if (!APP.loggedIn) {
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
if (paths.length === 1) {
msg = Messages.fm_removePermanentlyDialog;
}
Cryptpad.confirm(msg, function(res) {
$(ifrw).focus();
if (!res) { return; }
filesOp.delete(pathsList, refresh);
});
return;
}
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass("properties")) {
if (paths.length !== 1) { return; }
var el = filesOp.find(paths[0].path);
getProperties(el, function (e, $prop) {
if (e) { return void logError(e); }
Cryptpad.alert($prop[0], undefined, true);
});
}
module.hideMenu();
});
$contentContextMenu.on('click', 'a', function (e) {
e.stopPropagation();
var path = $(this).data('path');
var onCreated = function (err, info) {
if (err === E_OVER_LIMIT) {
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
}
if (err) {
return void Cryptpad.alert(Messages.fm_error_cantPin);
}
module.newFolder = info.newPath;
refresh();
};
if ($(this).hasClass("newfolder")) {
filesOp.addFolder(path, null, onCreated);
}
else if ($(this).hasClass("newdoc")) {
var type = $(this).data('type') || 'pad';
sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
window.open('/' + type + '/');
}
module.hideMenu();
});
$trashTreeContextMenu.on('click', 'a', function (e) {
e.stopPropagation();
var paths = $(this).data('paths');
if (paths.length !== 1 || !paths[0].element || !filesOp.comparePath(paths[0].path, [TRASH])) {
log(Messages.fm_forbidden);
debug("Trash tree context menu on a forbidden or unexisting element. ", paths);
return;
}
if ($(this).hasClass("empty")) {
Cryptpad.confirm(Messages.fm_emptyTrashDialog, function(res) {
if (!res) { return; }
filesOp.emptyTrash(refresh);
});
}
module.hideMenu();
});
$trashContextMenu.on('click', 'a', function (e) {
e.stopPropagation();
var paths = $(this).data('paths');
if (paths.length === 0) {
log(Messages.fm_forbidden);
debug("Trash context menu on a forbidden or unexisting element. ", paths);
return;
}
var path = paths[0].path;
var name = paths[0].path[paths[0].path.length - 1];
if ($(this).hasClass("remove")) {
if (paths.length === 1) {
Cryptpad.confirm(Messages.fm_removePermanentlyDialog, function(res) {
if (!res) { return; }
filesOp.delete([path], refresh);
});
return;
}
var pathsList = [];
paths.forEach(function (p) { pathsList.push(p.path); });
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
Cryptpad.confirm(msg, function(res) {
if (!res) { return; }
filesOp.delete(pathsList, refresh);
});
}
else if ($(this).hasClass("restore")) {
if (paths.length !== 1) { return; }
if (path.length === 4) {
var el = filesOp.find(path);
if (filesOp.isFile(el)) {
name = filesOp.getTitle(el);
} else {
name = path[1];
}
}
Cryptpad.confirm(Messages._getKey("fm_restoreDialog", [name]), function(res) {
if (!res) { return; }
filesOp.restore(path, refresh);
});
}
else if ($(this).hasClass("properties")) {
if (paths.length !== 1 || path.length !== 4) { return; }
var element = filesOp.find(path.slice(0,3)); // element containing the oldpath
var sPath = stringifyPath(element.path);
Cryptpad.alert('<strong>' + Messages.fm_originalPath + "</strong>:<br>" + sPath, undefined, true);
}
module.hideMenu();
});
// Chrome considers the double-click means "select all" in the window
$content.on('mousedown', function (e) {
$content.focus();
e.preventDefault();
});
$appContainer.on('mouseup', function (e) {
//if (sel.down) { return; }
if (e.which !== 1) { return ; }
module.hideMenu(e);
//removeSelected(e);
});
$appContainer.on('click', function (e) {
if (e.which !== 1) { return ; }
removeInput();
hideNewButton();
});
$appContainer.on('drag drop', function (e) {
removeInput();
module.hideMenu(e);
});
$appContainer.on('mouseup drop', function () {
$iframe.find('.droppable').removeClass('droppable');
});
$appContainer.on('keydown', function (e) {
// "Del"
if (e.which === 46) {
if (filesOp.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) {
return; // We can't remove elements directly from filesData
}
var $selected = $iframe.find('.selected');
if (!$selected.length) { return; }
var paths = [];
var isTrash = filesOp.isPathIn(currentPath, [TRASH]);
$selected.each(function (idx, elmt) {
if (!$(elmt).data('path')) { return; }
paths.push($(elmt).data('path'));
});
// If we are in the trash or anon pad or if we are holding the "shift" key, delete permanently,
if (!APP.loggedIn || isTrash || e.shiftKey) {
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
if (paths.length === 1) {
msg = Messages.fm_removePermanentlyDialog;
}
Cryptpad.confirm(msg, function(res) {
$(ifrw).focus();
if (!res) { return; }
filesOp.delete(paths, refresh);
});
return;
}
// else move to trash
moveElements(paths, [TRASH], false, refresh);
}
});
$appContainer.contextmenu(function () {
module.hideMenu();
return false;
});
var onRefresh = {
refresh: function() {
if (onRefresh.to) {
window.clearTimeout(onRefresh.to);
}
onRefresh.to = window.setTimeout(refresh, 500);
}
};
proxy.on('change', [], function () {
if (history.isHistoryMode) { return; }
var path = arguments[2];
if (path[0] !== 'drive') { return false; }
path = path.slice(1);
var cPath = currentPath.slice();
if ((filesOp.isPathIn(cPath, ['hrefArray', TRASH]) && cPath[0] === path[0]) ||
(path.length >= cPath.length && filesOp.isSubpath(path, cPath))) {
// Reload after a few ms to make sure all the change events have been received
onRefresh.refresh();
} else if (path.length && path[0] === FILES_DATA) {
onRefresh.refresh();
}
module.resetTree();
return false;
}).on('remove', [], function () {
if (history.isHistoryMode) { return; }
var path = arguments[1];
if (path[0] !== 'drive') { return false; }
path = path.slice(1);
var cPath = currentPath.slice();
if ((filesOp.isPathIn(cPath, ['hrefArray', TRASH]) && cPath[0] === path[0]) ||
(path.length >= cPath.length && filesOp.isSubpath(path, cPath))) {
// Reload after a few to make sure all the change events have been received
onRefresh.to = window.setTimeout(refresh, 500);
}
module.resetTree();
return false;
}).on('change', ['drive', 'migrate'], function () {
var path = arguments[2];
var value = arguments[1];
if (path[1] === "migrate" && value === 1) {
if (APP.onDisconnect) { APP.onDisconnect(true); }
}
});
history.onEnterHistory = function (obj) {
var files = obj.drive;
filesOp = FO.init(files, config);
appStatus.isReady = true;
refresh();
};
history.onLeaveHistory = function () {
var files = proxy.drive;
filesOp = FO.init(files, config);
refresh();
};
var createReadme = function (proxy, cb) {
if (sessionStorage.createReadme) {
var hash = Cryptpad.createRandomHash();
Get.put(hash, Messages.driveReadme, function (e) {
if (e) { logError(e); }
var href = '/pad/#' + hash;
var data = {
href: href,
title: Messages.driveReadmeTitle,
atime: new Date().toISOString(),
ctime: new Date().toISOString()
};
filesOp.pushData(data, function (e, id) {
if (e) { return void console.error("Error while creating the default pad:", e); } // TODO LIMIT?
filesOp.add(id);
if (typeof(cb) === "function") { cb(); }
});
});
delete sessionStorage.createReadme;
return;
}
if (typeof(cb) === "function") { cb(); }
};
var fmConfig = {
noHandlers: true,
onUploaded: function (ev, data) {
try {
// Get the folder path
var newPath = findDropPath(ev.target);
if (!newPath) { return void refresh(); }
var href = data.url;
// Get the current file location in ROOT
var id = filesOp.getIdFromHref(href);
var paths = filesOp.findFile(id);
if (paths.length !== 1) { return; }
// Try to move and refresh
moveElements([paths[0]], newPath, true);
refresh();
} catch (e) {
console.error(e);
refresh();
}
},
body: $iframe.find('body')
};
APP.FM = Cryptpad.createFileManager(fmConfig);
createReadme(proxy, function () {
refresh();
APP.userList.onChange();
Cryptpad.removeLoadingScreen();
});
};
var setHistory = function (bool, update) {
history.isHistoryMode = bool;
setEditable(!bool);
if (!bool && update) {
history.onLeaveHistory();
}
};
var setName = APP.setName = function (newName) {
if (typeof(newName) !== 'string') { return; }
var myUserNameTemp = newName.trim();
if(myUserNameTemp.length > 32) {
myUserNameTemp = myUserNameTemp.substr(0, 32);
}
var myUserName = myUserNameTemp;
Cryptpad.setDisplayName(myUserName, function (err) {
if (err) {
logError("Couldn't set username", err);
return;
}
if (myUserName === "") {
myUserName = Messages.anonymous;
}
APP.$displayName.text(myUserName);
});
};
var migrateAnonDrive = function (proxy, cb) {
if (sessionStorage.migrateAnonDrive) {
Merge.anonDriveIntoUser(proxy, function () {
delete sessionStorage.migrateAnonDrive;
if (typeof(cb) === "function") { cb(); }
});
} else {
if (typeof(cb) === "function") { cb(); }
}
};
// don't initialize until the store is ready.
Cryptpad.ready(function () {
Cryptpad.reportAppUsage();
if (!APP.loggedIn) { Cryptpad.feedback('ANONYMOUS_DRIVE'); }
var $iframe = APP.$iframe = $('#pad-iframe').contents();
APP.$bar = $iframe.find('#toolbar');
var storeObj = Cryptpad.getStore().getProxy && Cryptpad.getStore().getProxy().proxy ? Cryptpad.getStore().getProxy() : undefined;
if (window.location.hash && window.location.hash === "#iframe") {
$iframe.find('body').addClass('iframe');
window.location.hash = "";
APP.homePageIframe = true;
}
var hash = window.location.hash.slice(1) || Cryptpad.getUserHash() || localStorage.FS_hash;
var secret = Cryptpad.getSecrets('drive', hash);
var readOnly = APP.readOnly = secret.keys && !secret.keys.editKeyStr;
var listmapConfig = module.config = {
data: {},
websocketURL: Cryptpad.getWebsocketURL(),
channel: secret.channel,
readOnly: readOnly,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
logging: false
};
var proxy;
if (storeObj && !window.location.hash.slice(1)) { proxy = storeObj.proxy; }
else {
var rt = window.rt = module.rt = Listmap.create(listmapConfig);
proxy = rt.proxy;
}
var onCreate = function (info) {
var realtime = module.realtime = info.realtime;
var editHash = APP.editHash = !readOnly ? Cryptpad.getEditHashFromKeys(info.channel, secret.keys) : undefined;
var viewHash = APP.viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys);
APP.hash = readOnly ? viewHash : editHash;
if (!readOnly && !localStorage.FS_hash && !Cryptpad.getUserHash() && !window.location.hash) {
localStorage.FS_hash = editHash;
}
module.patchText = TextPatcher.create({
realtime: realtime,
logging: true,
});
var userList = APP.userList = info.userList;
var config = {
displayed: ['useradmin', 'limit', 'newpad', 'pageTitle'],
userList: {
list: userList,
userNetfluxId: info.myID
},
common: Cryptpad,
readOnly: readOnly,
ifrw: window,
realtime: info.realtime,
network: info.network,
$container: APP.$bar,
pageTitle: Messages.type.drive
};
var toolbar = APP.toolbar = Toolbar.create(config);
var $rightside = toolbar.$rightside;
$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
var $userBlock = toolbar.$userAdmin;
APP.$displayName = APP.$bar.find('.' + Toolbar.constants.username);
if (APP.homePageIframe) {
var $linkToMain = toolbar.linkToMain;
$linkToMain.attr('href', '#');
$linkToMain.attr('title', '');
$linkToMain.css('cursor', 'default');
$linkToMain.off('click');
}
/* add the usage */
Cryptpad.createUsageBar(function (err, $limitContainer) {
if (err) { return void logError(err); }
APP.$limit = $limitContainer;
}, true);
/* add a history button */
APP.histConfig = {
onLocal: function () {
proxy.drive = history.currentObj.drive;
},
onRemote: function () {},
setHistory: setHistory,
applyVal: function (val) {
var obj = JSON.parse(val || '{}');
history.currentObj = obj;
history.onEnterHistory(obj);
},
$toolbar: APP.$bar,
href: window.location.origin + window.location.pathname + '#' + APP.hash
};
if (!readOnly && !APP.loggedIn) {
// TODO secure drive
// cryptpad-backup --> cp-toolbar-backup
var $backupButton = Cryptpad.createButton('', true).removeClass('fa').removeClass('fa-question').addClass('cryptpad-backup');
$backupButton.append($backupIcon.clone().css('marginRight', '0px'));
$backupButton.attr('title', Messages.fm_backup_title);
$backupButton.on('click', function() {
var url = window.location.origin + window.location.pathname + '#' + editHash;
var msg = Messages.fm_alert_backupUrl + '<input type="text" readonly="readonly" id="fm_backupUrl" value="'+url+'">';
Cryptpad.alert(msg, undefined, true);
$('#fm_backupUrl').val(url);
$('#fm_backupUrl').click(function () {
$(this).select();
});
});
$userBlock.append($backupButton);
}
Cryptpad.onDisplayNameChanged(setName);
};
var onReady = function () {
APP.$iframe.find('body').css('display', '');
module.files = proxy;
if (!proxy.drive || typeof(proxy.drive) !== 'object') { proxy.drive = {}; }
migrateAnonDrive(proxy, function () {
initLocalStorage();
init(proxy);
APP.userList.onChange();
Cryptpad.removeLoadingScreen();
});
};
var onDisconnect = APP.onDisconnect = function (noAlert) {
setEditable(false);
if (APP.refresh) { APP.refresh(); }
APP.toolbar.failed();
if (!noAlert) { Cryptpad.alert(Messages.common_connectionLost, undefined, true); }
};
var onReconnect = function (info) {
setEditable(true);
if (APP.refresh) { APP.refresh(); }
APP.toolbar.reconnecting(info.myId);
Cryptpad.findOKButton().click();
};
if (storeObj && !window.location.hash) {
onCreate(storeObj.info);
onReady();
} else {
proxy.on('create', function (info) {
onCreate(info);
}).on('ready', function () {
onReady();
});
}
proxy.on('disconnect', function () {
onDisconnect();
});
proxy.on('reconnect', function (info) {
onReconnect(info);
});
Cryptpad.onLogout(function () { setEditable(false); });
});
Cryptpad.onError(function (info) {
if (info) {
onConnectError();
}
});
});
});