Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging

pull/1/head
ansuz 7 years ago
commit 044384a576

@ -1,74 +1,10 @@
define(function() {
var config = {};
/* Select the buttons displayed on the main page to create new collaborative sessions
* Existing types : pad, code, poll, slide
*/
config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'whiteboard', 'file', 'todo', 'contacts'];
config.registeredOnlyTypes = ['file', 'contacts'];
/* Cryptpad apps use a common API to display notifications to users
* by default, notifications are hidden after 5 seconds
* You can change their duration here (measured in milliseconds)
*/
config.notificationTimeout = 5000;
config.disableUserlistNotifications = false;
config.hideLoadingScreenTips = false;
config.enablePinning = true;
config.whiteboardPalette = [
'#000000', // black
'#FFFFFF', // white
'#848484', // grey
'#8B4513', // saddlebrown
'#FF0000', // red
'#FF8080', // peach?
'#FF8000', // orange
'#FFFF00', // yellow
'#80FF80', // light green
'#00FF00', // green
'#00FFFF', // cyan
'#008B8B', // dark cyan
'#0000FF', // blue
'#FF00FF', // fuschia
'#FF00C0', // hot pink
'#800080', // purple
];
config.enableTemplates = true;
config.enableHistory = true;
/* user passwords are hashed with scrypt, and salted with their username.
this value will be appended to the username, causing the resulting hash
to differ from other CryptPad instances if customized. This makes it
such that anyone who wants to bruteforce common credentials must do so
again on each CryptPad instance that they wish to attack.
WARNING: this should only be set when your CryptPad instance is first
created. Changing it at a later time will break logins for all existing
users.
*/
config.loginSalt = '';
config.minimumPasswordLength = 8;
config.badStateTimeout = 30000;
config.applicationsIcon = {
file: 'fa-file-text-o',
pad: 'fa-file-word-o',
code: 'fa-file-code-o',
slide: 'fa-file-powerpoint-o',
poll: 'fa-calendar',
whiteboard: 'fa-paint-brush',
todo: 'fa-tasks',
contacts: 'fa-users',
};
config.displayCreationScreen = false;
config.disableAnonymousStore = false;
return config;
/*
* You can override the configurable values from this file.
* The recommended method is to make a copy of this file (/customize.dist/application_config.js)
in a 'customize' directory (/customize/application_config.js).
* If you want to check all the configurable values, you can open the internal configuration file
but you should not change it directly (/common/application_config_internal.js)
*/
define(['/common/application_config_internal.js'], function (AppConfig) {
return AppConfig;
});

@ -4,6 +4,7 @@
<head>
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="description" content="CryptPad is an open-source browser-based suite of collaborative editors. It uses client-side encryption so that the server cannot read users' documents"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>

@ -3,10 +3,12 @@
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "1.25.0",
"dependencies": {
"chainpad-server": "^1.0.1",
"chainpad-server": "^2.0.0",
"express": "~4.10.1",
"nthen": "~0.1.0",
"pull-stream": "^3.6.1",
"saferphore": "0.0.1",
"stream-to-pull-stream": "^1.7.2",
"tweetnacl": "~0.12.2",
"ws": "^1.0.1"
},

@ -1,6 +1,11 @@
/*@flow*/
/* jshint esversion: 6 */
/* global Buffer */
var Fs = require("fs");
var Path = require("path");
var nThen = require("nthen");
const ToPull = require('stream-to-pull-stream');
const Pull = require('pull-stream');
var mkPath = function (env, channelId) {
return Path.join(env.root, channelId.slice(0, 2), channelId) + '.ndjson';
@ -8,7 +13,7 @@ var mkPath = function (env, channelId) {
var getMetadataAtPath = function (Env, path, cb) {
var remainder = '';
var stream = Fs.createReadStream(path, 'utf8');
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
var complete = function (err, data) {
var _cb = cb;
cb = undefined;
@ -25,16 +30,16 @@ var getMetadataAtPath = function (Env, path, cb) {
var parsed = null;
try {
parsed = JSON.parse(metadata);
complete(void 0, parsed);
complete(undefined, parsed);
}
catch (e) {
console.log();
console.log("getMetadataAtPath");
console.error(e);
complete('INVALID_METADATA');
}
});
stream.on('end', function () {
complete(null);
complete();
});
stream.on('error', function (e) { complete(e); });
};
@ -59,7 +64,7 @@ var closeChannel = function (env, channelName, cb) {
var clearChannel = function (env, channelId, cb) {
var path = mkPath(env, channelId);
getMetadataAtPath(env, path, function (e, metadata) {
if (e) { return cb(e); }
if (e) { return cb(new Error(e)); }
if (!metadata) {
return void Fs.truncate(path, 0, function (err) {
if (err) {
@ -87,7 +92,7 @@ var clearChannel = function (env, channelId, cb) {
var readMessages = function (path, msgHandler, cb) {
var remainder = '';
var stream = Fs.createReadStream(path, 'utf8');
var stream = Fs.createReadStream(path, { encoding: 'utf8' });
var complete = function (err) {
var _cb = cb;
cb = undefined;
@ -106,6 +111,60 @@ var readMessages = function (path, msgHandler, cb) {
stream.on('error', function (e) { complete(e); });
};
const NEWLINE_CHR = ('\n').charCodeAt(0);
const mkBufferSplit = () => {
let remainder = null;
return Pull((read) => {
return (abort, cb) => {
read(abort, function (end, data) {
if (end) {
cb(end, remainder ? [remainder, data] : [data]);
remainder = null;
return;
}
const queue = [];
for (;;) {
const offset = data.indexOf(NEWLINE_CHR);
if (offset < 0) {
remainder = remainder ? Buffer.concat([remainder, data]) : data;
break;
}
let subArray = data.slice(0, offset);
if (remainder) {
subArray = Buffer.concat([remainder, subArray]);
remainder = null;
}
queue.push(subArray);
data = data.slice(offset + 1);
}
cb(end, queue);
});
};
}, Pull.flatten());
};
const mkOffsetCounter = () => {
let offset = 0;
return Pull.map((buff) => {
const out = { offset: offset, buff: buff };
// +1 for the eaten newline
offset += buff.length + 1;
return out;
});
};
const readMessagesBin = (env, id, start, msgHandler, cb) => {
const stream = Fs.createReadStream(mkPath(env, id), { start: start });
let keepReading = true;
Pull(
ToPull.read(stream),
mkBufferSplit(),
mkOffsetCounter(),
Pull.asyncMap((data, moreCb) => { msgHandler(data, moreCb, ()=>{ keepReading = false; moreCb(); }); }),
Pull.drain(()=>(keepReading), cb)
);
};
var checkPath = function (path, callback) {
// TODO check if we actually need to use stat at all
Fs.stat(path, function (err) {
@ -117,7 +176,8 @@ var checkPath = function (path, callback) {
callback(err);
return;
}
Fs.mkdir(Path.dirname(path), function (err) {
// 511 -> octal 777
Fs.mkdir(Path.dirname(path), 511, function (err) {
if (err && err.code !== 'EEXIST') {
callback(err);
return;
@ -154,7 +214,28 @@ var flushUnusedChannels = function (env, cb, frame) {
cb();
};
var getChannel = function (env, id, callback) {
var channelBytes = function (env, chanName, cb) {
var path = mkPath(env, chanName);
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(undefined, stats.size);
});
};
/*::
export type ChainPadServer_ChannelInternal_t = {
atime: number,
writeStream: typeof(process.stdout),
whenLoaded: ?Array<(err:?Error, chan:?ChainPadServer_ChannelInternal_t)=>void>,
onError: Array<(?Error)=>void>,
path: string
};
*/
var getChannel = function (
env,
id,
callback /*:(err:?Error, chan:?ChainPadServer_ChannelInternal_t)=>void*/
) {
if (env.channels[id]) {
var chan = env.channels[id];
chan.atime = +new Date();
@ -178,9 +259,9 @@ var getChannel = function (env, id, callback) {
});
}
var path = mkPath(env, id);
var channel = env.channels[id] = {
var channel /*:ChainPadServer_ChannelInternal_t*/ = env.channels[id] = {
atime: +new Date(),
writeStream: undefined,
writeStream: (undefined /*:any*/),
whenLoaded: [ callback ],
onError: [ ],
path: path
@ -193,6 +274,9 @@ var getChannel = function (env, id, callback) {
if (err) {
delete env.channels[id];
}
if (!channel.writeStream) {
throw new Error("getChannel() complete called without channel writeStream");
}
whenLoaded.forEach(function (wl) { wl(err, (err) ? undefined : channel); });
};
var fileExists;
@ -211,7 +295,7 @@ var getChannel = function (env, id, callback) {
var stream = channel.writeStream = Fs.createWriteStream(path, { flags: 'a' });
env.openFiles++;
stream.on('open', waitFor());
stream.on('error', function (err) {
stream.on('error', function (err /*:?Error*/) {
env.openFiles--;
// this might be called after this nThen block closes.
if (channel.whenLoaded) {
@ -228,20 +312,22 @@ var getChannel = function (env, id, callback) {
});
};
var message = function (env, chanName, msg, cb) {
const messageBin = (env, chanName, msgBin, cb) => {
getChannel(env, chanName, function (err, chan) {
if (err) {
if (!chan) {
cb(err);
return;
}
let called = false;
var complete = function (err) {
var _cb = cb;
cb = undefined;
if (_cb) { _cb(err); }
if (called) { return; }
called = true;
cb(err);
};
chan.onError.push(complete);
chan.writeStream.write(msg + '\n', function () {
chan.onError.splice(chan.onError.indexOf(complete) - 1, 1);
chan.writeStream.write(msgBin, function () {
/*::if (!chan) { throw new Error("Flow unreachable"); }*/
chan.onError.splice(chan.onError.indexOf(complete), 1);
if (!cb) { return; }
//chan.messages.push(msg);
chan.atime = +new Date();
@ -250,9 +336,13 @@ var message = function (env, chanName, msg, cb) {
});
};
var message = function (env, chanName, msg, cb) {
messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb);
};
var getMessages = function (env, chanName, handler, cb) {
getChannel(env, chanName, function (err, chan) {
if (err) {
if (!chan) {
cb(err);
return;
}
@ -271,21 +361,43 @@ var getMessages = function (env, chanName, handler, cb) {
errorState = true;
return void cb(err);
}
if (!chan) { throw new Error("impossible, flow checking"); }
chan.atime = +new Date();
cb();
});
});
};
var channelBytes = function (env, chanName, cb) {
var path = mkPath(env, chanName);
Fs.stat(path, function (err, stats) {
if (err) { return void cb(err); }
cb(void 0, stats.size);
});
/*::
export type ChainPadServer_MessageObj_t = { buff: Buffer, offset: number };
export type ChainPadServer_Storage_t = {
readMessagesBin: (
channelName:string,
start:number,
asyncMsgHandler:(msg:ChainPadServer_MessageObj_t, moreCb:()=>void, abortCb:()=>void)=>void,
cb:(err:?Error)=>void
)=>void,
message: (channelName:string, content:string, cb:(err:?Error)=>void)=>void,
messageBin: (channelName:string, content:Buffer, cb:(err:?Error)=>void)=>void,
getMessages: (channelName:string, msgHandler:(msg:string)=>void, cb:(err:?Error)=>void)=>void,
removeChannel: (channelName:string, cb:(err:?Error)=>void)=>void,
closeChannel: (channelName:string, cb:(err:?Error)=>void)=>void,
flushUnusedChannels: (cb:()=>void)=>void,
getChannelSize: (channelName:string, cb:(err:?Error, size:?number)=>void)=>void,
getChannelMetadata: (channelName:string, cb:(err:?Error|string, data:?any)=>void)=>void,
clearChannel: (channelName:string, (err:?Error)=>void)=>void
};
module.exports.create = function (conf, cb) {
export type ChainPadServer_Config_t = {
verbose?: boolean,
filePath?: string,
channelExpirationMs?: number,
openFileLimit?: number
};
*/
module.exports.create = function (
conf /*:ChainPadServer_Config_t*/,
cb /*:(store:ChainPadServer_Storage_t)=>void*/
) {
var env = {
root: conf.filePath || './datastore',
channels: { },
@ -294,15 +406,22 @@ module.exports.create = function (conf, cb) {
openFiles: 0,
openFileLimit: conf.openFileLimit || 2048,
};
Fs.mkdir(env.root, function (err) {
// 0x1ff -> 777
Fs.mkdir(env.root, 0x1ff, function (err) {
if (err && err.code !== 'EEXIST') {
// TODO: somehow return a nice error
throw err;
}
cb({
readMessagesBin: (channelName, start, asyncMsgHandler, cb) => {
readMessagesBin(env, channelName, start, asyncMsgHandler, cb);
},
message: function (channelName, content, cb) {
message(env, channelName, content, cb);
},
messageBin: (channelName, content, cb) => {
messageBin(env, channelName, content, cb);
},
getMessages: function (channelName, msgHandler, cb) {
getMessages(env, channelName, msgHandler, cb);
},

@ -7,12 +7,12 @@ define([
CodeMirror.defineSimpleMode("orgmode", {
start: [
{regex: /^(^\*{1,6}\s)(TODO|DOING|WAITING|NEXT){0,1}(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED){0,1}(.*)$/, token: ["header org-level-star", "header org-todo", "header org-done", "header"]},
{regex: /(^\+[^\/]*\+)/, token: ["strikethrough"]},
{regex: /(^\*[^\/]*\*)/, token: ["strong"]},
{regex: /(^\/[^\/]*\/)/, token: ["em"]},
{regex: /(^\_[^\/]*\_)/, token: ["link"]},
{regex: /(^\~[^\/]*\~)/, token: ["comment"]},
{regex: /(^\=[^\/]*\=)/, token: ["comment"]},
{regex: /(\+[^\+]+\+)/, token: ["strikethrough"]},
{regex: /(\*[^\*]+\*)/, token: ["strong"]},
{regex: /(\/[^\/]+\/)/, token: ["em"]},
{regex: /(\_[^\_]+\_)/, token: ["link"]},
{regex: /(\~[^\~]+\~)/, token: ["comment"]},
{regex: /(\=[^\=]+\=)/, token: ["comment"]},
{regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links
{regex: /\[[xX\s]?\]/, token: 'qualifier'}, // checkbox
{regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments

@ -0,0 +1,115 @@
/*
* This is an internal configuration file.
* If you want to change some configurable values, use the '/customize/application_config.js'
* file (make a copy from /customize.dist/application_config.js)
*/
define(function() {
var config = {};
/* Select the buttons displayed on the main page to create new collaborative sessions
* Existing types : pad, code, poll, slide
*/
config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'whiteboard', 'file', 'todo', 'contacts'];
config.registeredOnlyTypes = ['file', 'contacts'];
/* Cryptpad apps use a common API to display notifications to users
* by default, notifications are hidden after 5 seconds
* You can change their duration here (measured in milliseconds)
*/
config.notificationTimeout = 5000;
config.disableUserlistNotifications = false;
config.hideLoadingScreenTips = false;
config.enablePinning = true;
// Update the default colors available in the whiteboard application
config.whiteboardPalette = [
'#000000', // black
'#FFFFFF', // white
'#848484', // grey
'#8B4513', // saddlebrown
'#FF0000', // red
'#FF8080', // peach?
'#FF8000', // orange
'#FFFF00', // yellow
'#80FF80', // light green
'#00FF00', // green
'#00FFFF', // cyan
'#008B8B', // dark cyan
'#0000FF', // blue
'#FF00FF', // fuschia
'#FF00C0', // hot pink
'#800080', // purple
];
// Set enableTemplates to false to remove the button allowing users to save a pad as a template
// and remove the template category in CryptDrive
config.enableTemplates = true;
// Set enableHistory to false to remove the "History" button in all the apps.
config.enableHistory = true;
/* user passwords are hashed with scrypt, and salted with their username.
this value will be appended to the username, causing the resulting hash
to differ from other CryptPad instances if customized. This makes it
such that anyone who wants to bruteforce common credentials must do so
again on each CryptPad instance that they wish to attack.
WARNING: this should only be set when your CryptPad instance is first
created. Changing it at a later time will break logins for all existing
users.
*/
config.loginSalt = '';
config.minimumPasswordLength = 8;
// Amount of time (ms) before aborting the session when the algorithm cannot synchronize the pad
config.badStateTimeout = 30000;
// Customize the icon used for each application.
// You can update the colors by making a copy of /customize.dist/src/less2/include/colortheme.less
config.applicationsIcon = {
file: 'fa-file-text-o',
pad: 'fa-file-word-o',
code: 'fa-file-code-o',
slide: 'fa-file-powerpoint-o',
poll: 'fa-calendar',
whiteboard: 'fa-paint-brush',
todo: 'fa-tasks',
contacts: 'fa-users',
};
// EXPERIMENTAL: Enabling "displayCreationScreen" may cause UI issues and possible loss of data
config.displayCreationScreen = false;
// Prevent anonymous users from storing pads in their drive
config.disableAnonymousStore = false;
// Hide the usage bar in settings and drive
//config.hideUsageBar = true;
// Disable feedback for all the users and hide the settings part about feedback
//config.disableFeedback = true;
// Add new options in the share modal (extend an existing tab or add a new tab).
// More info about how to use it on the wiki:
// https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions
//config.customizeShareOptions = function (hashes, tabs, config) {};
// Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is
// indicating if the user is registered or anonymous. Here you can change the way anonymous users
// work in CryptPad, use an external SSO or even force registration
// *NOTE*: You have to call the `callback` function to continue the loading process
//config.beforeLogin = function(isLoggedIn, callback) {};
// Add code to be executed on every page after the user object is loaded (also work for
// unregistered users). This allows you to interact with your users' drive
// *NOTE*: You have to call the `callback` function to continue the loading process
//config.afterLogin = function(api, callback) {};
// Disabling the profile app allows you to import the profile informations (display name, avatar)
// from an external source and make sure the users can't change them from CryptPad.
// You can use config.afterLogin to import these values in the users' drive.
//config.disableProfile = true;
return config;
});

@ -1,4 +1,7 @@
define(['/customize/messages.js'], function (Messages) {
define([
'/customize/messages.js',
'/customize/application_config.js'
], function (Messages, AppConfig) {
var Feedback = {};
Feedback.init = function (state) {
@ -19,6 +22,7 @@ define(['/customize/messages.js'], function (Messages) {
http.send();
};
Feedback.send = function (action, force) {
if (AppConfig.disableFeedback) { return; }
if (!action) { return; }
if (force !== true) {
if (!Feedback.state) { return; }

@ -953,7 +953,32 @@ define([
};
if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol
if (!href) { return void displayDefault(); }
var centerImage = function ($img, $image, img) {
var w = img.width;
var h = img.height;
if (w>h) {
$image.css('max-height', '100%');
$img.css('flex-direction', 'column');
if (cb) { cb($img); }
return;
}
$image.css('max-width', '100%');
$img.css('flex-direction', 'row');
if (cb) { cb($img); }
};
var parsed = Hash.parsePadUrl(href);
if (parsed.type !== "file" || parsed.hashData.type !== "file") {
var $img = $('<media-tag>').appendTo($container);
var img = new Image();
$(img).attr('src', href);
img.onload = function () {
centerImage($img, $(img), img);
$(img).appendTo($img);
};
return;
}
var secret = Hash.getSecrets('file', parsed.hash);
if (secret.keys && secret.channel) {
var cryptKey = secret.keys && secret.keys.fileKeyStr;
@ -971,17 +996,7 @@ define([
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
UIElements.displayMediatagImage(Common, $img, function (err, $image, img) {
if (err) { return void console.error(err); }
var w = img.width;
var h = img.height;
if (w>h) {
$image.css('max-height', '100%');
$img.css('flex-direction', 'column');
if (cb) { cb($img); }
return;
}
$image.css('max-width', '100%');
$img.css('flex-direction', 'row');
if (cb) { cb($img); }
centerImage($img, $image, img);
});
});
}
@ -1259,7 +1274,7 @@ define([
$userAdminContent.append($userAccount).append(Util.fixHTML(accountName));
$userAdminContent.append($('<br>'));
}
if (config.displayName) {
if (config.displayName && !AppConfig.disableProfile) {
// Hide "Display name:" in read only mode
$userName.append(Messages.user_displayName + ': ');
$userName.append($displayedName);
@ -1282,14 +1297,14 @@ define([
});
}
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName) {
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls},
content: Messages.user_rename
});
}
if (accountName) {
if (accountName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': 'cp-toolbar-menu-profile'},

@ -727,6 +727,10 @@ define([
};
Nthen(function (waitFor) {
if (AppConfig.beforeLogin) {
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
}
}).nThen(function (waitFor) {
var cfg = {
query: onMessage, // TODO temporary, will be replaced by a webworker channel
userHash: LocalStore.getUserHash(),
@ -763,6 +767,7 @@ define([
}
initFeedback(data.feedback);
initialized = true;
}));
}).nThen(function (waitFor) {
// Load the new pad when the hash has changed
@ -829,6 +834,10 @@ define([
delete sessionStorage.migrateAnonDrive;
}));
}
}).nThen(function (waitFor) {
if (AppConfig.afterLogin) {
AppConfig.afterLogin(common, waitFor());
}
}).nThen(function () {
updateLocalVersion();
f(void 0, env);

@ -341,7 +341,7 @@ define([
// "priv" is not shared with other users but is needed by the apps
priv: {
edPublic: store.proxy.edPublic,
friends: store.proxy.friends,
friends: store.proxy.friends || {},
settings: store.proxy.settings,
thumbnails: !Util.find(store.proxy, ['settings', 'general', 'disableThumbnails'])
}

@ -238,6 +238,7 @@ define([
var $nameValue = $('<span>', {
'class': 'cp-toolbar-userlist-name-value'
}).text(name).appendTo($nameSpan);
if (!Config.disableProfile) {
var $button = $('<button>', {
'class': 'fa fa-pencil cp-toolbar-userlist-name-edit',
title: Messages.user_rename
@ -288,6 +289,7 @@ define([
editingUserName.select[1]);
setTimeout(function () { $nameInput.focus(); });
}
}
} else if (Common.isLoggedIn() && data.curvePublic && !friends[data.curvePublic]
&& !priv.readOnly) {
if (pendingFriends.indexOf(data.netfluxId) !== -1) {

@ -571,7 +571,6 @@ define([
// RENAME
exp.rename = function (path, newName, cb) {
if (sframeChan) {
console.log(path, newName);
return void sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "rename",
data: {

@ -18,50 +18,6 @@
<div id="cp-app-drive-toolbar"></div>
<div id="cp-app-drive-content" tabindex="2"></div>
</div>
<div id="cp-app-drive-context-tree" class="cp-app-drive-context dropdown cp-unselectable">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" data-icon="fa-folder-open" class="cp-app-drive-context-open dropdown-item" data-localization="fc_open">Open</a></li>
<li><a tabindex="-1" data-icon="fa-eye" class="cp-app-drive-context-openro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
<li><a tabindex="-1" data-icon="fa-pencil" class="cp-app-drive-context-rename cp-app-drive-context-editable dropdown-item" data-localization="fc_rename">Rename</a></li>
<li><a tabindex="-1" data-icon="fa-trash" class="cp-app-drive-context-delete cp-app-drive-context-editable dropdown-item" data-localization="fc_delete">Delete</a></li>
<li><a tabindex="-1" data-icon="fa-eraser" class="cp-app-drive-context-deleteowned dropdown-item" data-localization="fc_delete_owned">Delete permanently</a></li>
<li><a tabindex="-1" data-icon="fa-folder" class="cp-app-drive-context-newfolder cp-app-drive-context-editable dropdown-item" data-localization="fc_newfolder">New folder</a></li>
<li><a tabindex="-1" data-icon="fa-database" class="cp-app-drive-context-properties dropdown-item" data-localization="fc_prop">Properties</a></li>
<li><a tabindex="-1" data-icon="fa-hashtag" class="cp-app-drive-context-hashtag dropdown-item" data-localization="fc_hashtag">Tags</a></li>
</ul>
</div>
<div id="cp-app-drive-context-content" class="cp-app-drive-context dropdown cp-unselectable">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" data-icon="fa-folder" class="cp-app-drive-context-newfolder cp-app-drive-context-editable dropdown-item" data-localization="fc_newfolder">New folder</a></li>
<li><a tabindex="-1" data-icon="fa-file-word-o" class="cp-app-drive-context-newdoc cp-app-drive-context-own cp-app-drive-context-editable dropdown-item" data-type="pad" data-localization="button_newpad">New pad</a></li>
<li><a tabindex="-1" data-icon="fa-file-code-o" class="cp-app-drive-context-newdoc cp-app-drive-context-own cp-app-drive-context-editable dropdown-item" data-type="code" data-localization="button_newcode">New code</a></li>
<li><a tabindex="-1" data-icon="fa-file-powerpoint-o" class="cp-app-drive-context-newdoc cp-app-drive-context-own cp-app-drive-context-editable dropdown-item" data-type="slide" data-localization="button_newslide">New slide</a></li>
<li><a tabindex="-1" data-icon="fa-calendar" class="cp-app-drive-context-newdoc cp-app-drive-context-own cp-app-drive-context-editable dropdown-item" data-type="poll" data-localization="button_newpoll">New poll</a></li>
<li><a tabindex="-1" data-icon="fa-paint-brush" class="cp-app-drive-context-newdoc cp-app-drive-context-own cp-app-drive-context-editable dropdown-item" data-type="whiteboard" data-localization="button_newwhiteboard">New whiteboard</a></li>
</ul>
</div>
<div id="cp-app-drive-context-default" class="cp-app-drive-context dropdown cp-unselectable">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" data-icon="fa-folder-open" class="cp-app-drive-context-open dropdown-item" data-localization="fc_open">Open</a></li>
<li><a tabindex="-1" data-icon="fa-eye" class="cp-app-drive-context-openro dropdown-item" data-localization="fc_open_ro">Open (read-only)</a></li>
<li><a tabindex="-1" data-icon="fa-trash" class="cp-app-drive-context-delete dropdown-item" data-localization="fc_delete">Delete</a></li>
<li><a tabindex="-1" data-icon="fa-eraser" class="cp-app-drive-context-deleteowned dropdown-item" data-localization="fc_delete_owned">Delete permanently</a></li>
<li><a tabindex="-1" data-icon="fa-database" class="cp-app-drive-context-properties dropdown-item" data-localization="fc_prop">Properties</a></li>
<li><a tabindex="-1" data-icon="fa-hashtag" class="cp-app-drive-context-hashtag dropdown-item" data-localization="fc_hashtag">Tags</a></li>
</ul>
</div>
<div id="cp-app-drive-context-trashtree" class="cp-app-drive-context dropdown cp-unselectable">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" data-icon="fa-trash-o" class="cp-app-drive-context-empty cp-app-drive-context-editable dropdown-item" data-localization="fc_empty">Empty the trash</a></li>
</ul>
</div>
<div id="cp-app-drive-context-trash" class="cp-app-drive-context dropdown cp-unselectable">
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu" style="display:block;position:static;margin-bottom:5px;">
<li><a tabindex="-1" data-icon="fa-eraser" class="cp-app-drive-context-remove cp-app-drive-context-editable dropdown-item" data-localization="fc_remove">Delete permanently</a></li>
<li><a tabindex="-1" data-icon="fa-repeat" class="cp-app-drive-context-restore cp-app-drive-context-editable dropdown-item" data-localization="fc_restore">Restore</a></li>
<li><a tabindex="-1" data-icon="fa-database" class="cp-app-drive-context-properties dropdown-item" data-localization="fc_prop">Properties</a></li>
</ul>
</div>
</div>
</body>
</html>

@ -11,6 +11,7 @@ define([
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-realtime.js',
'/common/hyperscript.js',
'/common/userObject.js',
'/customize/application_config.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
@ -32,6 +33,7 @@ define([
nThen,
SFCommon,
CommonRealtime,
h,
FO,
AppConfig,
Listmap,
@ -155,19 +157,30 @@ define([
};
// Icons
var faFolder = 'fa-folder';
var faFolderOpen = 'fa-folder-open';
var faReadOnly = 'fa-eye';
var faRename = 'fa-pencil';
var faTrash = 'fa-trash';
var faDelete = 'fa-eraser';
var faProperties = 'fa-database';
var faTags = 'fa-hashtag';
var faEmpty = 'fa-trash-o';
var faRestore = 'fa-repeat';
var faShowParent = 'fa-location-arrow';
var $folderIcon = $('<span>', {
"class": "fa fa-folder cp-app-drive-icon-folder cp-app-drive-content-icon"
"class": faFolder + " fa cp-app-drive-icon-folder cp-app-drive-content-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 cp-app-drive-icon-folder"});
var $folderOpenedIcon = $('<span>', {"class": faFolderOpen + " fa cp-app-drive-icon-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 $trashIcon = $('<span>', {"class": "fa " + faTrash});
var $trashEmptyIcon = $('<span>', {"class": "fa fa-trash-o"});
//var $collapseIcon = $('<span>', {"class": "fa fa-minus-square-o cp-app-drive-icon-expcol"});
var $expandIcon = $('<span>', {"class": "fa fa-plus-square-o cp-app-drive-icon-expcol"});
@ -181,7 +194,7 @@ define([
var $searchIcon = $('<span>', {"class": "fa fa-search cp-app-drive-tree-search-con"});
var $addIcon = $('<span>', {"class": "fa fa-plus"});
var $renamedIcon = $('<span>', {"class": "fa fa-flag"});
var $readonlyIcon = $('<span>', {"class": "fa fa-eye"});
var $readonlyIcon = $('<span>', {"class": "fa " + faReadOnly});
var $ownedIcon = $('<span>', {"class": "fa fa-id-card-o"});
var $ownerIcon = $('<span>', {"class": "fa fa-id-card"});
@ -200,6 +213,91 @@ define([
});
};
var createContextMenu = function () {
var menu = h('div.cp-app-drive-context.dropdown.cp-unselectable', [
h('ul.dropdown-menu', {
'role': 'menu',
'aria-labelledby': 'dropdownMenu',
'style': 'display:block;position:static;margin-bottom:5px;'
}, [
h('li', h('a.cp-app-drive-context-open.dropdown-item', {
'tabindex': '-1',
'data-icon': faFolderOpen,
}, Messages.fc_open)),
h('li', h('a.cp-app-drive-context-openro.dropdown-item', {
'tabindex': '-1',
'data-icon': faReadOnly,
}, Messages.fc_open_ro)),
h('li', h('a.cp-app-drive-context-openparent.dropdown-item', {
'tabindex': '-1',
'data-icon': faShowParent,
}, Messages.fm_openParent)),
h('li', h('a.cp-app-drive-context-newfolder.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faFolder,
}, Messages.fc_newfolder)),
h('li', h('a.cp-app-drive-context-hashtag.dropdown-item', {
'tabindex': '-1',
'data-icon': faTags,
}, Messages.fc_hashtag)),
h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable.cp-app-drive-context-own', {
'tabindex': '-1',
'data-icon': AppConfig.applicationsIcon.pad,
'data-type': 'pad'
}, Messages.button_newpad)),
h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable.cp-app-drive-context-own', {
'tabindex': '-1',
'data-icon': AppConfig.applicationsIcon.code,
'data-type': 'code'
}, Messages.button_newcode)),
h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable.cp-app-drive-context-own', {
'tabindex': '-1',
'data-icon': AppConfig.applicationsIcon.slide,
'data-type': 'slide'
}, Messages.button_newslide)),
h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable.cp-app-drive-context-own', {
'tabindex': '-1',
'data-icon': AppConfig.applicationsIcon.poll,
'data-type': 'poll'
}, Messages.button_newpoll)),
h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable.cp-app-drive-context-own', {
'tabindex': '-1',
'data-icon': AppConfig.applicationsIcon.whiteboard,
'data-type': 'whiteboard'
}, Messages.button_newwhiteboard)),
h('li', h('a.cp-app-drive-context-empty.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faEmpty,
}, Messages.fc_empty)),
h('li', h('a.cp-app-drive-context-restore.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faRestore,
}, Messages.fc_restore)),
h('li', h('a.cp-app-drive-context-rename.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faRename,
}, Messages.fc_rename)),
h('li', h('a.cp-app-drive-context-delete.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faTrash,
}, Messages.fc_delete)),
h('li', h('a.cp-app-drive-context-deleteowned.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faDelete,
}, Messages.fc_delete_owned)),
h('li', h('a.cp-app-drive-context-remove.dropdown-item.cp-app-drive-context-editable', {
'tabindex': '-1',
'data-icon': faDelete,
}, Messages.fc_remove)),
h('li', h('a.cp-app-drive-context-properties.dropdown-item', {
'tabindex': '-1',
'data-icon': faProperties,
}, Messages.fc_prop)),
])
]);
return $(menu);
};
var andThen = function (common, proxy) {
var files = proxy.drive;
var metadataMgr = common.getMetadataMgr();
@ -227,7 +325,8 @@ define([
var $content = APP.$content = $("#cp-app-drive-content");
var $appContainer = $(".cp-app-drive-container");
var $driveToolbar = $("#cp-app-drive-toolbar");
var $contextMenu = $("#cp-app-drive-context-tree");
var $contextMenu = createContextMenu().appendTo($appContainer);
var $contentContextMenu = $("#cp-app-drive-context-content");
var $defaultContextMenu = $("#cp-app-drive-context-default");
var $trashTreeContextMenu = $("#cp-app-drive-context-trashtree");
@ -638,68 +737,112 @@ define([
},0);
};
var filterContextMenu = function ($menu, paths) {
//var path = $element.data('path');
var filterContextMenu = function (type, paths) {
if (!paths || paths.length === 0) { logError('no paths'); }
$contextMenu.find('li').hide();
var show = [];
var filter;
if (type === "content") {
filter = function ($el, className) {
if (className === 'newfolder') { return; }
return AppConfig.availablePadTypes.indexOf($el.attr('data-type')) === -1;
};
} else {
// In case of multiple selection, we must hide the option if at least one element
// is not compatible
var containsFolder = false;
var hide = [];
var hasFolder = false;
paths.forEach(function (p) {
var path = p.path;
var $element = p.element;
if (path.length === 1) {
// Can't rename or delete root elements
hide.push($menu.find('a.cp-app-drive-context-rename'));
hide.push($menu.find('a.cp-app-drive-context-delete'));
}
if (!APP.editable) {
hide.push($menu.find('a.cp-app-drive-context-editable'));
}
if (!isOwnDrive()) {
hide.push($menu.find('a.cp-app-drive-context-own'));
hide.push('delete');
hide.push('rename');
}
if (!$element.is('.cp-app-drive-element-owned')) {
hide.push($menu.find('a.cp-app-drive-context-deleteowned'));
hide.push('deleteowned');
}
if ($element.is('.cp-app-drive-element-notrash')) {
hide.push($menu.find('a.cp-app-drive-context-delete'));
// We can't delete elements in virtual categories
hide.push('delete');
} else {
// We can only open parent in virtual categories
hide.push('openparent');
}
if ($element.is('.cp-app-drive-element-file')) {
// No folder in files
hide.push($menu.find('a.cp-app-drive-context-newfolder'));
hide.push('newfolder');
if ($element.is('.cp-app-drive-element-readonly')) {
// Keep only open readonly
hide.push($menu.find('a.cp-app-drive-context-open'));
hide.push('open'); // Remove open 'edit' mode
} else if ($element.is('.cp-app-drive-element-noreadonly')) {
// Keep only open readonly
hide.push($menu.find('a.cp-app-drive-context-openro'));
hide.push('openro'); // Remove open 'view' mode
}
} else {
if (hasFolder) {
} else { // it's a folder
if (containsFolder) {
// More than 1 folder selected: cannot create a new subfolder
hide.push($menu.find('a.cp-app-drive-context-newfolder'));
hide.push('newfolder');
}
hasFolder = true;
hide.push($menu.find('a.cp-app-drive-context-openro'));
hide.push($menu.find('a.cp-app-drive-context-properties'));
hide.push($menu.find('a.cp-app-drive-context-hashtag'));
containsFolder = true;
hide.push('openro');
hide.push('properties');
hide.push('hashtag');
}
// If we're in the trash, hide restore and properties for non-root elements
if ($menu.find('a.cp-app-drive-context-restore').length && path && path.length > 4) {
hide.push($menu.find('a.cp-app-drive-context-restore'));
hide.push($menu.find('a.cp-app-drive-context-properties'));
if (type === "trash" && path && path.length > 4) {
hide.push('restore');
hide.push('properties');
}
});
if (paths.length > 1) {
hide.push($menu.find('a.cp-app-drive-context-restore'));
hide.push($menu.find('a.cp-app-drive-context-properties'));
hide.push($menu.find('a.cp-app-drive-context-rename'));
hide.push('restore');
hide.push('properties');
hide.push('rename');
hide.push('openparent');
}
if (hasFolder && paths.length > 1) {
if (containsFolder && paths.length > 1) {
// Cannot open multiple folders
hide.push($menu.find('a.cp-app-drive-context-open'));
hide.push('open');
}
return hide;
filter = function ($el, className) {
if (hide.indexOf(className) !== -1) { return true; }
};
}
switch(type) {
case 'content':
show = ['newfolder', 'newdoc'];
break;
case 'tree':
show = ['open', 'openro', 'rename', 'delete', 'deleteowned', 'newfolder',
'properties', 'hashtag'];
break;
case 'default':
show = ['open', 'openro', 'openparent', 'delete', 'deleteowned', 'properties', 'hashtag'];
break;
case 'trashtree': {
show = ['empty'];
break;
}
case 'trash': {
show = ['remove', 'restore', 'properties'];
}
}
var filtered = [];
show.forEach(function (className) {
var $el = $contextMenu.find('.cp-app-drive-context-' + className);
if (!APP.editable && $el.is('.cp-app-drive-context-editable')) { return; }
if (!isOwnDrive && $el.is('.cp-app-drive-context-own')) { return; }
if (filter($el, className)) { return; }
$el.parent('li').show();
filtered.push('.cp-app-drive-context-' + className);
});
return filtered;
};
var getSelectedPaths = function ($element) {
@ -763,16 +906,13 @@ define([
$container.html('');
var $element = $li.length === 1 ? $li : $($li[0]);
var paths = getSelectedPaths($element);
var $menu = $element.data('context');
if (!$menu) { return; }
var menuType = $element.data('context');
if (!menuType) { return; }
//var actions = [];
var $actions = $menu.find('a');
var toHide = filterContextMenu($menu, paths);
var toShow = filterContextMenu(menuType, paths);
var $actions = $contextMenu.find('a');
$actions = $actions.filter(function (i, el) {
for (var j = 0; j < toHide.length; j++) {
if ($(el).is(toHide[j])) { return false; }
}
return true;
return toShow.some(function (className) { return $(el).is(className); });
});
$actions.each(function (i, el) {
var $a = $('<button>', {'class': 'cp-app-drive-element'});
@ -863,7 +1003,8 @@ define([
updateContextButton();
};
var displayMenu = function (e, $menu) {
var displayMenu = function (e) {
var $menu = $contextMenu;
$menu.css({ display: "block" });
if (APP.mobile()) { return; }
var h = $menu.outerHeight();
@ -905,102 +1046,50 @@ define([
};
// Open the selected context menu on the closest "li" element
var openContextMenu = function (e, $menu) {
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 (type === 'trash' && !$element.data('path')) { return; }
if (!$element.length) {
logError("Unable to locate the .element tag", e.target);
$menu.hide();
log(Messages.fm_contextMenuError);
return false;
}
if (!$element.hasClass('cp-app-drive-element-selected')) { //paths.length === 1) {
if (!$element.hasClass('cp-app-drive-element-selected')) {
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;
paths = getSelectedPaths($element);
}
$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;
};
$contextMenu.attr('data-menu-type', type);
var openContentContextMenu = function (e) {
APP.hideMenu();
e.stopPropagation();
var path = $(e.target).closest('#' + FOLDER_CONTENT_ID).data('path');
if (!path) { return; }
var $menu = $contentContextMenu;
removeSelected();
filterContextMenu(type, paths);
if (!APP.editable) {
$menu.find('a.cp-app-drive-context-editable').parent('li').hide();
}
if (!isOwnDrive()) {
$menu.find('a.cp-app-drive-context-own').parent('li').hide();
}
displayMenu(e);
$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) {
if ($contextMenu.find('li:visible').length === 0) {
debug("No visible element in the context menu. Abort.");
$menu.hide();
$contextMenu.hide();
return true;
}
$menu.find('a').data('path', path);
$contextMenu.data('paths', paths);
return false;
};
};
var getElementName = function (path) {
var file = filesOp.find(path);
@ -1303,11 +1392,11 @@ define([
onElementClick(e, $element, newPath);
});
if (!isTrash) {
$element.contextmenu(openDirectoryContextMenu);
$element.data('context', $contextMenu);
$element.contextmenu(openContextMenu('tree'));
$element.data('context', 'tree');
} else {
$element.contextmenu(openTrashContextMenu);
$element.data('context', $trashContextMenu);
$element.contextmenu(openContextMenu('trash'));
$element.data('context', 'trash');
}
var isNewFolder = APP.newFolder && filesOp.comparePath(newPath, APP.newFolder);
if (isNewFolder) {
@ -1911,8 +2000,8 @@ define([
e.stopPropagation();
onElementClick(e, $element, path);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
if (draggable) {
addDragAndDropHandlers($element, path, false, false);
}
@ -1951,8 +2040,8 @@ define([
e.stopPropagation();
onElementClick(e, $element);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
$container.append($element);
});
};
@ -2082,8 +2171,8 @@ define([
e.stopPropagation();
onElementClick(e, $element, path);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
/*if (draggable) {
addDragAndDropHandlers($element, path, false, false);
}*/
@ -2122,8 +2211,8 @@ define([
e.stopPropagation();
onElementClick(e, $element);
});
$element.contextmenu(openDefaultContextMenu);
$element.data('context', $defaultContextMenu);
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
$container.append($element);
});
};
@ -2261,7 +2350,7 @@ define([
} else if (isOwned) {
displayOwned($list);
} else {
$dirContent.contextmenu(openContentContextMenu);
$dirContent.contextmenu(openContextMenu('content'));
if (filesOp.hasSubfolder(root)) { $list.append($folderHeader); }
// display sub directories
var keys = Object.keys(root);
@ -2374,7 +2463,7 @@ define([
}
$rootElement.addClass('cp-app-drive-tree-root');
$rootElement.find('>.cp-app-drive-element-row')
.contextmenu(openDirectoryContextMenu);
.contextmenu(openContextMenu('tree'));
$('<ul>', {'class': 'cp-app-drive-tree-docs'})
.append($rootElement).appendTo($container);
$container = $rootElement;
@ -2396,7 +2485,7 @@ define([
(isCurrentFolder ? $folderOpenedIcon : $folderIcon);
var $element = createTreeElement(key, $icon.clone(), newPath, true, true, subfolder, isCurrentFolder);
$element.appendTo($list);
$element.find('>.cp-app-drive-element-row').contextmenu(openDirectoryContextMenu);
$element.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree'));
createTree($element, newPath);
});
};
@ -2425,7 +2514,8 @@ define([
var isOpened = filesOp.comparePath(path, currentPath);
var $trashElement = createTreeElement(TRASH_NAME, $icon, [TRASH], false, true, false, isOpened);
$trashElement.addClass('root');
$trashElement.find('>.cp-app-drive-element-row').contextmenu(openTrashTreeContextMenu);
$trashElement.find('>.cp-app-drive-element-row')
.contextmenu(openContextMenu('trashtree'));
var $trashList = $('<ul>', { 'class': 'cp-app-drive-tree-category' })
.append($trashElement);
$container.append($trashList);
@ -2588,22 +2678,44 @@ define([
UIElements.getProperties(common, data, cb);
};
if (!APP.loggedIn) {
$contextMenu.find('.cp-app-drive-context-delete').text(Messages.fc_remove)
.attr('data-icon', 'fa-eraser');
}
var deletePaths = function (paths) {
var pathsList = [];
paths.forEach(function (p) { pathsList.push(p.path); });
var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]);
if (paths.length === 1) {
msg = Messages.fm_removePermanentlyDialog;
}
UI.confirm(msg, function(res) {
$(window).focus();
if (!res) { return; }
filesOp.delete(pathsList, refresh);
});
};
$contextMenu.on("click", "a", function(e) {
e.stopPropagation();
var paths = $(this).data('paths');
var paths = $contextMenu.data('paths');
var pathsList = [];
var type = $contextMenu.attr('data-menu-type');
var el;
if (paths.length === 0) {
log(Messages.fm_forbidden);
debug("Directory context menu on a forbidden or unexisting element. ", paths);
debug("Context menu on a forbidden or unexisting element. ", paths);
return;
}
if ($(this).hasClass("cp-app-drive-context-rename")) {
if (paths.length !== 1) { return; }
displayRenameInput(paths[0].element, paths[0].path);
}
else if($(this).hasClass("cp-app-drive-context-delete")) {
var pathsList = [];
if (!APP.loggedIn) {
return void deletePaths(paths);
}
paths.forEach(function (p) { pathsList.push(p.path); });
moveElements(pathsList, [TRASH], false, refresh);
}
@ -2640,108 +2752,37 @@ define([
else if ($(this).hasClass('cp-app-drive-context-openro')) {
paths.forEach(function (p) {
var el = filesOp.find(p.path);
if (filesOp.isFolder(el)) { return; }
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('cp-app-drive-context-newfolder')) {
if (paths.length !== 1) { return; }
var onCreated = function (err, info) {
var onFolderCreated = function (err, info) {
if (err) { return void logError(err); }
APP.newFolder = info.newPath;
APP.displayDirectory(paths[0].path);
};
filesOp.addFolder(paths[0].path, null, onCreated);
}
else if ($(this).hasClass("cp-app-drive-context-properties")) {
if (paths.length !== 1) { return; }
el = filesOp.find(paths[0].path);
getProperties(el, function (e, $prop) {
if (e) { return void logError(e); }
UI.alert($prop[0], undefined, true);
});
}
else if ($(this).hasClass("cp-app-drive-context-hashtag")) {
if (paths.length !== 1) { return; }
el = filesOp.find(paths[0].path);
var data = filesOp.getFileData(el);
if (!data) { return void console.error("Expected to find a file"); }
var href = data.href;
common.updateTags(href);
}
APP.hideMenu();
});
if (!APP.loggedIn) {
$defaultContextMenu.find('.cp-app-drive-context-delete').text(Messages.fc_remove)
.attr('data-icon', 'fa-eraser');
}
$defaultContextMenu.on("click", "a", function(e) {
e.stopPropagation();
var paths = $(this).data('paths');
var el;
if (paths.length === 0) {
log(Messages.fm_forbidden);
debug("Context menu on a forbidden or unexisting element. ", paths);
return;
}
if ($(this).hasClass('cp-app-drive-context-open')) {
paths.forEach(function (p) {
var $element = p.element;
$element.dblclick();
});
filesOp.addFolder(paths[0].path, null, onFolderCreated);
}
else if ($(this).hasClass('cp-app-drive-context-openro')) {
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('cp-app-drive-context-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;
}
UI.confirm(msg, function(res) {
$(window).focus();
if (!res) { return; }
filesOp.delete(pathsList, refresh);
else if ($(this).hasClass("cp-app-drive-context-newdoc")) {
var ntype = $(this).data('type') || 'pad';
var path2 = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
common.sessionStorage.put(Constants.newPadPathKey, path2, function () {
common.openURL('/' + ntype + '/');
});
return;
}
moveElements(pathsList, [TRASH], false, refresh);
}
else if ($(this).hasClass('cp-app-drive-context-deleteowned')) {
var msgD = Messages.fm_deleteOwnedPads;
UI.confirm(msgD, function(res) {
$(window).focus();
if (!res) { return; }
// Try to delete each selected pad from server, and delete from drive if no error
var n = nThen(function () {});
paths.forEach(function (p) {
var el = filesOp.find(p.path);
var data = filesOp.getFileData(el);
var parsed = Hash.parsePadUrl(data.href);
var channel = Util.base64ToHex(parsed.hashData.channel);
n = n.nThen(function (waitFor) {
sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel,
waitFor(function (e) {
if (e) { return void console.error(e); }
filesOp.delete([p.path], refresh);
}));
});
});
});
else if ($(this).hasClass("cp-app-drive-context-properties")) {
if (type === 'trash') {
var pPath = paths[0].path;
if (paths.length !== 1 || pPath.length !== 4) { return; }
var element = filesOp.find(pPath.slice(0,3)); // element containing the oldpath
var sPath = stringifyPath(element.path);
UI.alert('<strong>' + Messages.fm_originalPath + "</strong>:<br>" + sPath, undefined, true);
return;
}
else if ($(this).hasClass("cp-app-drive-context-properties")) {
if (paths.length !== 1) { return; }
el = filesOp.find(paths[0].path);
getProperties(el, function (e, $prop) {
@ -2757,98 +2798,45 @@ define([
var href = data.href;
common.updateTags(href);
}
APP.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 UI.alert(Messages.pinLimitDrive, null, true);
}
if (err) {
return void UI.alert(Messages.fm_error_cantPin);
}
APP.newFolder = info.newPath;
refresh();
};
if ($(this).hasClass("cp-app-drive-context-newfolder")) {
filesOp.addFolder(path, null, onCreated);
}
else if ($(this).hasClass("cp-app-drive-context-newdoc")) {
var type = $(this).data('type') || 'pad';
var path2 = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
common.sessionStorage.put(Constants.newPadPathKey, path2, function () {
common.openURL('/' + type + '/');
});
}
APP.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])) {
else if ($(this).hasClass("cp-app-drive-context-empty")) {
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("cp-app-drive-context-empty")) {
UI.confirm(Messages.fm_emptyTrashDialog, function(res) {
if (!res) { return; }
filesOp.emptyTrash(refresh);
});
}
APP.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("cp-app-drive-context-remove")) {
if (paths.length === 1) {
UI.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]);
UI.confirm(msg, function(res) {
if (!res) { return; }
filesOp.delete(pathsList, refresh);
});
else if ($(this).hasClass("cp-app-drive-context-remove")) {
return void deletePaths(paths);
}
else if ($(this).hasClass("cp-app-drive-context-restore")) {
if (paths.length !== 1) { return; }
if (path.length === 4) {
var el = filesOp.find(path);
if (filesOp.isFile(el)) {
name = filesOp.getTitle(el);
var restorePath = paths[0].path;
var restoreName = paths[0].path[paths[0].path.length - 1];
if (restorePath.length === 4) {
var rEl = filesOp.find(restorePath);
if (filesOp.isFile(rEl)) {
restoreName = filesOp.getTitle(rEl);
} else {
name = path[1];
restoreName = restorePath[1];
}
}
UI.confirm(Messages._getKey("fm_restoreDialog", [name]), function(res) {
UI.confirm(Messages._getKey("fm_restoreDialog", [restoreName]), function(res) {
if (!res) { return; }
filesOp.restore(path, refresh);
filesOp.restore(restorePath, refresh);
});
}
else if ($(this).hasClass("cp-app-drive-context-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);
UI.alert('<strong>' + Messages.fm_originalPath + "</strong>:<br>" + sPath, undefined, true);
else if ($(this).hasClass("cp-app-drive-context-openparent")) {
if (paths.length !== 1) { return; }
var parentPath = paths[0].path.slice();
if (filesOp.isInTrashRoot(parentPath)) { parentPath = [TRASH]; }
else { parentPath.pop(); }
el = filesOp.find(paths[0].path);
APP.selectedFiles = [el];
APP.displayDirectory(parentPath);
}
APP.hideMenu();
});

@ -9,6 +9,7 @@ define([
'/common/common-interface.js',
'/common/common-realtime.js',
'/customize/messages.js',
'/customize/application_config.js',
'/bower_components/marked/marked.min.js',
'cm/lib/codemirror',
@ -33,6 +34,7 @@ define([
UI,
Realtime,
Messages,
AppConfig,
Marked,
CodeMirror
)
@ -478,6 +480,10 @@ define([
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
if (AppConfig.disableProfile) {
common.gotoURL('/drive/');
return;
}
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);

@ -70,6 +70,14 @@ define([
if (!AppConfig.dislayCreationScreen) {
delete categories.creation;
}
if (AppConfig.disableFeedback) {
var feedbackIdx = categories.account.indexOf('cp-settings-userfeedback');
categories.account.splice(feedbackIdx, 1);
}
if (AppConfig.disableProfile) {
var displaynameIdx = categories.account.indexOf('cp-settings-displayname');
categories.account.splice(displaynameIdx, 1);
}
var create = {};
@ -155,8 +163,7 @@ define([
create['logout-everywhere'] = function () {
if (!common.isLoggedIn()) { return; }
var $div = $('<div>', { 'class': 'cp-settings-logout-everywhere cp-sidebarlayout-element'});
$('<label>', { 'for': 'cp-settings-logout-everywhere'})
.text(Messages.settings_logoutEverywhereTitle).appendTo($div);
$('<label>').text(Messages.settings_logoutEverywhereTitle).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages.settings_logoutEverywhere).appendTo($div);
var $button = $('<button>', {

Loading…
Cancel
Save