Merge branch 'form' into form-del

This commit is contained in:
yflory 2022-10-26 17:51:10 +02:00
commit 5d350f1c45
47 changed files with 4933 additions and 451 deletions

View File

@ -8,7 +8,6 @@ www/common/onlyoffice/v2*
www/common/onlyoffice/v4
www/common/onlyoffice/v5
server.js
www/scratch
www/accounts
www/lib

View File

@ -183,6 +183,11 @@
}
}
}
.cp-dropdown-content {
a {
text-decoration: none;
}
}
}
.cp-alertify-type-container {
overflow: visible !important;

View File

@ -120,6 +120,7 @@
border-width: 0 @checkmark-width @checkmark-width 0;
border-width: 0 var(--checkmark-width) var(--checkmark-width) 0;
position: absolute;
box-sizing: border-box;
}
&:focus {
box-shadow: 0px 0px 5px @cp_checkmark-back1;

View File

@ -48,7 +48,7 @@
button {
.fa-caret-down {
margin-right: 1em !important;
margin-right: 0.5em !important;
}
* {
.tools_unselectable();

View File

@ -91,7 +91,7 @@
height: 100%;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
background-color: @cp_buttons-primary;
background-color: @cp_buttons-default-color;
&.danger, &.btn-danger, &.danger-alt, &.btn-danger-alt {
background-color: @cp_buttons-red;
}
@ -327,6 +327,9 @@
fill: @cryptpad_text_col;
}
}
.flatpickr-monthDropdown-month {
background: @cp_flatpickr-bg;
}
}
.flatpickr-current-month {
span.cur-month:hover {

View File

@ -5,6 +5,7 @@
& {
each(@colortheme_apps, {
button .cp-icon-color-@{key},
.cp-icon-color-@{key} { color: @value; }
});

View File

@ -27,14 +27,16 @@
color: @cryptpad_color_red;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
.cp-reminder, .cp-avatar {
cursor: pointer;
&:hover {
background-color: @cp_dropdown-bg-hover;
}
}
.cp-avatar {
.avatar_main(30px);
padding: 0 5px;
}
.cp-notification-content {
flex: 1;
align-items: stretch;

View File

@ -967,6 +967,9 @@
}
}
}
.cp-toolbar-dropdown-nowrap {
white-space: nowrap;
}
.cp-toolbar-bottom {
color: @cp_toolbar-bottom-fg;
display: inline-flex;
@ -998,11 +1001,15 @@
.fa, .cptools {
margin-right: 5px;
}
.cp-dropdown-button-title .cp-icon {
margin-left: 5px;
}
&:hover {
background-color: fade(@cp_toolbar-bottom-bg, 70%);
}
}
.cp-toolbar-bottom-left > button,
.cp-toolbar-bottom-left > span > button,
.cp-toolbar-bottom-mid > button,
.cp-toolbar-bottom-right > button,
.cp-toolbar-bottom-right > span > button {
@ -1070,7 +1077,7 @@
.cp-toolbar-name, .cp-button-name {
display: none;
}
i {
i, span {
margin-right: 0;
}
}

View File

@ -11,6 +11,7 @@ const Core = require("./commands/core");
const Quota = require("./commands/quota");
const Util = require("./common-util");
const Package = require("../package.json");
const Path = require("path");
var canonicalizeOrigin = function (s) {
if (typeof(s) === 'undefined') { return; }
@ -296,7 +297,7 @@ module.exports.create = function (config) {
var paths = Env.paths;
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
return Path.resolve(typeof(config[key]) === 'string'? config[key]: def);
};
Env.incrementBytesWritten = function (n) {

View File

@ -405,6 +405,7 @@ Meta.createLineHandler = function (ref, errorHandler) {
ref.meta = {};
ref.index = 0;
ref.logged = {};
var overwritten = false;
return function (err, line) {
if (err) {
@ -449,6 +450,8 @@ Meta.createLineHandler = function (ref, errorHandler) {
// Thus, accept both the first and second lines you process as valid initial state
// preferring the second if it exists
if (index < 2 && line && typeof(line) === 'object') {
if (overwritten) { return; } // hack to avoid overwriting metadata a second time
overwritten = true;
// special case!
ref.meta = line;
return;

View File

@ -1144,8 +1144,8 @@ module.exports.create = function (conf, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var env = {
root: conf.filePath || './datastore',
archiveRoot: conf.archivePath || './data/archive',
root: Path.resolve(conf.filePath || './datastore'),
archiveRoot: Path.resolve(conf.archivePath || './data/archive'),
// supply a volumeId if you want a store to archive channels to and from
// to its own subpath within the archive directory
volumeId: conf.volumeId || 'datastore',

View File

@ -1,5 +1,5 @@
/*
globals require console
globals process
*/
var Express = require('express');
var Http = require('http');
@ -8,7 +8,6 @@ var Path = require("path");
var nThen = require("nthen");
var Util = require("./lib/common-util");
var Default = require("./lib/defaults");
var Keys = require("./lib/keys");
var config = require("./lib/load-config");
var Env = require("./lib/env").create(config);
@ -116,16 +115,17 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
});
}());
const serveStatic = Express.static(Env.paths.blob, {
setHeaders: function (res) {
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
res.set('Access-Control-Allow-Headers', 'Content-Length');
res.set('Access-Control-Expose-Headers', 'Content-Length');
}
});
app.use('/blob', function (req, res, next) {
if (req.method === 'HEAD') {
Express.static(Path.join(__dirname, Env.paths.blob), {
setHeaders: function (res, path, stat) {
res.set('Access-Control-Allow-Origin', Env.enableEmbedding? '*': Env.permittedEmbedders);
res.set('Access-Control-Allow-Headers', 'Content-Length');
res.set('Access-Control-Expose-Headers', 'Content-Length');
}
})(req, res, next);
return;
return void serveStatic(req, res, next);
}
next();
});
@ -150,32 +150,33 @@ app.use(function (req, res, next) {
// serve custom app content from the customize directory
// useful for testing pages customized with opengraph data
app.use(Express.static(__dirname + '/customize/www'));
app.use(Express.static(__dirname + '/www'));
app.use(Express.static(Path.resolve('customize/www')));
app.use(Express.static(Path.resolve('www')));
// FIXME I think this is a regression caused by a recent PR
// correct this hack without breaking the contributor's intended behaviour.
var mainPages = config.mainPages || Default.mainPages();
var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
app.get(mainPagePattern, Express.static(__dirname + '/customize'));
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
app.get(mainPagePattern, Express.static(Path.resolve('customize')));
app.get(mainPagePattern, Express.static(Path.resolve('customize.dist')));
app.use("/blob", Express.static(Path.join(__dirname, Env.paths.blob), {
app.use("/blob", Express.static(Env.paths.blob, {
maxAge: Env.DEV_MODE? "0d": "365d"
}));
app.use("/datastore", Express.static(Path.join(__dirname, Env.paths.data), {
app.use("/datastore", Express.static(Env.paths.data, {
maxAge: "0d"
}));
app.use("/block", Express.static(Path.join(__dirname, Env.paths.block), {
app.use("/block", Express.static(Env.paths.block, {
maxAge: "0d",
}));
app.use("/customize", Express.static(__dirname + '/customize'));
app.use("/customize", Express.static(__dirname + '/customize.dist'));
app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
app.use(/^\/[^\/]*$/, Express.static('customize'));
app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
app.use("/customize", Express.static(Path.resolve('customize')));
app.use("/customize", Express.static(Path.resolve('customize.dist')));
app.use("/customize.dist", Express.static(Path.resolve('customize.dist')));
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize')));
app.use(/^\/[^\/]*$/, Express.static(Path.resolve('customize.dist')));
// if dev mode: never cache
var cacheString = function () {
@ -216,7 +217,7 @@ var makeRouteCache = function (template, cacheName) {
};
};
var serveConfig = makeRouteCache(function (host) {
var serveConfig = makeRouteCache(function () {
return [
'define(function(){',
'return ' + JSON.stringify({
@ -244,10 +245,10 @@ var serveConfig = makeRouteCache(function (host) {
accounts_api: Env.accounts_api,
}, null, '\t'),
'});'
].join(';\n')
].join(';\n');
}, 'configCache');
var serveBroadcast = makeRouteCache(function (host) {
var serveBroadcast = makeRouteCache(function () {
var maintenance = Env.maintenance;
if (maintenance && maintenance.end && maintenance.end < (+new Date())) {
maintenance = undefined;
@ -260,21 +261,21 @@ var serveBroadcast = makeRouteCache(function (host) {
maintenance: maintenance
}, null, '\t'),
'});'
].join(';\n')
].join(';\n');
}, 'broadcastCache');
app.get('/api/config', serveConfig);
app.get('/api/broadcast', serveBroadcast);
var define = function (obj) {
var defineBlock = function (obj) {
return `define(function (){
return ${JSON.stringify(obj, null, '\t')};
});`
});`;
};
app.get('/api/instance', function (req, res) { // XXX use caching?
res.setHeader('Content-Type', 'text/javascript');
res.send(define({
res.send(defineBlock({
name: Env.instanceName,
description: Env.instanceDescription,
location: Env.instanceJurisdiction,
@ -282,10 +283,10 @@ app.get('/api/instance', function (req, res) { // XXX use caching?
}));
});
var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
var fivehundred_path = Path.resolve(__dirname + '/customize.dist/500.html');
var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');
var custom_fivehundred_path = Path.resolve(__dirname + '/customize/500.html');
var four04_path = Path.resolve('customize.dist/404.html');
var fivehundred_path = Path.resolve('customize.dist/500.html');
var custom_four04_path = Path.resolve('customize/404.html');
var custom_fivehundred_path = Path.resolve('/customize/500.html');
var send404 = function (res, path) {
if (!path && path !== four04_path) { path = four04_path; }
@ -321,7 +322,7 @@ app.get('/api/updatequota', function (req, res) {
});
});
app.get('/api/profiling', function (req, res, next) {
app.get('/api/profiling', function (req, res) {
if (!Env.enableProfiling) { return void send404(res); }
res.setHeader('Content-Type', 'text/javascript');
res.send(JSON.stringify({
@ -329,13 +330,13 @@ app.get('/api/profiling', function (req, res, next) {
}));
});
app.use(function (req, res, next) {
app.use(function (req, res) {
res.status(404);
send404(res, custom_four04_path);
});
// default message for thrown errors in ExpressJS routes
app.use(function (err, req, res, next) {
app.use(function (err, req, res) {
Env.Log.error('EXPRESSJS_ROUTING', {
error: err.stack || err,
});
@ -346,7 +347,7 @@ app.use(function (err, req, res, next) {
var httpServer = Env.httpServer = Http.createServer(app);
nThen(function (w) {
Fs.exists(__dirname + "/customize", w(function (e) {
Fs.exists(Path.resolve("customize"), w(function (e) {
if (e) { return; }
console.log("CryptPad is customizable, see customize.dist/readme.md for details");
}));
@ -377,7 +378,7 @@ nThen(function (w) {
Http.createServer(app).listen(Env.httpSafePort, Env.httpAddress, w());
}
}).nThen(function () {
var wsConfig = { server: httpServer };
//var wsConfig = { server: httpServer };
// Initialize logging then start the API server
require("./lib/log").create(config, function (_log) {

View File

@ -17,6 +17,9 @@
.cp-small { display: none; }
}
.flatpickr-calendar {
z-index: 100001 !important; // Alertify is 100000
}
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
padding: 0;
& > div {
@ -101,6 +104,21 @@
color: @cryptpad_text_col !important;
}
}
.tui-full-calendar-floating-layer.cp-calendar-popup-flex {
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
justify-content: center !important;
align-items: center !important;
.tui-full-calendar-popup {
width: 540px !important;
}
}
#tui-full-calendar-popup-arrow {
display: none !important;
}
}
.tui-full-calendar-timegrid-timezone {
background-color: @cp_sidebar-right-bg !important;
@ -116,13 +134,30 @@
border-color: @cp_calendar-border !important;
}
.tui-full-calendar-popup {
border-radius: @variables_radius_L;
}
.tui-full-calendar-popup-container {
background: @cp_flatpickr-bg;
color: @cryptpad_text_col;
border-radius: @variables_radius;
border-radius: @variables_radius_L;
font-weight: normal;
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
display: none;
}
.tui-full-calendar-popup-detail-item {
a {
color: @cryptpad_color_link;
text-decoration: underline;
}
}
.tui-full-calendar-section-button-save {
height: 40px;
.btn-primary { // Update button
margin-right: 0px;
}
}
}
li.tui-full-calendar-popup-section-item {
padding: 0 6px;
@ -186,6 +221,9 @@
width: 100%;
height: 32px;
border-radius: @variables_radius;
input[type="checkbox"].tui-full-calendar-checkbox-square:checked + span {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABbmlDQ1BpY2MAACiRdZHNK0RRGMZ/Zoh8NIqFNItZDFmMEiVLxsJmkgZlsLn3zpe6c93uvZNkq2wspizExtfCf8BW2VJKkZKs/AG+Npqu97hqJM7t3PfXc87zds5zIJQyjZJbPwAly3PSE8nYXGY+1vhEMy20EyaqGa49NjWV4t/xfkOdqtf9qtf/+/4cLdmca0Bdk/CwYTue8KhwasWzFW8KdxpFLSu8L5xw5IDCF0rXA35UXAj4VbEzkx6HkOoZK/xg/QcbRack3CccL5ll4/s86iatOWt2Wmq3zCguaSZIEkOnzBImHv1SLcnsb9/Al2+SZfEY8rdZxRFHgaJ4E6KWpWtOal70nHwmqyr333m6+aHBoHtrEhoefP+lBxq3oFrx/Y8D368eQvgezqyaf1lyGnkTvVLT4nsQWYeT85qmb8PpBnTd2ZqjfUlhmaF8Hp6PoS0DHVfQvBBk9b3O0S3MrMkTXcLOLvTK/sjiJ6CLZ94KREMsAAAACXBIWXMAAAsSAAALEgHS3X78AAABHUlEQVQoFWNkaP//n4EMwESGHrAW6mtkZvz3G2g03BsBagz/nRUQ7sNp49//TJ+Byv6BlMbrMjCsC2Jg3BbKwOAgB9GMolGAg4GBESIOIrmA+A9I03xvFHGwCrhGkDOe5TAw9LvAFbEn6jEwwTT9BtodvJ7h/4FHYH0MLBCKgaHTgYGBE8jLN2FgYAJae+4FA8NcLwZWkAtAmoLWMTBsuYNwECMsHrVEGBj2RzEwiIEciATANgE1bb6DJAgMNLhTr71hYHBcxsDw+htCAQ5NIAU/4RpBPKjmf2++MfwHaQpZD7cJyEMB3+BORRL+rybE8FOEk4Hj2FO4KMgdrFAMitun2DTCVSMxYAkBFFYg9j94qCIpwsZEil5wyDIDAAXIUsnSKmq7AAAAAElFTkSuQmCC);
}
.tui-full-calendar-ic-checkbox {
margin-left: 5px;
border-radius: 2px;
@ -196,6 +234,7 @@
.tui-full-calendar-popup-detail {
font: @colortheme_app-font;
color: @cryptpad_text_col;
box-shadow: @cryptpad_ui_shadow;
.tui-full-calendar-popup-container {
padding-bottom: 17px;
}
@ -203,28 +242,44 @@
font-size: 14px;
}
.tui-full-calendar-section-button {
margin-top: 10px;
border: 0;
display: flex;
align-items: center;
align-items: start;
button {
flex: 1;
margin: 0;
}
}
.tui-full-calendar-popup-top-line {
border-radius: 10px 10px 0px 0px;
height: 10px;
}
.tui-full-calendar-popup-vertical-line {
visibility: hidden;
width: 10px;
}
}
.cp-recurrence-label, .cp-notif-label {
color: @cryptpad_text_col;
margin-right: 1rem;
i {
margin-right: 0.5rem;
}
}
.cp-calendar-recurrence-container {
margin-top: 1rem;
.cp-calendar-rec-translated-str {
margin-top: 0.5rem;
}
}
.cp-calendar-add-notif {
flex-flow: column;
align-items: baseline !important;
margin: 10px 0;
.cp-notif-label {
color: @cp_sidebar-hint;
margin-right: 20px;
}
margin: 1rem 0;
* {
font-size: @colortheme_app-font-size;
font-weight: normal;
@ -234,34 +289,58 @@
}
.cp-calendar-notif-list-container {
margin-bottom: 10px;
.cp-notif-label {
margin-top: 0.5em;
}
}
.cp-calendar-notif-list {
display: flex;
flex-flow: column;
.cp-notif-entry {
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem;
.cp-notif-value {
width: 170px;
display: inline-flex;
line-height: 30px;
vertical-align: middle;
.cp-before {
flex: 1;
min-width: 0;
}
}
span:not(:last-child) {
margin-right: 5px;
margin: 0px 5px;
}
.btn-danger-outline {
margin-right: 0px !important;
background-color: transparent;
color: @cryptpad_text_col;
border-color: @cryptpad_text_col;
&:hover {
color: @cp_buttons-red-color;
background-color: @cp_buttons-red;
border-color: @cp_buttons-red;
}
}
}
}
.cp-notif-empty {
display: none;
margin-bottom: 2px;
border-radius: @variables_radius;
background-color: fade(@cryptpad_text_col, 10%);
padding: 0.25rem 0.5rem;
line-height: 30px;
}
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
display: block;
}
.cp-calendar-notif-form {
align-items: center;
margin-bottom: 20px;
// margin-bottom: 20px;
input {
width: 80px;
margin-right: 5px;
@ -270,15 +349,120 @@
}
.cp-calendar-close {
top: 17px;
right: 17px;
height: auto;
margin-right: 0px;
line-height: initial;
border: 1px solid;
&:not(:hover) {
background: transparent;
}
}
}
.cp-calendar-rec-inline, .cp-calendar-rec-block {
&:not(:last-child) {
margin-bottom: 10px;
}
}
.cp-calendar-rec-inline {
display: flex;
flex-flow: row;
align-items: center;
& > *:not(:first-child) { margin-left: 5px; }
.cp-dropdown-container {
position: unset;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
.cp-checkmark {
margin-right: 0.5rem;
}
}
.cp-calendar-rec-block {
.cp-calendar-rec-block-title {
margin-bottom: 0.5rem !important;
}
.cp-radio {
margin-bottom: 0.5rem;
}
input[type="radio"]:not(:checked) ~ .cp-checkmark-label {
input {
filter: grayscale(1);
}
}
.cp-checkmark-label {
& > *:not(:first-child) { margin-left: 5px; }
width: 100%;
//height: 26px;
display: flex;
align-items: center;
& > input {
margin-bottom: 0 !important;
}
input {
display: inline;
height: 24px !important;
padding: 0 5px !important;
}
input[type="text"] {
width: 200px !important;
}
input[type="number"] {
width: 80px !important;
margin-bottom: 0 !important;
}
}
}
#cp-calendar-rec-monthly-pick ~ .cp-checkmark-label {
display: flex;
align-items: center;
& > span {
margin-right: 20px;
}
}
button.cp-calendar-pick-el {
display: flex;
align-items: center;
justify-content: center;
&:not(:last-child) {
margin-right: 5px;
}
}
div.cp-calendar-weekly-pick {
button {
width: 50px;
}
}
div.cp-calendar-monthly-pick {
display: flex;
flex-flow: column;
& > div {
display: flex;
&:not(:last-child) {
margin-bottom: 5px;
}
button {
height: 25px;
width: 25px;
&.lastday {
width: 115px;
}
}
}
}
.tui-full-calendar-ic-repeat-b {
display: none;
& ~ * {
display: none;
}
}
#cp-toolbar .cp-calendar-browse {
display: flex;
align-items: center;
@ -395,6 +579,7 @@
align-items: center;
justify-content: center;
border-radius: @variables_radius;
flex-shrink: 0;
}
&.cp-active {
background-color: @cp_sidebar-left-item-bg;

View File

@ -2,7 +2,9 @@
// Calendars will be exported using this format instead of plain text.
define([
'/customize/pages.js',
], function (Pages) {
'/common/common-util.js',
'/calendar/recurrence.js'
], function (Pages, Util, Rec) {
var module = {};
var getICSDate = function (str) {
@ -57,60 +59,197 @@ define([
var data = content[uid];
// DTSTAMP: now...
// UID: uid
var start, end;
if (data.isAllDay && data.startDay && data.endDay) {
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
var getDT = function (data) {
var start, end;
if (data.isAllDay) {
var startDate = new Date(data.start);
var endDate = new Date(data.end);
data.startDay = data.startDay || (startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate());
data.endDay = data.endDay || (endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate());
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
} else {
start = "DTSTART:"+getICSDate(data.start);
end = "DTEND:"+getICSDate(data.end);
}
return {
start: start,
end: end
};
};
Array.prototype.push.apply(ICS, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
]);
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
var getRRule = function (data) {
if (!data.recurrenceRule || !data.recurrenceRule.freq) { return; }
var r = data.recurrenceRule;
var rrule = "RRULE:";
rrule += "FREQ="+r.freq.toUpperCase();
Object.keys(r).forEach(function (k) {
if (k === "freq") { return; }
if (k === "by") {
Object.keys(r.by).forEach(function (_k) {
rrule += ";BY"+_k.toUpperCase()+"="+r.by[_k];
});
return;
}
rrule += ";"+k.toUpperCase()+"="+r[k];
});
return rrule;
};
var addEvent = function (arr, data, recId) {
var uid = data.id;
var dt = getDT(data);
var start = dt.start;
var end = dt.end;
var rrule = getRRule(data);
Array.prototype.push.apply(arr, [
'BEGIN:VEVENT',
'DTSTAMP:'+getICSDate(+new Date()),
'UID:'+uid,
start,
end,
recId,
rrule,
'SUMMARY:'+ data.title,
'LOCATION:'+ data.location,
].filter(Boolean));
if (Array.isArray(data.reminders)) {
data.reminders.forEach(function (valueMin) {
var time = valueMin * 60;
var days = Math.floor(time / DAY);
time -= days * DAY;
var hours = Math.floor(time / HOUR);
time -= hours * HOUR;
var minutes = Math.floor(time / MINUTE);
time -= minutes * MINUTE;
var seconds = time;
var str = "-P" + days + "D";
if (hours || minutes || seconds) {
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
}
Array.prototype.push.apply(arr, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(arr, data.cp_hidden);
}
arr.push('END:VEVENT');
};
var applyChanges = function (base, changes) {
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(base[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
base[k] = +d;
};
Object.keys(changes || {}).forEach(function (k) {
if (k === "start" || k === "end") {
return applyDiff(changes, k);
}
base[k] = changes[k];
});
};
var prev = data;
// Check if we have "one-time" or "from date" updates.
// "One-time" updates will be added accordingly to the ICS specs
// "From date" updates will be added as new events and will add
// an "until" value to the initial event's RRULE
var toAdd = [];
if (data.recurrenceRule && data.recurrenceRule.freq && data.recUpdate) {
var ru = data.recUpdate;
var _all = {};
var duration = data.end - data.start;
var all = Rec.getAllOccurrences(data); // "false" if infinite
Object.keys(ru.from || {}).forEach(function (d) {
if (!Object.keys(ru.from[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].from = ru.from[d];
});
Object.keys(ru.one || {}).forEach(function (d) {
if (!Object.keys(ru.one[d] || {}).length) { return; }
_all[d] = _all[d] || {};
_all[d].one = ru.one[d];
});
Object.keys(_all).sort(function (a, b) {
return Number(a) - Number(b);
}).forEach(function (d) {
d = Number(d);
var r = prev.recurrenceRule;
// This rule won't apply if we've reached "until" or "count"
var idx = all && all.indexOf(d);
if (all && idx === -1) {
// Make sure we don't have both count and until
if (all.length === r.count) { delete r.until; }
else { delete r.count; }
return;
}
var ud = _all[d];
if (ud.from) { // "From" updates are not supported by ICS: make a new event
var _new = Util.clone(prev);
r.until = getICSDate(d - 1); // Stop previous recursion
delete r.count;
addEvent(ICS, prev, null); // Add previous event
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
toAdd = [];
prev = _new;
if (all) { all = all.slice(idx); }
// if we updated the recurrence rule, count is reset, nothing to do
// if we didn't update the recurrence, we need to fix the count
var _r = _new.recurrenceRule;
if (all && !ud.from.recurrenceRule && _r && _r.count) {
_r.count -= idx;
}
prev.start = d;
prev.end = d + duration;
prev.id = Util.uid();
applyChanges(prev, ud.from);
duration = prev.end - prev.start;
}
if (ud.one) { // Add update
var _one = Util.clone(prev);
_one.start = d;
_one.end = d + duration;
applyChanges(_one, ud.one);
var recId = "RECURRENCE-ID:"+getICSDate(+d);
delete _one.recurrenceRule;
addEvent(toAdd, _one, recId); // Add updated event
}
Array.prototype.push.apply(ICS, [
'BEGIN:VALARM',
'ACTION:DISPLAY',
'DESCRIPTION:This is an event reminder',
'TRIGGER:'+str,
'END:VALARM'
]);
});
}
if (Array.isArray(data.cp_hidden)) {
Array.prototype.push.apply(ICS, data.cp_hidden);
}
ICS.push('END:VEVENT');
addEvent(ICS, prev);
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
});
ICS.push('END:VCALENDAR');
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
return new Blob([ ICS.join('\r\n') ], { type: 'text/calendar;charset=utf-8' });
};
module.import = function (content, id, cb) {
@ -171,7 +310,7 @@ define([
}
// Store other properties
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp', 'rrule', 'recurrence-id'];
var hidden = [];
ev.getAllProperties().forEach(function (p) {
if (used.indexOf(p.name) !== -1) { return; }
@ -192,8 +331,25 @@ define([
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
});
// Get recurrence rule
var rrule = ev.getFirstPropertyValue('rrule');
var rec;
if (rrule && rrule.freq) {
rec = {};
rec.freq = rrule.freq.toLowerCase();
if (rrule.interval) { rec.interval = rrule.interval; }
if (rrule.count) { rec.count = rrule.count; }
if (Object.keys(rrule).includes('wkst')) { rec.wkst = (rrule.wkst + 6) % 7; }
if (rrule.until) { rec.until = +new Date(rrule.until); }
Object.keys(rrule.parts || {}).forEach(function (k) {
rec.by = rec.by || {};
var _k = k.toLowerCase().slice(2); // "BYDAY" ==> "day"
rec.by[_k] = rrule.parts[k];
});
}
// Create event
res[uid] = {
var obj = {
calendarId: id,
id: uid,
category: 'time',
@ -203,15 +359,48 @@ define([
start: start,
end: end,
reminders: reminders,
cp_hidden: hidden
cp_hidden: hidden,
};
if (rec) { obj.recurrenceRule = rec; }
if (!hidden.length) { delete res[uid].cp_hidden; }
if (!reminders.length) { delete res[uid].reminders; }
if (!hidden.length) { delete obj.cp_hidden; }
if (!reminders.length) { delete obj.reminders; }
var recId = ev.getFirstPropertyValue('recurrence-id');
if (recId) {
setTimeout(function () {
if (!res[uid]) { return; }
var old = res[uid];
var time = +new Date(recId);
var diff = {};
var from = {};
Object.keys(obj).forEach(function (k) {
if (JSON.stringify(old[k]) === JSON.stringify(obj[k])) { return; }
if (['start','end'].includes(k)) {
diff[k] = Rec.diffDate(old[k], obj[k]);
return;
}
if (k === "recurrenceRule") {
from[k] = obj[k];
return;
}
diff[k] = obj[k];
});
old.recUpdate = old.recUpdate || {one:{},from:{}};
if (Object.keys(from).length) { old.recUpdate.from[time] = from; }
if (Object.keys(diff).length) { old.recUpdate.one[time] = diff; }
});
return;
}
res[uid] = obj;
});
cb(null, res);
// setTimeout to make sure we call back after the "recurrence-id" setTimeout
// are called
setTimeout(function () {
cb(null, res);
});
});
};

File diff suppressed because it is too large Load Diff

View File

@ -6,13 +6,21 @@ define([
'/common/sframe-common-outer.js',
], function (nThen, ApiConfig, DomReady, SFCommonO) {
// Loaded in load #2
var hash, href;
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
var obj = SFCommonO.initIframe(waitFor, true);
href = obj.href;
hash = obj.hash;
}).nThen(function (/*waitFor*/) {
var addData = function (meta) {
var addData = function (meta, Cryptpad, user, Utils) {
if (hash) {
var parsed = Utils.Hash.parsePadUrl(href);
if (parsed.hashData && parsed.hashData.newPadOpts) {
meta.calendarOpts = Utils.Hash.decodeDataOptions(parsed.hashData.newPadOpts);
}
}
meta.calendarHash = Boolean(window.location.hash);
};
SFCommonO.start({

869
www/calendar/recurrence.js Normal file
View File

@ -0,0 +1,869 @@
define([
'/common/common-util.js',
], function (Util) {
var Rec = {};
var debug = function () {};
// Get week number with any "WKST" (firts day of the week)
// Week 1 is the first week of the year containing at least 4 days in this year
// It depends on which day is considered the first day of the week (default Monday)
// In our case, wkst is a number matching the JS rule: 0 == Sunday
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var newYear = new Date(date.getFullYear(),0,1);
var day = newYear.getDay() - wkst; //the day of week the year begins on
day = (day >= 0 ? day : day + 7);
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
var weeknum;
// Week 1 / week 53
if (day < 4) {
weeknum = Math.floor((daynum+day-1)/7) + 1;
if (weeknum > 52) {
var nYear = new Date(date.getFullYear() + 1,0,1);
var nday = nYear.getDay() - wkst;
nday = nday >= 0 ? nday : nday + 7;
weeknum = nday < 4 ? 1 : 53;
}
}
else {
weeknum = Math.floor((daynum+day-1)/7);
}
return weeknum;
};
var getYearDay = function (date) {
var start = new Date(date.getFullYear(), 0, 0);
var diff = (date - start) +
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
var oneDay = 1000 * 60 * 60 * 24;
return Math.floor(diff / oneDay);
};
var setYearDay = function (date, day) {
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
if (day < 0) {
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
day = max + day + 1;
}
date.setMonth(0);
date.setDate(day);
return true;
};
var getEndData = function (s, e) {
if (s > e) { return void console.error("Wrong data"); }
var days;
if (e.getFullYear() === s.getFullYear()) {
days = getYearDay(e) - getYearDay(s);
} else { // eYear < sYear
var tmp = new Date(s.getFullYear(), 11, 31);
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
var de = getYearDay(e);
days = d1 + de;
while ((tmp.getFullYear()+1) < e.getFullYear()) {
tmp.setFullYear(tmp.getFullYear()+1);
days += getYearDay(tmp);
}
}
return {
h: e.getHours(),
m: e.getMinutes(),
days: days
};
};
var setEndData = function (s, e, data) {
e.setTime(+s);
if (!data) { return; }
e.setHours(data.h);
e.setMinutes(data.m);
e.setSeconds(0);
e.setDate(s.getDate() + data.days);
};
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
var getDayData = function (str) {
var pos = Number(str.slice(0,-2));
var day = DAYORDER.indexOf(str.slice(-2));
return pos ? [pos, day] : day;
};
var goToFirstWeekDay = function (date, wkst) {
var d = date.getDay();
wkst = typeof(wkst) === "number" ? wkst : 1;
if (d >= wkst) {
date.setDate(date.getDate() - (d-wkst));
} else {
date.setDate(date.getDate() - (7+d-wkst));
}
};
var getDateStr = function (date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
};
var FREQ = {};
FREQ['daily'] = function (s, i) {
s.setDate(s.getDate()+i);
};
FREQ['weekly'] = function (s,i) {
s.setDate(s.getDate()+(i*7));
};
FREQ['monthly'] = function (s,i) {
s.setMonth(s.getMonth()+i);
};
FREQ['yearly'] = function (s,i) {
s.setFullYear(s.getFullYear()+i);
};
// EXPAND is used to create iterations added from a BYxxx rule
// dateA is the start date and b is the number or id of the BYxxx rule item
var EXPAND = {};
EXPAND['month'] = function (dateS, origin, b) {
var oS = new Date(origin.start);
var a = dateS.getMonth() + 1;
var toAdd = (b-a+12)%12;
var m = dateS.getMonth() + toAdd;
dateS.setMonth(m);
dateS.setDate(oS.getDate());
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
return true;
};
EXPAND['weekno'] = function (dateS, origin, week, rule) {
var wkst = rule && rule.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var oS = new Date(origin.start);
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
var doubleOne = lastW === 1;
if (lastW === 1) { lastW = 52; }
var a = getWeekNo(dateS, wkst);
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
var toAdd = week - a;
var weekS = new Date(+dateS);
// Go to the selected week
weekS.setDate(weekS.getDate() + (toAdd * 7));
goToFirstWeekDay(weekS, wkst);
// Then make sure we are in the correct start day
var all = 'aaaaaaa'.split('').map(function (o, i) {
var date = new Date(+weekS);
date.setDate(date.getDate() + i);
if (date.getFullYear() !== dateS.getFullYear()) { return; }
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
}).filter(Boolean);
// If we're looking for week 1 and the last week is a week 1, add the days
if (week === 1 && doubleOne) {
goToFirstWeekDay(lastD, wkst);
'aaaaaaa'.split('').some(function (o, i) {
var date = new Date(+lastD);
date.setDate(date.getDate() + i);
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
if (date.getFullYear() > dateS.getFullYear()) { return true; }
all.push(date);
});
}
return all.length ? all : undefined;
};
EXPAND['yearday'] = function (dateS, origin, b) {
var y = dateS.getFullYear();
var state = setYearDay(dateS, b);
if (!state) { return; } // Invalid day "b"
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
return true;
};
EXPAND['monthday'] = function (dateS, origin, b, rule) {
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
var setMonthDay = function (date, day) {
var m = date.getMonth();
if (day < 0) {
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
day = tmp.getDate() + day + 1;
}
date.setDate(day);
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
};
// Monthly events
if (rule.freq === 'monthly') {
return setMonthDay(dateS, b);
}
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
var date = new Date(dateS.getFullYear(), i, 1);
var ok = setMonthDay(date, b);
return ok ? date : undefined;
}).filter(Boolean);
return all.length ? all : undefined;
};
EXPAND['day'] = function (dateS, origin, b, rule) {
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
var day = getDayData(b);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
var all = [];
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
var filterPos = function (m) {
if (!pos) { return; }
var _all = [];
'aaaaaaaaaaaa'.split('').some(function (a, i) {
if (typeof(m) !== "undefined" && i !== m) { return; }
var _pos;
var tmp = all.filter(function (d) {
return d.getMonth() === i;
});
if (pos < 0) {
_pos = tmp.length + pos;
} else {
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
}
_all.push(tmp[_pos]);
return typeof(m) !== "undefined" && i === m;
});
all = _all.filter(Boolean); // The "5th" {day} won't always exist
};
var tmp;
if (rule.freq === 'yearly') {
tmp = new Date(+dateS);
var y = dateS.getFullYear();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getFullYear() === y) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos();
return all;
}
if (rule.freq === 'monthly') {
tmp = new Date(+dateS);
var m = dateS.getMonth();
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
while (tmp.getMonth() === m) {
all.push(new Date(+tmp));
tmp.setDate(tmp.getDate()+7);
}
filterPos(m);
return all;
}
if (rule.freq === 'weekly') {
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
}
return true;
};
var LIMIT = {};
LIMIT['month'] = function (events, rule) {
return events.filter(function (s) {
return rule.includes(s.getMonth()+1);
});
};
LIMIT['weekno'] = function (events, weeks, rules) {
return events.filter(function (s) {
var wkst = rules && rules.wkst;
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
if (lastW === 1) { lastW = 52; }
var w = getWeekNo(s, wkst);
return weeks.some(function (week) {
if (week > 0) { return week === w; }
return w === (lastW + week + 1);
});
});
};
LIMIT['yearday'] = function (events, days) {
return events.filter(function (s) {
var d = getYearDay(s);
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
return days.some(function (day) {
if (day > 0) { return day === d; }
return d === (max + day + 1);
});
});
};
LIMIT['monthday'] = function (events, rule) {
return events.filter(function (s) {
var r = Util.clone(rule);
// Transform the negative monthdays into positive for this specific month
r = r.map(function (b) {
if (b < 0) {
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
b = tmp.getDate() + b + 1;
}
return b;
});
return r.includes(s.getDate());
});
};
LIMIT['day'] = function (events, days, rules) {
return events.filter(function (s) {
var dayStr = s.toLocaleDateString();
// Check how to handle position in BYDAY rules (last day of the month or the year?)
var type = 'yearly';
if (rules.freq === 'monthly' ||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
type = 'monthly';
}
// Check if this event matches one of the allowed days
return days.some(function (r) {
// rule elements are strings with pos and day
var day = getDayData(r);
var pos;
if (Array.isArray(day)) {
pos = day[0];
day = day[1];
}
if (!pos) {
return s.getDay() === day;
}
// If we have a position, we can use EXPAND.day to get the nth {day} of the
// year/month and compare if it matches with
var d = new Date(s.getFullYear(), s.getMonth(), 1);
if (type === 'yearly') { d.setMonth(0); }
var res = EXPAND["day"](d, {}, r, {freq: type});
return res.some(function (date) {
return date.toLocaleDateString() === dayStr;
});
});
});
};
LIMIT['setpos'] = function (events, rule) {
var init = events.slice();
var rules = Util.deduplicateString(rule.slice().map(function (n) {
if (n > 0) { return (n-1); }
if (n === 0) { return; }
return init.length + n;
}));
return events.filter(function (ev) {
var idx = init.indexOf(ev);
return rules.includes(idx);
});
};
var BYORDER = ['month','weekno','yearday','monthday','day'];
var BYDAYORDER = ['month','monthday','day'];
Rec.getMonthId = function (d) {
return d.getFullYear() + '-' + d.getMonth();
};
var cache = window.CP_calendar_cache = {};
var recurringAcross = {};
Rec.resetCache = function () {
cache = window.CP_calendar_cache = {};
recurringAcross = {};
};
var iterate = function (rule, _origin, s) {
// "origin" is the original event to detect the start of BYxxx
var origin = Util.clone(_origin);
var oS = new Date(origin.start);
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
// "uid" is used for the cache
var uid = s.toLocaleDateString();
cache[id] = cache[id] || {};
var inter = rule.interval || 1;
var freq = rule.freq;
var all = [];
var limit = function (byrule, n) {
all = LIMIT[byrule](all, n, rule);
};
var expand = function (byrule) {
return function (n) {
// Set the start date at the beginning of the current FREQ
var _s = new Date(+s);
if (rule.freq === 'yearly') {
// January 1st
_s.setMonth(0);
_s.setDate(1);
} else if (rule.freq === 'monthly') {
_s.setDate(1);
} else if (rule.freq === 'weekly') {
goToFirstWeekDay(_s, rule.wkst);
} else if (rule.freq === 'daily') {
// We don't have < byday rules so we can't expand daily rules
}
var add = EXPAND[byrule](_s, origin, n, rule);
if (!add) { return; }
if (Array.isArray(add)) {
add = add.filter(function (dateS) {
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
});
Array.prototype.push.apply(all, add);
} else {
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
all.push(_s);
}
};
};
// Manage interval for the next iteration
var it = Util.once(function () {
FREQ[freq](s, inter);
});
var addDefault = function () {
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
var _s = new Date(+s);
if (freq === "monthly" || freq === "yearly") {
_s.setDate(oS.getDate());
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
// will still only be displayed on months with 31 days
}
all.push(_s);
};
if (Array.isArray(cache[id][uid])) {
debug('Get cache', id, uid);
if (freq === "monthly") {
s.setDate(15);
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
s.setDate(28);
}
it();
return cache[id][uid];
}
if (rule.by && freq === 'yearly') {
var order = BYORDER.slice();
var monthLimit = false;
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
order.shift();
monthLimit = true;
}
var first = true;
order.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
if (first) {
r.forEach(expand(_order));
first = false;
} else if (_order === "day") {
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
limit('day', rule.by.day);
} else {
rule.by.day.forEach(expand('day'));
}
} else {
limit(_order, r);
}
});
if (rule.by.month && monthLimit) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'monthly') {
// We're going to compute all the entries for the coming month
if (!rule.by.monthday && !rule.by.day) {
addDefault();
} else if (rule.by.monthday) {
rule.by.monthday.forEach(expand('monthday'));
} else if (rule.by.day) {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
if (rule.by.day && rule.by.monthday) {
limit('day', rule.by.day);
}
}
if (rule.by && freq === 'weekly') {
// We're going to compute all the entries for the coming week
if (!rule.by.day) {
addDefault();
} else {
rule.by.day.forEach(expand('day'));
}
if (rule.by.month) {
limit('month', rule.by.month);
}
}
if (rule.by && freq === 'daily') {
addDefault();
BYDAYORDER.forEach(function (_order) {
var r = rule.by[_order];
if (!r) { return; }
limit(_order, r);
});
}
all.sort(function (a, b) {
return a-b;
});
if (rule.by && rule.by.setpos) {
limit('setpos', rule.by.setpos);
}
if (!rule.by || !Object.keys(rule.by).length) {
addDefault();
} else {
it();
}
var done = [];
all = all.filter(function (newS) {
var start = new Date(+newS).toLocaleDateString();
if (done.includes(start)) { return false; }
done.push(start);
return true;
});
debug('Set cache', id, uid);
cache[id][uid] = all;
return all;
};
var getNextRules = function (obj) {
if (!obj.recUpdate) { return []; }
var _allRules = {};
var _obj = obj.recUpdate.from;
Object.keys(_obj || {}).forEach(function (d) {
var u = _obj[d];
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
});
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
.map(function (k) {
var r = Util.clone(_allRules[k]);
if (!FREQ[r.freq]) { return; }
if (r.interval && r.interval < 1) { return; }
r._start = Number(k);
return r;
}).filter(Boolean);
};
Rec.getRecurring = function (months, events) {
if (window.CP_DEV_MODE) { debug = console.warn; }
var toAdd = [];
months.forEach(function (monthId) {
// from 1st day of the month at 00:00 to last day at 23:59:59:999
var ms = monthId.split('-');
var _startMonth = new Date(ms[0], ms[1]);
var _endMonth = new Date(+_startMonth);
_endMonth.setMonth(_endMonth.getMonth() + 1);
_endMonth.setMilliseconds(-1);
debug('Compute month', _startMonth.toLocaleDateString());
var rec = events || [];
rec.forEach(function (obj) {
var _start = new Date(obj.start);
var _end = new Date(obj.end);
var _origin = obj;
var rule = obj.recurrenceRule;
if (!rule) { return; }
var nextRules = getNextRules(obj);
var nextRule = nextRules.shift();
if (_start >= _endMonth) { return; }
// Check the "until" date of the latest rule we can use and stop now
// if the recurrence ends before the current month
var until = rule.until;
var _nextRules = nextRules.slice();
var _nextRule = nextRule;
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
until = nextRule.until;
_nextRule = _nextRules.shift();
}
if (until < _startMonth) { return; }
var endData = getEndData(_start, _end);
if (rule.interval && rule.interval < 1) { return; }
if (!FREQ[rule.freq]) { return; }
/*
// Rule examples
rule.by = {
//month: [1, 4, 5, 8, 12],
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
//yearday: [1, 2, 29, 30, -2, -1, 250],
//monthday: [1, 2, 3, -3, -2, -1],
//day: ["MO", "WE", "FR"],
//setpos: [1, 2, -1, -2]
};
rule.wkst = 0;
rule.interval = 2;
rule.freq = 'yearly';
rule.count = 10;
*/
debug('Iterate over', obj.title, obj);
debug('Use rule', rule);
var count = rule.count;
var c = 1;
var next = function (start) {
var evS = new Date(+start);
if (count && c >= count) { return; }
debug('Start iteration', evS.toLocaleDateString());
var _toAdd = iterate(rule, obj, evS);
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
// Make sure to continue if the current year doesn't provide any result
if (!_toAdd.length) {
if (evS.getFullYear() < _startMonth.getFullYear() ||
evS < _endMonth) {
return void next(evS);
}
return;
}
var stop = false;
var newrule = false;
_toAdd.some(function (_newS) {
// Make event with correct start and end time
var _ev = Util.clone(obj);
_ev.id = _origin.id + '|' + (+_newS);
var _evS = new Date(+_newS);
var _evE = new Date(+_newS);
setEndData(_evS, _evE, endData);
_ev.start = +_evS;
_ev.end = +_evE;
_ev._count = c;
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
if (nextRule && _ev.start === nextRule._start) {
newrule = true;
}
var useNewRule = function () {
if (!newrule) { return; }
debug('Use new rule', nextRule);
_ev._count = c;
count = nextRule.count;
c = 1;
evS = +_evS;
obj = _ev;
rule = nextRule;
nextRule = nextRules.shift();
};
if (c >= count) { // Limit reached
debug(_evS.toLocaleDateString(), 'count');
stop = true;
return true;
}
if (_evS >= _endMonth) { // Won't affect us anymore
debug(_evS.toLocaleDateString(), 'endMonth');
stop = true;
return true;
}
if (rule.until && _evS > rule.until) {
debug(_evS.toLocaleDateString(), 'until');
stop = true;
return true;
}
if (_evS < _start) { // "Expand" rules may create events before the _start
debug(_evS.toLocaleDateString(), 'start');
return;
}
c++;
if (_evE < _startMonth) { // Ended before the current month
// Nothing to display but continue the recurrence
debug(_evS.toLocaleDateString(), 'startMonth');
if (newrule) { useNewRule(); }
return;
}
// If a recurring event start and end in different months, make sure
// it is only added once
if ((_evS < _endMonth && _evE >= _endMonth) ||
(_evS < _startMonth && _evE >= _startMonth)) {
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
return;
} else {
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
recurringAcross[_ev.id].push(_ev.start);
}
}
// Add this event
toAdd.push(_ev);
if (newrule) {
useNewRule();
return true;
}
});
if (!stop) { next(evS); }
};
next(_start);
debug('Added this month (all events)', toAdd.map(function (ev) {
return new Date(ev.start).toLocaleDateString();
}));
});
});
return toAdd;
};
Rec.getAllOccurrences = function (ev) {
if (!ev.recurrenceRule) { return [ev.start]; }
var r = ev.recurrenceRule;
// In case of infinite recursion, we can't get all
if (!r.until && !r.count) { return false; }
var all = [ev.start];
var d = new Date(ev.start);
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
var toAdd = [];
var i = 0;
var check = function () {
return r.count ? (all.length < r.count) : (+d <= r.until);
};
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
d.setMonth(d.getMonth() + 1);
i++;
}
return all;
};
Rec.diffDate = function (oldTime, newTime) {
var n = new Date(newTime);
var o = new Date(oldTime);
// Diff Days
var d = 0;
var mult = n < o ? -1 : 1;
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
n.setDate(n.getDate() - mult);
d++;
}
d = mult * d;
// Diff hours
n = new Date(newTime);
var h = n.getHours() - o.getHours();
// Diff minutes
var m = n.getMinutes() - o.getMinutes();
return {
d: d,
h: h,
m: m
};
};
var sortUpdate = function (obj) {
return Object.keys(obj).sort(function (d1, d2) {
return Number(d1) - Number(d2);
});
};
Rec.applyUpdates = function (events) {
events.forEach(function (ev) {
ev.raw = {
start: ev.start,
end: ev.end,
};
if (!ev.recUpdate) { return; }
var from = ev.recUpdate.from || {};
var one = ev.recUpdate.one || {};
var s = ev.start;
// Add "until" date to our recurrenceRule if it has been modified in future occurences
var nextRules = getNextRules(ev).filter(function (r) {
return r._start > s;
});
var nextRule = nextRules.shift();
var applyDiff = function (obj, k) {
var diff = obj[k]; // Diff is always compared to origin start/end
var d = new Date(ev.raw[k]);
d.setDate(d.getDate() + diff.d);
d.setHours(d.getHours() + diff.h);
d.setMinutes(d.getMinutes() + diff.m);
ev[k] = +d;
};
sortUpdate(from).forEach(function (d) {
if (s < Number(d)) { return; }
Object.keys(from[d]).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
if (k === "recurrenceRule" && !from[d][k]) { return; }
ev[k] = from[d][k];
});
});
Object.keys(one[s] || {}).forEach(function (k) {
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
if (k === "recurrenceRule" && !one[s][k]) { return; }
ev[k] = one[s][k];
});
if (ev.deleted) {
Object.keys(ev).forEach(function (k) {
delete ev[k];
});
}
if (nextRule && ev.recurrenceRule) {
ev.recurrenceRule._next = nextRule._start - 1;
}
if (ev.reminders) {
ev.raw.reminders = ev.reminders;
}
});
return events;
};
return Rec;
});

View File

@ -1566,7 +1566,7 @@ define([
console.error(err);
}
return h(`div.errorcp-test-status.${obj.type}`, [
return h(`div.error.cp-test-status.${obj.type}`, [
h('h5', obj.message),
h('div.table-container',
h('table', [

View File

@ -68,6 +68,7 @@ define([
'markdown',
'gfm',
'html',
'asciidoc',
'htmlembedded',
'htmlmixed',
'index.html',
@ -143,6 +144,31 @@ define([
previews['htmlmixed'] = function (val, $div, common) {
DiffMd.apply(val, $div, common);
};
previews['asciidoc'] = function (val, $div, common) {
require([
'asciidoctor',
'/lib/highlight/highlight.pack.js',
'css!/lib/highlight/styles/' + (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css')
], function (asciidoctor) {
var reg = asciidoctor.Extensions.create();
var Highlight = window.hljs;
reg.inlineMacro('media-tag', function () {
var t = this;
t.process(function (parent, target) {
var d = target.split('|');
return t.createInline(parent, 'quoted', `<media-tag src="${d[0]}" data-crypto-key="${d[1]}"></media-tag>`).convert();
});
});
var html = asciidoctor.convert(val, { attributes: 'showtitle', extension_registry: reg });
DiffMd.apply(html, $div, common);
$div.find('pre code').each(function (i, el) {
Highlight.highlightBlock(el);
});
});
};
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
var $previewContainer = $('#cp-app-code-preview');
@ -370,9 +396,17 @@ define([
evModeChange.reg(function (mode) {
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
// Embedding is enabled
framework.setMediaTagEmbedder(function (mt) {
framework.setMediaTagEmbedder(function (mt, d) {
editor.focus();
editor.replaceSelection($(mt)[0].outerHTML);
var txt = $(mt)[0].outerHTML;
if (editor.getMode().name === "asciidoc") {
if (d.static) {
txt = d.href + `[${d.name}]`;
} else {
txt = `media-tag:${d.src}|${d.key}[]`;
}
}
editor.replaceSelection(txt);
});
} else {
// Embedding is disabled

View File

@ -1417,7 +1417,7 @@ define([
return /HTML/.test(Object.prototype.toString.call(o)) &&
typeof(o.tagName) === 'string';
};
var allowedTags = ['a', 'p', 'hr', 'div'];
var allowedTags = ['a', 'li', 'p', 'hr', 'div'];
var isValidOption = function (o) {
if (typeof o !== "object") { return false; }
if (isElement(o)) { return true; }
@ -1541,6 +1541,7 @@ define([
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
if (config.isSelect && value) {
// We use JSON.stringify here to escape quotes
if (typeof(value) === "object") { value = JSON.stringify(value); }
var $val = $innerblock.find('[data-value='+JSON.stringify(value)+']');
setActive($val);
try {

View File

@ -36,6 +36,7 @@
};
Util.clone = function (o) {
if (o === undefined || o === null) { return o; }
return JSON.parse(JSON.stringify(o));
};

View File

@ -1223,8 +1223,8 @@ define([
pad.onMetadataEvent = Util.mkEvent();
pad.onChannelDeleted = Util.mkEvent();
pad.requestAccess = function (data, cb) {
postMessage("REQUEST_PAD_ACCESS", data, cb);
pad.contactOwner = function (data, cb) {
postMessage("CONTACT_PAD_OWNER", data, cb);
};
pad.giveAccess = function (data, cb) {
postMessage("GIVE_PAD_ACCESS", data, cb);

View File

@ -78,6 +78,7 @@ define([
var TAGS_NAME = Messages.fm_tagsName;
var SHARED_FOLDER = 'sf';
var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
var FILTER = "filter";
// Icons
var faFolder = 'cptools-folder';
@ -1149,8 +1150,12 @@ define([
common.getMediaTagPreview(mts, idx);
};
var FILTER_BY = "filterBy";
var refresh = APP.refresh = function () {
APP.displayDirectory(currentPath);
var type = APP.store[FILTER_BY];
var path = type ? [FILTER, type, currentPath] : currentPath;
APP.displayDirectory(path);
};
// `app`: true (force open wiht the app), false (force open in preview),
@ -2946,66 +2951,140 @@ define([
openIn(type, path, APP.team);
});
};
var getNewPadOptions = function (isInRoot) {
var options = [];
if (isInRoot) {
options.push({
class: 'cp-app-drive-new-folder',
icon: $folderIcon.clone()[0],
name: Messages.fm_folder,
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
class: 'cp-app-drive-new-shared-folder',
icon: $sharedFolderIcon.clone()[0],
name: Messages.fm_sharedFolder,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-fileupload',
icon: getIcon('fileupload')[0],
name: Messages.uploadButton,
});
if (APP.allowFolderUpload) {
options.push({
class: 'cp-app-drive-new-folderupload',
icon: getIcon('folderupload')[0],
name: Messages.uploadFolderButton,
});
}
options.push({ separator: true });
options.push({
class: 'cp-app-drive-new-link',
icon: getIcon('link')[0],
name: Messages.fm_link_new,
});
options.push({ separator: true });
}
getNewPadTypes().forEach(function (type) {
var typeClass = 'cp-app-drive-new-doc';
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
typeClass += ' cp-app-hidden cp-app-disabled';
} else if (premium === 0) {
typeClass += ' cp-app-disabled';
}
options.push({
class: typeClass,
type: type,
icon: getIcon(type)[0],
name: Messages.type[type],
});
});
if (APP.store[FILTER_BY]) {
var typeFilter = APP.store[FILTER_BY];
options = options.filter((obj) => {
if (obj.separator) { return false; }
if (typeFilter === 'link') {
return obj.class.includes('cp-app-drive-new-link');
}
if (typeFilter === 'file') {
return obj.class.includes('cp-app-drive-new-fileupload');
}
if (getNewPadTypes().indexOf(typeFilter) !== -1) {
return typeFilter === obj.type;
}
});
}
return options;
};
var createNewButton = function (isInRoot, $container) {
if (!APP.editable) { return; }
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
if (!manager.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
// Create dropdown
var options = getNewPadOptions(isInRoot).map(function (obj) {
if (obj.separator) { return { tag: 'hr' }; }
var newObj = {
tag: 'a',
attributes: { 'class': obj.class },
content: [ obj.icon, obj.name ]
};
if (obj.type) {
newObj.attributes['data-type'] = obj.type;
newObj.attributes['href'] = '#';
}
return newObj;
});
var dropdownConfig = {
buttonContent: [
h('i.fa.fa-plus'),
h('span.cp-button-name', Messages.fm_newButton),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
common: common
};
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
addNewPadHandlers($block, isInRoot);
$container.append($block);
};
var createFilterButton = function (isTemplate, $container) {
if (!APP.loggedIn) { return; }
// Create dropdown
var options = [];
if (isInRoot) {
if (APP.store[FILTER_BY]) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folder pewpew'},
attributes: {
'class': 'cp-app-drive-rm-filter',
},
content: [
$folderIcon.clone()[0],
Messages.fm_folder,
],
});
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-shared-folder'},
content: [
$sharedFolderIcon.clone()[0],
Messages.fm_sharedFolder,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-fileupload'},
content: [
getIcon('fileupload')[0],
Messages.uploadButton,
],
});
if (APP.allowFolderUpload) {
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-folderupload'},
content: [
getIcon('folderupload')[0],
Messages.uploadFolderButton,
],
});
}
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {'class': 'cp-app-drive-new-link'},
content: [
getIcon('link')[0],
Messages.fm_link_new,
h('i.fa.fa-times'),
Messages.fm_rmFilter,
],
});
options.push({tag: 'hr'});
}
getNewPadTypes().forEach(function (type) {
var attributes = {
'class': 'cp-app-drive-new-doc',
'class': 'cp-app-drive-filter-doc',
'data-type': type,
'href': '#'
};
@ -3026,21 +3105,72 @@ define([
],
});
});
if (!isTemplate) {
options.push({tag: 'hr'});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'link'
},
content: [
getIcon('link')[0],
Messages.fm_link_type,
],
});
options.push({
tag: 'a',
attributes: {
'class': 'cp-app-drive-filter-doc',
'data-type': 'file',
'href': '#'
},
content: [
getIcon('file')[0],
Messages.type['file'],
],
});
}
var dropdownConfig = {
buttonContent: [
h('span.fa.fa-plus'),
h('span', Messages.fm_newButton),
h('i.fa.fa-filter'),
h('span.cp-button-name', Messages.fm_filterBy),
],
buttonCls: 'cp-toolbar-dropdown-nowrap',
options: options,
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
feedback: 'DRIVE_FILTERBY',
common: common
};
if (APP.store[FILTER_BY]) {
var type = APP.store[FILTER_BY];
var message = type === 'link' ? Messages.fm_link_type : Messages.type[type];
dropdownConfig.buttonContent.push(
h('span.cp-button-name', ':'),
getIcon(type)[0],
h('span.cp-button-name', message)
);
}
var $block = UIElements.createDropdown(dropdownConfig);
// Custom style:
$block.find('button').addClass('cp-app-drive-toolbar-new');
// Add style
if (APP.store[FILTER_BY]) {
$block.find('button').addClass('cp-toolbar-button-active');
}
addNewPadHandlers($block, isInRoot);
// Add handlers
if (APP.store[FILTER_BY]) {
$block.find('a.cp-app-drive-rm-filter')
.click(function () {
APP.store[FILTER_BY] = undefined;
APP.displayDirectory(currentPath);
});
}
$block.find('a.cp-app-drive-filter-doc')
.click(function () {
var type = $(this).attr('data-type') || 'invalid-filter';
APP.store[FILTER_BY] = type;
APP.displayDirectory([FILTER, type, currentPath]);
});
$container.append($block);
};
@ -3302,65 +3432,38 @@ define([
return keys;
};
var filterPads = function (files, type, path, useId) {
var root = path && manager.find(path);
return files
.filter(function (e) {
return useId ? manager.isFile(e) : (path && manager.isFile(root[e]));
})
.filter(function (e) {
var id = useId ? e : root[e];
var data = manager.getFileData(id);
if (type === 'link') { return data.static; }
var href = data.href || data.roHref;
return href ? (href.split('/')[1] === type) : true;
// if types are unreachable, display files to avoid misleading the user
});
};
// Create the ghost icon to add pads/folders
var createNewPadIcons = function ($block, isInRoot) {
var $container = $('<div>');
if (isInRoot) {
// Folder
var $element1 = $('<li>', {
'class': 'cp-app-drive-new-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($folderIcon.clone()).appendTo($container);
$element1.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_folder));
// Shared Folder
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
var $element3 = $('<li>', {
'class': 'cp-app-drive-new-shared-folder cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend($sharedFolderIcon.clone()).appendTo($container);
$element3.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(Messages.fm_sharedFolder));
}
// Upload file
var $elementFileUpload = $('<li>', {
'class': 'cp-app-drive-new-fileupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('fileupload')).appendTo($container);
$elementFileUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadButton));
// Upload folder
if (APP.allowFolderUpload) {
var $elementFolderUpload = $('<li>', {
'class': 'cp-app-drive-new-folderupload cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('folderupload')).appendTo($container);
$elementFolderUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.uploadFolderButton));
}
// Link
var $elementLink = $('<li>', {
'class': 'cp-app-drive-new-link cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon('link')).appendTo($container);
$elementLink.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.fm_link_type));
}
// Pads
getNewPadTypes().forEach(function (type) {
var $element = $('<li>', {
'class': 'cp-app-drive-new-doc cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(getIcon(type)).appendTo($container);
$element.append($('<span>', {'class': 'cp-app-drive-new-name'})
.text(Messages.type[type]));
$element.attr('data-type', type);
getNewPadOptions(isInRoot).forEach(function (obj) {
if (obj.separator) { return; }
var premium = common.checkRestrictedApp(type);
if (premium < 0) {
$element.addClass('cp-app-hidden cp-app-disabled');
} else if (premium === 0) {
$element.addClass('cp-app-disabled');
var $element = $('<li>', {
'class': obj.class + ' cp-app-drive-element-row ' +
'cp-app-drive-element-grid'
}).prepend(obj.icon).appendTo($container);
$element.append($('<span>', { 'class': 'cp-app-drive-new-name' })
.text(obj.name));
if (obj.type) {
$element.attr('data-type', obj.type);
}
});
@ -3435,7 +3538,7 @@ define([
// 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 displayHrefArray = function ($container, rootName, draggable, typeFilter) {
var unsorted = files[rootName];
if (unsorted.length) {
var $fileHeader = getFileListHeader(true);
@ -3445,6 +3548,7 @@ define([
var sortBy = APP.store[SORT_FILE_BY];
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
sortedFiles = typeFilter ? filterPads(sortedFiles, typeFilter, false, true) : sortedFiles;
sortedFiles.forEach(function (id) {
var file = manager.getFileData(id);
if (!file) {
@ -3526,7 +3630,7 @@ define([
createGhostIcon($container);
};
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
var displayTrashRoot = function ($list, $folderHeader, $fileHeader, typeFilter) {
var filesList = [];
var root = files[TRASH];
var isEmpty = true;
@ -3549,14 +3653,25 @@ define([
isEmpty = false;
});
var sortedFolders = typeFilter ? [] : sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc);
if (typeFilter) {
var ids = sortedFiles.map(function (obj) { return obj.element; });
var idsFilter = filterPads(ids, typeFilter, false, true);
sortedFiles = sortedFiles.filter(function (obj) {
return (idsFilter.indexOf(obj.element) !== -1);
});
// prevent trash emptying while filter is active
isEmpty = true;
}
if (!isEmpty) {
var $empty = createEmptyTrashButton();
$content.append($empty);
}
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc());
if (manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
if (!typeFilter && manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
sortedFolders.forEach(function (f) {
var $element = createElement([TRASH], f.spath, root, true);
$list.append($element);
@ -3728,7 +3843,7 @@ define([
});
};
var displayRecent = function ($list) {
var displayRecent = function ($list, typeFilter) {
var filesList = manager.getRecentPads();
var limit = 20;
@ -3744,6 +3859,14 @@ define([
var i = 0;
var channels = [];
if (typeFilter) {
var ids = filesList.map(function (arr) { return arr[0]; });
var idsFilter = filterPads(ids, typeFilter, false, true);
filesList = filesList.filter(function (arr) {
return (idsFilter.indexOf(arr[0]) !== -1);
});
}
$list.append(h('li.cp-app-drive-element-separator', h('span', Messages.drive_active1Day)));
filesList.some(function (arr) {
var id = arr[0];
@ -3932,6 +4055,17 @@ define([
$content.html("");
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
.appendTo($content);
var typeFilter;
var isFilter = path[0] === FILTER;
if (isFilter) {
if (path.length < 3) { return; }
typeFilter = path[1];
path = path[2];
currentPath = path;
} else {
APP.store[FILTER_BY] = undefined;
}
var isInRoot = manager.isPathIn(path, [ROOT]);
var inTrash = manager.isPathIn(path, [TRASH]);
var isTrashRoot = manager.comparePath(path, [TRASH]);
@ -3939,6 +4073,8 @@ define([
var isAllFiles = manager.comparePath(path, [FILES_DATA]);
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
var isSearch = path[0] === SEARCH;
var isRecent = path[0] === RECENT;
var isOwned = path[0] === OWNED;
var isTags = path[0] === TAGS;
// ANON_SHARED_FOLDER
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
@ -4040,6 +4176,9 @@ define([
if (!readOnlyFolder) {
createNewButton(isInRoot, APP.toolbar.$bottomL);
}
if (!isTags && !isSearch) {
createFilterButton(isTemplate, APP.toolbar.$bottomL);
}
if (APP.mobile()) {
var $context = $('<button>', {
@ -4075,16 +4214,16 @@ define([
var $fileHeader = getFileListHeader(true);
if (isTemplate) {
displayHrefArray($list, path[0], true);
displayHrefArray($list, path[0], true, typeFilter);
} else if (isAllFiles) {
displayAllFiles($list);
} else if (isTrashRoot) {
displayTrashRoot($list, $folderHeader, $fileHeader);
displayTrashRoot($list, $folderHeader, $fileHeader, typeFilter);
} else if (isSearch) {
displaySearch($list, path[1]);
} else if (path[0] === RECENT) {
displayRecent($list);
} else if (path[0] === OWNED) {
} else if (isRecent) {
displayRecent($list, typeFilter);
} else if (isOwned) {
displayOwned($list);
} else if (isTags) {
displayTags($list);
@ -4093,11 +4232,12 @@ define([
displaySharedFolder($list);
} else {
if (!inTrash) { $dirContent.contextmenu(openContextMenu('content')); }
if (manager.hasSubfolder(root)) { $list.append($folderHeader); }
if (!isFilter && manager.hasSubfolder(root)) { $list.append($folderHeader); }
// display sub directories
var keys = Object.keys(root);
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFolders = isFilter ? [] : sortElements(true, path, keys, null, !getSortFolderDesc());
var sortedFiles = sortElements(false, path, keys, APP.store[SORT_FILE_BY], !getSortFileDesc());
sortedFiles = isFilter ? filterPads(sortedFiles, typeFilter, path) : sortedFiles;
sortedFolders.forEach(function (key) {
if (manager.isFile(root[key])) { return; }
var $element = createElement(path, key, root, true);

View File

@ -1061,7 +1061,7 @@ define([
var requestBlock = h('p', requestButton);
var $requestBlock = $(requestBlock).hide();
content.push(requestBlock);
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: false,
metadata: data
}, function (err, obj) {
@ -1072,9 +1072,10 @@ define([
$requestBlock.show().find('button').click(function () {
if (spinner.getState()) { return; }
spinner.spin();
sframeChan.query('Q_REQUEST_ACCESS', {
sframeChan.query('Q_CONTACT_OWNER', {
send: true,
metadata: data
metadata: data,
query: "REQUEST_PAD_ACCESS"
}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);

View File

@ -12,7 +12,7 @@ define([
"Asterisk asterisk",
"Brainfuck brainfuck .b",
"C text/x-csrc .c",
"C text/x-c++src .cpp",
"C++ text/x-c++src .cpp",
"C-like clike .c",
"Clojure clojure .clj",
"CMake cmake _", /* no extension */
@ -50,7 +50,6 @@ define([
"HTML htmlmixed .html",
"HTTP http _", /* no extension */
"IDL idl .idl",
"JADE jade .jade",
"Java text/x-java .java",
"JavaScript javascript .js",
"Jinja2 jinja2 .j2",

View File

@ -335,6 +335,29 @@ define([
}
};
handlers['FORM_RESPONSE'] = function(common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
Messages.form_responseNotification = "New responses have been sent to your form <b>{0}</b>";
content.getFormatText = function() {
return Messages._getKey('form_responseNotification', [title]);
};
if (href) {
content.handler = function() {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
handlers['COMMENT_REPLY'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -481,6 +504,18 @@ define([
var missed = content.msg.missed;
var start = msg.start;
var title = Util.fixHTML(msg.title);
content.handler = function () {
var priv = common.getMetadataMgr().getPrivateData();
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
return void window.APP.moveToDate(time);
}
var url = Hash.hashToHref('', 'calendar');
var optsUrl = Hash.getNewPadURL(url, {
time: time
});
common.openURL(optsUrl);
};
content.getFormatText = function () {
var now = +new Date();

View File

@ -1947,42 +1947,23 @@ define([
}).nThen(cb);
};
// requestPadAccess is used to check if we have a way to contact the owner
// of the pad AND to send the request if we want
// data.send === false ==> check if we can contact them
// data.send === true ==> send the request
Store.requestPadAccess = function (clientId, data, cb) {
// contactPadOwner is used to send "REQUEST_ACCESS" messages
// and to notify form owners when sending a response
Store.contactPadOwner = function (clientId, data, cb) {
var owner = data.owner;
// If the owner was not is the pad metadata, check if it is a friend.
// We'll contact the first owner for whom we know the mailbox
/* // TODO decide whether we want to re-enable this feature for our own contacts
// communicate the exception to users that 'muting' won't apply to friends
check mailbox in our contacts is not compatible with the new "mute pad" feature
var owners = data.owners;
if (!owner && Array.isArray(owners)) {
var friends = store.proxy.friends || {};
// If we have friends, check if an owner is one of them (with a mailbox)
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
}
}
*/
// If send is true, send the request to the owner.
if (owner) {
if (data.send) {
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel
var sendTo = function (query, msg, user, _cb) {
if (store.mailbox) {
return store.mailbox.sendTo(query, msg, user, _cb);
}
Mailbox.sendToAnon(store.anon_rpc, query, msg, user, _cb);
};
sendTo(data.query, {
channel: data.channel,
data: data.msgData
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic

View File

@ -4,12 +4,13 @@ define([
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/calendar/recurrence.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'chainpad-listmap',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {};
var getStore = function (ctx, id) {
@ -90,7 +91,29 @@ define([
});
};
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var getRecurring = function (ev) {
var mid = new Date();
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
var startId = Rec.getMonthId(start);
var midId = Rec.getMonthId(mid);
var endId = Rec.getMonthId(end);
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
var all = [ev];
Array.prototype.push.apply(all, toAdd);
return Rec.applyUpdates(all);
};
var clearDismissed = function (ctx, uid) {
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
Object.keys(h).filter(function (id) {
return id.indexOf(uid) === 0;
}).forEach(function (id) {
delete h[id];
});
};
var _updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var now = +new Date();
var ev = Util.clone(_ev);
var uid = ev.id;
@ -101,6 +124,10 @@ define([
}
reminders[uid] = [];
if (_ev.deleted) { return; }
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
var last = ctx.store.data.lastVisit;
if (ev.isAllDay) {
@ -119,10 +146,11 @@ define([
if (ev.end <= now && !missed) {
// No reminder for past events
delete reminders[uid];
clearDismissed(ctx, uid);
return;
}
var send = function () {
var send = function (d) {
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
if (hide) { return; }
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
@ -133,11 +161,18 @@ define([
missed: Boolean(missed),
content: ev
},
hash: 'REMINDER|'+uid
hash: 'REMINDER|'+uid+'-'+d
}, null, function () {
});
};
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
var sent = false;
var sendNotif = function (delay) {
sent = true;
ctx.Store.onReadyEvt.reg(function () {
send(delay);
});
};
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
@ -148,6 +183,10 @@ define([
var delay = delayMinutes * 60000;
var time = now + delay;
if (d.some(function (minutes) {
return delayMinutes >= minutes;
})) { return; }
// setTimeout only work with 32bit timeout values. If the event is too far away,
// ignore this event for now
// FIXME: call this function again in xxx days to reload these missing timeout?
@ -156,18 +195,35 @@ define([
// If we're too late to send a notification, send it instantly and ignore
// all notifications that were supposed to be sent even earlier
if (ev.start <= time) {
sendNotif();
sendNotif(delayMinutes);
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif();
sendNotif(delayMinutes);
}, (ev.start - time)));
});
if (!sent) {
// Remone any existing notification from the UI
ctx.Store.onReadyEvt.reg(function () {
ctx.store.mailbox.hideMessage('reminders', {
hash: 'REMINDER|'+uid
}, null, function () {
});
});
}
};
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
var all = getRecurring(Util.clone(ev));
all.forEach(function (_ev) {
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!ev) { return; } // XXX deleted event remote: delete reminders
if (!calendar || !calendar.reminders) { return; }
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
@ -352,10 +408,20 @@ define([
c.lm = lm;
var proxy = c.proxy = lm.proxy;
var _updateCalled = false;
var _update = function () {
if (_updateCalled) { return; }
_updateCalled = true;
setTimeout(function () {
_updateCalled = false;
update();
});
};
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) {
@ -372,24 +438,39 @@ define([
title: data.title
};
}
setTimeout(update);
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
_update();
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
addReminders(ctx, channel, n);
return void addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
addReminders(ctx, channel, {
return void addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
setTimeout(function () {
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
// Updated event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
// Updated recurring event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('remove', ['content'], function (x, p) {
_update();
if ((p.length >= 3 && p[2] === 'reminders') ||
(p.length >= 6 && p[5] === 'reminders')) {
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
@ -400,10 +481,10 @@ define([
updateLocalCalendars(ctx, c, md);
}).on('disconnect', function () {
c.offline = true;
setTimeout(update);
_update();
}).on('reconnect', function () {
c.offline = false;
setTimeout(update);
_update();
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
@ -411,7 +492,7 @@ define([
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
setTimeout(update);
_update();
}
cb(info);
});
@ -760,8 +841,11 @@ define([
var ev = c.proxy.content[data.ev.id];
if (!ev) { return void cb({error: "EINVAL"}); }
data.rawData = data.rawData || {};
// update the event
var changes = data.changes || {};
var type = data.type || {};
var newC;
if (changes.calendarId) {
@ -770,7 +854,122 @@ define([
newC.proxy.content = newC.proxy.content || {};
}
var RECUPDATE = {
one: {},
from: {}
};
if (['one','from','all'].includes(type.which)) {
ev.recUpdate = ev.recUpdate || RECUPDATE;
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
}
var update = ev.recUpdate;
var alwaysAll = ['calendarId'];
var keys = Object.keys(changes).filter(function (s) {
// we can only change the calendar or recurrence rule on the origin
return !alwaysAll.includes(s);
});
// Delete (future) affected keys
var cleanAfter = function (time) {
[update.from, update.one].forEach(function (obj) {
Object.keys(obj).forEach(function (d) {
if (Number(d) < time) { return; }
delete obj[d];
});
});
};
var cleanKeys = function (obj, when) {
Object.keys(obj).forEach(function (d) {
if (when && Number(d) < when) { return; }
keys.forEach(function (k) {
delete obj[d][k];
});
});
};
// Update recurrence rule. We may create a new event here
var dontSendUpdate = false;
if (typeof(changes.recurrenceRule) !== "undefined") {
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
cleanAfter(type.when);
} else {
update = ev.recUpdate = RECUPDATE;
}
}
if (type.which === "one") {
update.one[type.when] = update.one[type.when] || {};
// Nothing to delete
} else if (type.which === "from") {
update.from[type.when] = update.from[type.when] || {};
// Delete all "single/from" updates (affected keys only) after this "from" date
cleanKeys(update.from, type.when);
cleanKeys(update.one, type.when);
} else if (type.which === "all") {
// Delete all "single/from" updates (affected keys only) after
cleanKeys(update.from);
cleanKeys(update.one);
}
if (changes.start && (!type.which || type.which === "all")) {
var diff = changes.start - ev.start;
var newOne = {};
var newFrom = {};
Object.keys(update.one).forEach(function (time) {
newOne[Number(time)+diff] = update.one[time];
});
Object.keys(update.from).forEach(function (time) {
newFrom[Number(time)+diff] = update.from[time];
});
update.one = newOne;
update.from = newFrom;
}
// Clear the "dismissed" reminders when the user is updating reminders
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
if (changes.reminders) {
if (type.which === 'one') {
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
else { delete h[data.ev.id +'|'+ type.when]; }
} else if (type.which === "from") {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
var time = Number(id.split('|')[1]);
if (!time) { return; }
if (time < type.when) { return; }
delete h[id];
});
} else {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
delete h[id];
});
}
}
// Apply the changes
Object.keys(changes).forEach(function (key) {
if (!alwaysAll.includes(key) && type.which === "one") {
if (key === "recurrenceRule") {
if (data.rawData && data.rawData.isOrigin) {
return (ev[key] = changes[key]);
}
// Always "from", never "one" for recurrence rules
update.from[type.when] = update.from[type.when] || {};
return (update.from[type.when][key] = changes[key]);
}
update.one[type.when][key] = changes[key];
return;
}
if (!alwaysAll.includes(key) && type.which === "from") {
update.from[type.when][key] = changes[key];
return;
}
ev[key] = changes[key];
});
@ -790,6 +989,7 @@ define([
delete c.proxy.content[data.ev.id];
}
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
@ -806,8 +1006,8 @@ define([
addReminders(ctx, id, ev);
}
sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); }
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
if (newC && !dontSendUpdate) { sendUpdate(ctx, newC); }
cb();
});
};
@ -816,7 +1016,22 @@ define([
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
var evId = data.id.split('|')[0];
if (data.id === evId) {
delete c.proxy.content[data.id];
} else {
var ev = c.proxy.content[evId];
var s = data.raw && data.raw.start;
if (s) {
ev.recUpdate = ev.recUpdate || {
one: {},
from: {}
};
ev.recUpdate.one[s] = {
deleted: true
};
}
}
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, {
id: data.id,
@ -867,6 +1082,20 @@ define([
openChannels(ctx);
}));
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
var uid = p[1].split('|')[0];
Object.keys(ctx.calendars).some(function (calId) {
var c = ctx.calendars[calId];
if (!c || !c.proxy || !c.proxy.content) { return; }
if (c.proxy.content[uid]) {
setTimeout(function () {
addReminders(ctx, calId, c.proxy.content[uid]);
});
return true;
}
});
});
calendar.closeTeam = function (teamId) {
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];

View File

@ -575,6 +575,59 @@ define([
cb();
};
// Hide duplicates when receiving a form notification:
// Keep only one notification per channel
var formNotifs = {};
handlers['FORM_RESPONSE'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
var channel = content.channel;
if (!channel) { return void cb(true); }
var title, href;
ctx.Store.getAllStores().some(function (s) {
var res = s.manager.findChannel(channel);
// Check if the pad is in our drive
return res.some(function (obj) {
if (!obj.data) { return; }
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
href = obj.data.href || obj.data.roHref;
title = obj.data.filename || obj.data.title;
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
});
});
// If we don't have the edit url, ignore this notification
if (!href) { return void cb(true); }
// Add the title
content.href = href;
content.title = title;
// Remove duplicates
var old = formNotifs[channel];
var toRemove = old ? old.data : undefined;
// Update the data
formNotifs[channel] = {
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['FORM_RESPONSE'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = content.channel;
var old = formNotifs[channel];
if (old && old.data && old.data.hash === hash) {
delete formNotifs[channel];
}
};
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var comments = {};

View File

@ -164,6 +164,22 @@ proxy.mailboxes = {
});
});
};
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
var Nacl = Crypto.Nacl;
var curveSeed = Nacl.randomBytes(32);
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
sendTo({
store: {
anon_rpc: anonRpc,
proxy: {
curvePrivate: curvePrivate,
curvePublic: curvePublic
}
}
}, type, msg, user, cb);
};
// Mark a message as read
var dismiss = function (ctx, data, cId, cb) {
@ -177,6 +193,15 @@ proxy.mailboxes = {
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
var uid = hash.slice(9).split('-')[0];
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
if (!d) {
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
d = h[uid] = h[uid] || [];
}
var delay = hash.split('-')[1];
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
return;
}
@ -590,6 +615,9 @@ proxy.mailboxes = {
});
};
mailbox.hideMessage = function (type, msg) {
hideMessage(ctx, type, msg.hash, ctx.clients);
};
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;

View File

@ -79,7 +79,7 @@ define([
GET_HISTORY: Store.getHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,
CONTACT_PAD_OWNER: Store.contactPadOwner,
GIVE_PAD_ACCESS: Store.givePadAccess,
BURN_PAD: Store.burnPad,
GET_PAD_METADATA: Store.getPadMetadata,

View File

@ -20,7 +20,9 @@ define([
'netflux-client': '/bower_components/netflux-websocket/netflux-client',
'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux',
'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap',
'cm-extra': '/lib/codemirror-extra-modes'
'cm-extra': '/lib/codemirror-extra-modes',
// asciidoctor same
'asciidoctor': '/lib/asciidoctor/asciidoctor.min'
},
map: {
'*': {

View File

@ -80,7 +80,7 @@ define([
try {
var val = JSON.parse(states[idx].getContent().doc);
var md = config.extractMetadata(val);
var users = Object.keys(md.users).sort();
var users = Object.keys(md.users || {}).sort();
return users.join();
} catch (e) {
console.error(e);

View File

@ -13,6 +13,7 @@ define([
Mailbox.create = function (Common) {
var mailbox = Common.mailbox;
var sframeChan = Common.getSframeChannel();
var priv = Common.getMetadataMgr().getPrivateData();
var execCommand = function (cmd, data, cb) {
sframeChan.query('Q_MAILBOX_COMMAND', {
@ -67,6 +68,14 @@ define([
}
} else if (data.type === 'reminders') {
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
if (priv.app !== 'calendar') { avatar.classList.add('cp-reminder'); }
$(avatar).click(function (e) {
e.stopPropagation();
if (data.content && data.content.handler) {
return void data.content.handler();
}
Common.openURL(Hash.hashToHref('', 'calendar'));
});
} else if (userData && typeof(userData) === "object" && userData.profile) {
avatar = h('span.cp-avatar');
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
@ -120,7 +129,8 @@ define([
onViewedHandlers.push(function (data) {
var hash = data.hash.replace(/"/g, '\\\"');
var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
if (/^REMINDER\|/.test(hash)) { hash = hash.split('-')[0]; }
var $notif = $('.cp-notification[data-hash^="'+hash+'"]:not(.cp-app-notification-archived)');
if ($notif.length) {
$notif.remove();
}

View File

@ -1010,9 +1010,9 @@ define([
});
});
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
// CONTACT_OWNER is used both to check IF we can contact an owner (send === false)
// AND also to send the request if we want (send === true)
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
sframeChan.on('Q_CONTACT_OWNER', function (data, cb) {
if (readOnly && hashes.editHash) {
return void cb({error: 'ALREADYKNOWN'});
}
@ -1030,8 +1030,6 @@ define([
var crypto = Crypto.createEncryptor(_secret.keys);
nThen(function (waitFor) {
// Try to get the owner's mailbox from the pad metadata first.
// If it's is an older owned pad, check if the owner is a friend
// or an acquaintance (from async-store directly in requestAccess)
var todo = function (obj) {
owners = obj.owners;
@ -1065,11 +1063,12 @@ define([
}));
}).nThen(function () {
// If we are just checking (send === false) and there is a mailbox field, cb state true
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
if (!send) { return void cb({state: Boolean(owner)}); }
Cryptpad.padRpc.requestAccess({
Cryptpad.padRpc.contactOwner({
send: send,
query: data.query,
msgData: data.msgData,
channel: _secret.channel,
owner: owner,
owners: owners

View File

@ -663,49 +663,6 @@ MessengerUI, Messages, Pages) {
return $shareBlock;
};
/*
var createRequest = function (toolbar, config) {
if (!config.metadataMgr) {
throw new Error("You must provide a `metadataMgr` to display the request access button");
}
// We can only requets more access if we're in read-only mode
if (config.readOnly !== 1) { return; }
var $requestBlock = $('<button>', {
'class': 'fa fa-lock cp-toolbar-share-button',
title: Messages.requestEdit_button
}).hide();
// If we have access to the owner's mailbox, display the button and enable it
// false => check if we can contact the owner
// true ==> send the request
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:false}, function (err, obj) {
if (obj && obj.state) {
var locked = false;
$requestBlock.show().click(function () {
if (locked) { return; }
locked = true;
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:true}, function (err, obj) {
if (obj && obj.state) {
UI.log(Messages.requestEdit_sent);
$requestBlock.hide();
} else {
locked = false;
}
});
});
}
});
toolbar.$leftside.append($requestBlock);
toolbar.request = $requestBlock;
return $requestBlock;
};
*/
var createTitle = function (toolbar, config) {
var $titleContainer = $('<span>', {
'class': TITLE_CLS

View File

@ -1500,5 +1500,57 @@
"og_register": "Registriere einen Account auf {0}",
"admin_conflictExplanation": "Es gibt zwei Versionen dieses Dokuments. Wenn du die archivierte Version wiederherstellst, wird die aktuelle Version überschrieben. Wenn du die aktuelle Version archivierst, überschreibst du die archivierte Version. Beide Aktionen können nicht rückgängig gemacht werden.",
"admin_note": "Abo-Notiz",
"admin_planName": "Abo-Name"
"admin_planName": "Abo-Name",
"calendar_rec_until_count2": "Ereignissen",
"calendar_rec_until_date": "Am",
"calendar_rec_until_no": "Nie",
"calendar_rec_until": "Wiederholen beenden",
"calendar_str_filter_month": "Monate: {0}",
"calendar_str_filter_weekno": "Wochen: {0}",
"calendar_str_filter_day": "Tage: {0}",
"calendar_rec_edit": "Dies ist ein sich wiederholendes Ereignis",
"calendar_rec_edit_one": "Nur dieses Ereignis bearbeiten",
"calendar_rec_edit_from": "Alle zukünftigen Ereignisse bearbeiten",
"calendar_rec_edit_all": "Alle Ereignisse bearbeiten",
"calendar_rec_stop": "Nicht mehr wiederholen",
"calendar_month_last": "letzter Tag",
"calendar_rec_until_count": "Nach",
"calendar_rec_freq_yearly": "Jahre",
"calendar_rec_freq_monthly": "Monate",
"calendar_rec_freq_weekly": "Wochen",
"calendar_rec_freq_daily": "Tage",
"calendar_str_filter": "Filter:",
"calendar_rec_txt": "Wiederholen alle",
"calendar_rec_custom": "Benutzerdefiniert",
"calendar_rec_weekdays": "Täglich an Wochentagen",
"calendar_rec_weekend": "Täglich an Wochenenden",
"calendar_rec_weekly": "Wöchentlich am {0}",
"calendar_rec_daily": "Täglich",
"calendar_rec_no": "Einmalig",
"calendar_rec": "Wiederholen",
"fm_rmFilter": "Filter entfernen",
"fm_filterBy": "Filter",
"calendar_rec_yearly": "Jährlich am {2}",
"calendar_str_yearly": "{0} Jahr(e)",
"calendar_str_monthly": "{0} Monat(e)",
"calendar_nth_5": "fünften",
"calendar_rec_every_date": "Jeden {0}",
"calendar_nth_4": "vierten",
"calendar_nth_3": "dritten",
"calendar_list": "{0}, {1}",
"calendar_nth_2": "zweiten",
"calendar_list_end": "{0} oder {1}",
"calendar_nth_1": "ersten",
"calendar_rec_monthly": "Monatlich, Tag {1}",
"calendar_str_for": "für {0} Ereignisse",
"calendar_str_until": "bis zum {0}",
"calendar_str_monthday": "am {0}",
"calendar_rec_monthly_nth": "Jeden {0} {1} im Monat",
"calendar_rec_yearly_nth": "Jeden {0} {1} im {2}",
"calendar_rec_monthly_pick": "An Tagen",
"calendar_str_daily": "{0} Tag(e)",
"calendar_str_weekly": "{0} Woche(n)",
"calendar_str_nthdayofmonth": "am {0} im {1}",
"calendar_str_day": "am {0}",
"calendar_nth_last": "letzten"
}

View File

@ -1500,5 +1500,60 @@
"admin_listMyInstanceHint": "Si tu instancia es adecuada para el uso público puedes consentir a que sea enlistada en los directorios de la red. La telemetría del servidor debe estar activada para que esto tenga algún efecto.",
"admin_listMyInstanceTitle": "Listar mi instancia en los directorios públicos",
"admin_consentToContactLabel": "Consiento",
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor."
"admin_consentToContactHint": "La telemetría del servidor incluye el correo de contacto del administrador/a para que así los/as desarrolladores/as puedan notificarte de problemas serios con el software o tu configuración. Nunca será compartido, vendido, o usado por razones de marketing. Consiente al contacto si te gustaría estar informado/a de problemas críticos en tu servidor.",
"calendar_nth_5": "quinto",
"calendar_nth_last": "último",
"calendar_rec_monthly_nth": "Cada {0} {1} del mes",
"calendar_rec_yearly_nth": "Cada {0} {1} de {2}",
"calendar_rec_every_date": "Cada {0}",
"calendar_nth_4": "cuarto",
"calendar_month_last": "último día",
"calendar_nth_3": "tercero",
"calendar_list": "{0}, {1}",
"calendar_nth_2": "segundo",
"calendar_list_end": "{0} ó {1}",
"calendar_nth_1": "primero",
"calendar_str_yearly": "{0} año(s)",
"calendar_str_monthly": "{0} mes(es)",
"calendar_rec_monthly_pick": "Se repite en",
"calendar_str_weekly": "{0} semana(s)",
"calendar_rec_until_count2": "tiempos",
"calendar_rec_until_count": "Después",
"calendar_str_daily": "{0} día(s)",
"calendar_rec_until_date": "En",
"calendar_str_day": "en {0}",
"calendar_rec_until_no": "Nunca",
"calendar_rec_until": "Dejar de repetir",
"calendar_str_monthday": "en el {0}",
"calendar_rec_freq_yearly": "años",
"calendar_str_nthdayofmonth": "en el {0} de {1}",
"calendar_str_for": "por {0} veces",
"calendar_rec_freq_monthly": "meses",
"calendar_str_until": "hasta {0}",
"calendar_rec_freq_weekly": "semanas",
"calendar_rec_freq_daily": "días",
"calendar_str_filter": "Filtros:",
"calendar_rec_txt": "Repetir cada",
"calendar_str_filter_month": "Meses: {0}",
"calendar_str_filter_weekno": "Semanas: {0}",
"calendar_str_filter_yearday": "Días del año: {0}",
"calendar_rec_custom": "Personalizado",
"calendar_str_filter_monthday": "Días del mes: {0}",
"calendar_rec_weekdays": "Diariamente los días de semana",
"calendar_str_filter_day": "Días: {0}",
"calendar_rec_edit": "Este es un evento repetido",
"calendar_rec_weekend": "Diariamente los fines de semana",
"calendar_rec_edit_one": "Solo editar este evento",
"calendar_rec_yearly": "Anualmente en {2}",
"calendar_rec_edit_from": "Editar todos los futuros eventos",
"calendar_rec_monthly": "Mensual, día {1}",
"calendar_rec_edit_all": "Editar todos los eventos",
"calendar_rec_weekly": "Semanal en {0}",
"calendar_rec_stop": "Dejar de repetir",
"calendar_rec_daily": "Diario",
"calendar_rec_updated": "Regla actualizada en {0}",
"calendar_rec_no": "Ninguno",
"calendar_rec": "Repetir",
"fm_rmFilter": "Eliminar filtro",
"fm_filterBy": "Filtro"
}

View File

@ -1500,5 +1500,65 @@
"og_features": "{0} Fonctionnalités",
"og_encryptedAppType": "Chiffré {0}",
"admin_conflictExplanation": "Il existe deux versions de ce document. La restauration de la version archivée va écraser la version courante. L'archivage de la version courante va écraser le document archivé. Aucune de ces actions ne peut être annulée.",
"admin_documentConflict": "Archiver/restaurer"
"admin_documentConflict": "Archiver/restaurer",
"fm_rmFilter": "Désactiver le filtre",
"fm_filterBy": "Filtrer",
"calendar_nth_2": "deuxième",
"calendar_nth_1": "premier",
"calendar_rec_monthly_pick": "Certains jours",
"calendar_rec_until_count2": "fois",
"calendar_rec_until_count": "Après",
"calendar_rec_until_date": "Le",
"calendar_rec_until_no": "Jamais",
"calendar_rec_until": "Se termine",
"calendar_rec_freq_yearly": "ans",
"calendar_rec_freq_monthly": "mois",
"calendar_rec_freq_weekly": "semaines",
"calendar_rec_freq_daily": "jours",
"calendar_rec_txt": "Répéter tous les",
"calendar_rec_custom": "Personnalisé",
"calendar_rec_weekdays": "Chaque jour (semaine)",
"calendar_rec_weekend": "Chaque jour (week-end)",
"calendar_rec_yearly": "Chaque année le {2}",
"calendar_rec_monthly": "Chaque {1} du mois",
"calendar_rec_weekly": "Chaque semaine le {0}",
"calendar_rec_stop": "Ne plus répéter",
"calendar_rec_daily": "Chaque jour",
"calendar_rec_updated": "Règle mise à jour le {0}",
"calendar_rec_no": "Une fois",
"calendar_rec": "Répéter",
"calendar_rec_monthly_nth": "Chaque {0} {1} du mois",
"calendar_rec_yearly_nth": "Chaque {0} {1} de {2}",
"calendar_rec_every_date": "Chaque {0}",
"calendar_month_last": "dernier jour",
"calendar_list": "{0}, {1}",
"calendar_list_end": "{0} ou {1}",
"calendar_str_yearly": "{0} an(s)",
"calendar_str_monthly": "{0} mois",
"calendar_str_weekly": "{0} semaine(s)",
"calendar_str_daily": "{0} jour(s)",
"calendar_str_day": "le {0}",
"calendar_str_monthday": "le {0}",
"calendar_str_nthdayofmonth": "le {0} {1}",
"calendar_str_for": "pour {0} fois",
"calendar_str_until": "jusqu'au {0}",
"calendar_str_filter": "Filtres :",
"calendar_str_filter_month": "Mois : {0}",
"calendar_str_filter_weekno": "Semaines : {0}",
"calendar_str_filter_yearday": "Jours de l'année : {0}",
"calendar_str_filter_monthday": "Jours du mois : {0}",
"calendar_str_filter_day": "Jours : {0}",
"calendar_rec_edit": "Cet événement se répète",
"calendar_rec_edit_one": "Modifier seulement cet événement",
"calendar_rec_edit_from": "Modifier les événements futurs",
"calendar_rec_edit_all": "Modifier tous les événements",
"calendar_nth_last": "dernier",
"calendar_nth_5": "cinquième",
"calendar_nth_4": "quatrième",
"calendar_nth_3": "troisième",
"calendar_removeNotification": "Supprimer le rappel",
"calendar_rec_warn_updateall": "La règle de répétition de cet événement a été modifiée. Le premier événement sur {0} sera conservé, tous les autres seront remplacés.",
"calendar_rec_warn_update": "La règle de répétition de cet événement a été modifiée. Les événements futurs seront remplacés.",
"calendar_rec_warn_delall": "Cet événement ne sera plus répété. Le premier événement du {0} sera conservé, tous les autres seront supprimés.",
"calendar_rec_warn_del": "Cet événement ne sera plus répété. Les événements futurs seront supprimés."
}

View File

@ -1495,5 +1495,49 @@
"admin_blockKey": "ブロックの公開鍵",
"admin_blockMetadataPlaceholder": "ブロックの絶対または相対URL",
"admin_restoreReason": "復元の理由を指定し、確認して続行してください",
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください"
"admin_archiveReason": "アーカイブの理由を指定し、確認して続行してください",
"calendar_nth_1": "第1",
"calendar_list_end": "{0}または{1}",
"calendar_nth_2": "第2",
"calendar_list": "{0}、{1}",
"calendar_nth_3": "第3",
"calendar_nth_last": "最終",
"calendar_nth_5": "第5",
"calendar_rec_every_date": "毎{0}",
"calendar_nth_4": "第4",
"calendar_month_last": "最終日",
"calendar_str_yearly": "{0}年",
"calendar_str_weekly": "{0}週",
"calendar_str_monthly": "{0}月",
"calendar_rec_until_count2": "回",
"calendar_str_daily": "{0}日",
"calendar_str_filter_month": "月:{0}",
"calendar_str_filter_weekno": "週:{0}",
"calendar_rec_custom": "ユーザー定義",
"calendar_rec_edit": "これは繰り返すイベントです",
"calendar_rec_weekend": "毎週末",
"calendar_rec_edit_one": "このイベントのみを編集",
"calendar_rec_yearly": "毎年{2}",
"calendar_rec_edit_from": "全ての未来のイベントを編集",
"calendar_rec_monthly": "毎月{1}日",
"calendar_rec_edit_all": "全てのイベントを編集",
"calendar_rec_weekly": "毎週{0}",
"calendar_rec_stop": "繰り返しを停止",
"calendar_rec_daily": "毎日",
"calendar_rec_until": "繰り返しを停止",
"calendar_str_monthday": "{0}に",
"calendar_rec_freq_yearly": "年",
"calendar_str_for": "{0}回まで",
"calendar_rec_freq_monthly": "月",
"calendar_str_until": "{0}まで",
"calendar_rec_freq_weekly": "週",
"calendar_rec_freq_daily": "日",
"calendar_str_filter": "フィルター:",
"calendar_rec_txt": "繰り返しの頻度",
"calendar_rec_no": "なし",
"calendar_rec": "繰り返す",
"fm_rmFilter": "フィルターを削除",
"fm_filterBy": "フィルター",
"admin_conflictExplanation": "このドキュメントには2つのバージョンがあります。アーカイブされたバージョンを復元すると、現在のバージョンが上書きされます。現在のバージョンをアーカイブすると、アーカイブ済のバージョンが上書きされます。どちらのアクションも取り消しできません。",
"admin_documentConflict": "アーカイブ/復元"
}

View File

@ -1500,5 +1500,65 @@
"ui_jsRequired": "JavaScript must be enabled to perform encryption in your browser",
"og_encryptedAppType": "Encrypted {0}",
"admin_documentConflict": "Archive/restore",
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone."
"admin_conflictExplanation": "Two versions of this document exist. Restoring the archived version will overwrite the live version. Archiving the live version will overwrite the archived version. Neither action can be undone.",
"fm_filterBy": "Filter",
"fm_rmFilter": "Remove filter",
"calendar_rec": "Repeat",
"calendar_rec_no": "One time",
"calendar_rec_updated": "Rule updated on {0}",
"calendar_rec_daily": "Daily",
"calendar_rec_stop": "Stop repeating",
"calendar_rec_weekly": "Weekly on {0}",
"calendar_rec_edit_all": "Edit all events",
"calendar_rec_monthly": "Monthly, day {1}",
"calendar_rec_edit_from": "Edit future events",
"calendar_rec_yearly": "Yearly on {2}",
"calendar_rec_edit_one": "Edit only this event",
"calendar_rec_weekend": "Daily on weekends",
"calendar_rec_edit": "This is a repeating event",
"calendar_str_filter_day": "Days: {0}",
"calendar_rec_weekdays": "Daily on weekdays",
"calendar_str_filter_monthday": "Days of month: {0}",
"calendar_rec_custom": "Custom",
"calendar_str_filter_yearday": "Days of year: {0}",
"calendar_str_filter_weekno": "Weeks: {0}",
"calendar_str_filter_month": "Months: {0}",
"calendar_rec_txt": "Repeat every",
"calendar_str_filter": "Filters:",
"calendar_rec_freq_daily": "days",
"calendar_rec_freq_weekly": "weeks",
"calendar_str_until": "until {0}",
"calendar_rec_freq_monthly": "months",
"calendar_str_for": "for {0} times",
"calendar_str_nthdayofmonth": "on the {0} of {1}",
"calendar_rec_freq_yearly": "years",
"calendar_str_monthday": "on the {0}",
"calendar_rec_until": "Stop Repeating",
"calendar_rec_until_no": "Never",
"calendar_str_day": "on {0}",
"calendar_rec_until_date": "On",
"calendar_str_daily": "{0} day(s)",
"calendar_rec_until_count": "After",
"calendar_rec_until_count2": "times",
"calendar_str_weekly": "{0} week(s)",
"calendar_rec_monthly_pick": "On days",
"calendar_str_monthly": "{0} month(s)",
"calendar_str_yearly": "{0} year(s)",
"calendar_nth_1": "first",
"calendar_list_end": "{0} or {1}",
"calendar_nth_2": "second",
"calendar_list": "{0}, {1}",
"calendar_nth_3": "third",
"calendar_month_last": "last day",
"calendar_nth_4": "fourth",
"calendar_rec_every_date": "Every {0}",
"calendar_rec_yearly_nth": "Every {0} {1} of {2}",
"calendar_rec_monthly_nth": "Every {0} {1} of the month",
"calendar_nth_last": "last",
"calendar_nth_5": "fifth",
"calendar_rec_warn_del": "This event will no longer repeat. Future events will be removed.",
"calendar_rec_warn_delall": "This event will no longer repeat. The first event on {0} will be kept, all others will be removed.",
"calendar_rec_warn_update": "The rule for repeating this event was modified. Future events will be replaced.",
"calendar_rec_warn_updateall": "The rule for repeating this event was modified. The first event on {0} will be kept, all others will be replaced.",
"calendar_removeNotification": "Remove reminder"
}

View File

@ -989,7 +989,7 @@
}
.cp-poll-cell {
width: 100px;
height: 35px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;

View File

@ -13,8 +13,71 @@ define([
value += '"' + vv + '"';
return value;
};
Export.results = function (content, answers, TYPES, order, isArray) {
var exportJSON = function (content, answers, TYPES, order) {
var form = content.form;
var res = {
questions: {},
responses: []
};
var q = res.questions;
var r = res.responses;
// Add questions
var i = 1;
order.forEach(function (key) {
var obj = form[key];
if (!obj) { return; }
var type = obj.type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type] && TYPES[type].exportCSV) {
var _obj = Util.clone(obj);
_obj.q = "tmp";
q[id] = {
question: obj.q,
items: TYPES[type].exportCSV(false, _obj).map(function (str) {
return str.slice(6); // Remove "tmp | "
})
};
} else {
q[id] = obj.q || Messages.form_default;
}
});
Object.keys(answers || {}).forEach(function (key) {
var obj = answers[key];
var time = new Date(obj.time).toISOString();
var msg = obj.msg || {};
var user = msg._userdata || {};
var data = {
'_time': time,
'_name': user.name || Messages.anonymous
};
var i = 1;
order.forEach(function (key) {
if (!form[key]) { return; }
var type = form[key].type;
if (!TYPES[type]) { return; } // Ignore static types
var id = `q${i++}`;
if (TYPES[type].exportCSV) {
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
return;
}
data[id] = msg[key];
});
r.push(data);
});
return JSON.stringify(res, 0, 2);
};
Export.results = function (content, answers, TYPES, order, format) {
if (!content || !content.form) { return; }
if (format === "json") { return exportJSON(content, answers, TYPES, order); }
var isArray = format === "array";
var csv = "";
var array = [];
var form = content.form;

View File

@ -2334,6 +2334,12 @@ define([
}
}
});
var sortNode = function (order) {
order.forEach(function (uid) {
$tag.append($tag.find('[data-id="'+uid+'"]'));
});
};
return {
tag: tag,
isEmpty: function () { return !this.getValue(); },
@ -2348,7 +2354,7 @@ define([
var toSort = extractValues(opts.values).map(function (val) {
return invMap[val];
});
sortable.sort(toSort);
sortNode(toSort);
reorder(true);
},
setEditable: function (state) {
@ -2365,7 +2371,7 @@ define([
var toSort = val.map(function (val) {
return invMap[val];
});
sortable.sort(toSort);
sortNode(toSort);
reorder();
}
};
@ -2658,6 +2664,21 @@ define([
}), title);
});
// Export JSON
Messages.form_exportJSON = "Export as JSON"; // XXX
var exportJSONButton = h('button.btn.btn-primary', [
h('i.cptools.cptools-code'),
Messages.form_exportJSON
]);
$(exportJSONButton).appendTo($controls);
$(exportJSONButton).click(function () {
var arr = Exporter.results(content, answers, TYPES, getFullOrder(content), "json");
if (!arr) { return void UI.warn(Messages.error); }
window.saveAs(new Blob([arr], {
type: 'application/json'
}), title+".json");
});
// Export in "sheet"
var export2Button = h('button.btn.btn-primary', [
h('i.fa.fa-file-excel-o'),
@ -2665,7 +2686,7 @@ define([
]);
$(export2Button).appendTo($controls);
$(export2Button).click(function () {
var arr = Exporter.results(content, answers, TYPES, getFullOrder(content), true);
var arr = Exporter.results(content, answers, TYPES, getFullOrder(content), "array");
if (!arr) { return void UI.warn(Messages.error); }
var sframeChan = framework._.sfCommon.getSframeChannel();
var title = framework._.title.title || framework._.title.defaultTitle;
@ -3222,6 +3243,17 @@ define([
if (content.answers.cantEdit) {
$(radioContainer).hide();
}
// TODO show the author that they can "mute" the pad
var priv = metadataMgr.getPrivateData();
sframeChan.query('Q_CONTACT_OWNER', {
send: true,
query: "FORM_RESPONSE",
msgData: { channel: priv.channel }
}, function (err, obj) {
if (err || !obj || !obj.state) { return console.error('ENOTIFY'); }
});
});
});

1476
www/lib/asciidoctor/asciidoctor.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,14 @@ define([
};
var getEndDate = function () {
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
return endPickr.parseDate(e.value);
var d = endPickr.parseDate(e.value);
if (endPickr.config.dateFormat === "Y-m-d") { // All day event
// Tui-calendar will remove 1s (1000ms) to the date for an unknown reason...
d.setMilliseconds(1000);
}
return d;
};
return {

View File

@ -12,4 +12,5 @@ This file is intended to be used as a log of what third-party source we have ven
* [Fabricjs 4.6.0](https://github.com/fabricjs/fabric.js) and [Fabric-history](https://github.com/lyzerk/fabric-history) for the whiteboard app
* [Requirejs optional module plugin](https://stackoverflow.com/a/27422370)
* [asciidoc.js 2.0.0](https://github.com/asciidoctor/codemirror-asciidoc/releases/tag/2.0.0) with slight changes to match the format of other codemirror modes
* [Asciidoctor.js 2.2.6](https://github.com/asciidoctor/asciidoctor.js/releases/tag/v2.2.6) for AsciiDoc rendering