diff --git a/.jshintignore b/.jshintignore
index 2ac398427..d3c96b2c0 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -8,7 +8,6 @@ www/common/onlyoffice/v2*
www/common/onlyoffice/v4
www/common/onlyoffice/v5
-server.js
www/scratch
www/accounts
www/lib
diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less
index 5bf91f351..3cd4c0318 100644
--- a/customize.dist/src/less2/include/alertify.less
+++ b/customize.dist/src/less2/include/alertify.less
@@ -183,6 +183,11 @@
}
}
}
+ .cp-dropdown-content {
+ a {
+ text-decoration: none;
+ }
+ }
}
.cp-alertify-type-container {
overflow: visible !important;
diff --git a/customize.dist/src/less2/include/checkmark.less b/customize.dist/src/less2/include/checkmark.less
index f41f1bcb8..61a707299 100644
--- a/customize.dist/src/less2/include/checkmark.less
+++ b/customize.dist/src/less2/include/checkmark.less
@@ -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;
diff --git a/customize.dist/src/less2/include/dropdown.less b/customize.dist/src/less2/include/dropdown.less
index 5f55cb3e7..f6304db05 100644
--- a/customize.dist/src/less2/include/dropdown.less
+++ b/customize.dist/src/less2/include/dropdown.less
@@ -48,7 +48,7 @@
button {
.fa-caret-down {
- margin-right: 1em !important;
+ margin-right: 0.5em !important;
}
* {
.tools_unselectable();
diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less
index fa51a3c8c..c5db3861e 100644
--- a/customize.dist/src/less2/include/forms.less
+++ b/customize.dist/src/less2/include/forms.less
@@ -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 {
diff --git a/customize.dist/src/less2/include/icon-colors.less b/customize.dist/src/less2/include/icon-colors.less
index 0d7ecb121..112d2261b 100644
--- a/customize.dist/src/less2/include/icon-colors.less
+++ b/customize.dist/src/less2/include/icon-colors.less
@@ -5,6 +5,7 @@
& {
each(@colortheme_apps, {
+ button .cp-icon-color-@{key},
.cp-icon-color-@{key} { color: @value; }
});
diff --git a/customize.dist/src/less2/include/notifications.less b/customize.dist/src/less2/include/notifications.less
index f3bc8c0b4..0d58f4dc3 100644
--- a/customize.dist/src/less2/include/notifications.less
+++ b/customize.dist/src/less2/include/notifications.less
@@ -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;
diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less
index fecd471ee..b1eb49887 100644
--- a/customize.dist/src/less2/include/toolbar.less
+++ b/customize.dist/src/less2/include/toolbar.less
@@ -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;
}
}
diff --git a/lib/env.js b/lib/env.js
index bb32f88c0..8824c347f 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -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) {
diff --git a/lib/metadata.js b/lib/metadata.js
index f61d2d509..495c33ff9 100644
--- a/lib/metadata.js
+++ b/lib/metadata.js
@@ -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;
diff --git a/lib/storage/file.js b/lib/storage/file.js
index ef3d2ddfe..f074aad4e 100644
--- a/lib/storage/file.js
+++ b/lib/storage/file.js
@@ -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',
diff --git a/server.js b/server.js
index cbed35cf7..9c4215392 100644
--- a/server.js
+++ b/server.js
@@ -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) {
diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less
index 87f37dcb1..e8b7be8ed 100644
--- a/www/calendar/app-calendar.less
+++ b/www/calendar/app-calendar.less
@@ -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;
diff --git a/www/calendar/export.js b/www/calendar/export.js
index 307abad33..694179a9f 100644
--- a/www/calendar/export.js
+++ b/www/calendar/export.js
@@ -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);
+ });
});
};
diff --git a/www/calendar/inner.js b/www/calendar/inner.js
index 7a2e53c76..b3e9754b1 100644
--- a/www/calendar/inner.js
+++ b/www/calendar/inner.js
@@ -17,6 +17,7 @@ define([
'/customize/application_config.js',
'/lib/calendar/tui-calendar.min.js',
'/calendar/export.js',
+ '/calendar/recurrence.js',
'/lib/datepicker/flatpickr.js',
'/common/inner/share.js',
@@ -48,6 +49,7 @@ define([
AppConfig,
Calendar,
Export,
+ Rec,
Flatpickr,
Share, Access, Properties
)
@@ -121,11 +123,21 @@ define([
return (brightness > 125) ? '#424242' : '#EEEEEE';
};
+ var getDateLanguage = function () {
+ console.error(Messages._languageUsed);
+ try {
+ new Date().toLocaleDateString(Messages._languageUsed, { weekday: 'long' });
+ return Messages._languageUsed;
+ } catch (e) {
+ return;
+ }
+ };
+
var getWeekDays = function (large) {
var baseDate = new Date(2017, 0, 1); // just a Sunday
var weekDays = [];
for(var i = 0; i < 7; i++) {
- weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' }));
+ weekDays.push(baseDate.toLocaleDateString(getDateLanguage(), { weekday: 'long' }));
baseDate.setDate(baseDate.getDate() + 1);
}
if (!large) {
@@ -134,6 +146,67 @@ define([
return weekDays.map(function (day) { return day.replace(/^./, function (str) { return str.toUpperCase(); }); });
};
+ // Get week number in our calendar view
+ var ISO8601_week_no = function (dt) {
+ var tdt = new Date(dt.valueOf());
+ var dayn = (dt.getDay() + 6) % 7;
+ tdt.setDate(tdt.getDate() - dayn + 3);
+ var firstThursday = tdt.valueOf();
+ tdt.setMonth(0, 1);
+ if (tdt.getDay() !== 4) {
+ tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
+ }
+ return 1 + Math.ceil((firstThursday - tdt) / 604800000);
+ };
+
+ var updateDateRange = function () {
+ var range = APP.calendar._renderRange;
+ var start = range.start._date.toLocaleDateString();
+ var end = range.end._date.toLocaleDateString();
+ var week = ISO8601_week_no(range.start._date);
+ var date = [
+ h('b.cp-small', Messages._getKey('calendar_weekNumber', [week])),
+ h('b', start),
+ h('span', ' - '),
+ h('b', end),
+ ];
+ if (APP.calendar._viewName === "day") {
+ date = h('b', start);
+ } else if (APP.calendar._viewName === "month") {
+ var month;
+ var mid = new Date(Math.floor(((+range.start._date) + (+range.end._date)) / 2));
+ try {
+ month = mid.toLocaleString('default', {
+ month: 'long',
+ year:'numeric'
+ });
+ month = month.replace(/^./, function (str) { return str.toUpperCase(); });
+ date = h('b', month);
+ } catch (e) {
+ // Use same as week range: first day of month to last day of month
+ }
+ }
+ APP.toolbar.$bottomM.empty().append(h('div', date));
+ };
+ APP.moveToDate = function (time) {
+ var cal = APP.calendar;
+ if (!cal) { return; }
+
+ // Move calendar to correct date
+ var d = new Date(time);
+ cal.setDate(d);
+ updateDateRange();
+
+ if (cal.getViewName() === 'month') { return; }
+
+ // Scroll to correct time
+ setTimeout(function () {
+ var h = d.toLocaleTimeString('en-US', { hour12: 0, timeStyle: "short" }).slice(0,2);
+ h = Number(h) % 24;
+ var $h = $('.tui-full-calendar-timegrid-timezone .tui-full-calendar-timegrid-hour');
+ try { $h.get(h).scrollIntoView(); } catch (e) { console.error(e); }
+ });
+ };
var getCalendars = function () {
var LOOKUP = {};
@@ -170,6 +243,8 @@ define([
});
};
var getSchedules = function () {
+ APP.recurringEvents = [];
+ APP.recurringDone = [];
var s = [];
var calendars = Object.keys(APP.calendars);
if (APP.currentCalendar) {
@@ -200,19 +275,36 @@ define([
if (c.readOnly) {
obj.isReadOnly = true;
}
- s.push(data.content[uid]);
+ if (obj.recurrenceRule) {
+ APP.recurringEvents.push(obj);
+ }
+ s.push(Util.clone(data.content[uid]));
});
});
return s;
};
+
+ var applyUpdates = Rec.applyUpdates;
+
+ var updateRecurring = function () {}; // Defined later
var renderCalendar = function () {
var cal = APP.calendar;
if (!cal) { return; }
- cal.clear();
- cal.setCalendars(getCalendars());
- cal.createSchedules(getSchedules(), true);
- cal.render();
+ try {
+ cal.clear();
+ cal.setCalendars(getCalendars());
+ cal.createSchedules(applyUpdates(getSchedules()), true);
+ cal.render();
+ if (APP.initTime && APP.moveToDate) {
+ APP.moveToDate(APP.initTime);
+ delete APP.initTime;
+ }
+ Rec.resetCache();
+ updateRecurring();
+ } catch (e) {
+ console.error(e);
+ }
};
var onCalendarUpdate = function (data) {
var cal = APP.calendar;
@@ -261,11 +353,21 @@ define([
monthGridHeaderExceed: function(hiddenSchedules) {
return '' + Messages._getKey('calendar_more', [hiddenSchedules]) + '';
},
- popupEdit: function() { return Messages.poll_edit; },
- popupDelete: function() { return Messages.kanban_delete; },
+ popupEdit: function(obj) {
+ APP.editModalData = obj.data && obj.data.root;
+ return Messages.poll_edit;
+ },
popupDetailLocation: function(schedule) {
- // TODO detect url and create 'a' tag
- return Messages._getKey('calendar_location', [Util.fixHTML(schedule.location)]);
+ var l = schedule.location;
+ var str = Util.fixHTML(l);
+ delete APP.nextLocationUid;
+ if (/^https?:\/\//.test(l)) {
+ var uid = "cp-link-"+Util.uid();
+ str = `${str}`;
+ APP.nextLocationUid = uid;
+ }
+
+ return Messages._getKey('calendar_location', [str]);
},
popupIsAllDay: function() { return Messages.calendar_allDay; },
titlePlaceholder: function() { return Messages.calendar_title; },
@@ -767,47 +869,51 @@ define([
};
- var ISO8601_week_no = function (dt) {
- var tdt = new Date(dt.valueOf());
- var dayn = (dt.getDay() + 6) % 7;
- tdt.setDate(tdt.getDate() - dayn + 3);
- var firstThursday = tdt.valueOf();
- tdt.setMonth(0, 1);
- if (tdt.getDay() !== 4) {
- tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
+ var _updateRecurring = function () {
+ var cal = APP.calendar;
+ if (!cal) { return; }
+
+ var range = APP.calendar._renderRange;
+ var startView = range.start._date;
+ var endView = range.end._date;
+ endView.setDate(endView.getDate() + 1);
+
+ var midView = new Date(((+startView) + (+endView)) / 2);
+
+ // We want to generate recurring events month per month.
+ // In "month" view, we may see up to 3 different months
+ // at the same time.
+ var startId = Rec.getMonthId(startView);
+ var midId = Rec.getMonthId(midView);
+ var endId = Rec.getMonthId(endView);
+ var todo = Util.deduplicateString([startId, midId, endId]);
+ todo = todo.filter(function (monthId) {
+ return !APP.recurringDone.includes(monthId);
+ });
+
+ var toAdd = Rec.getRecurring(todo, APP.recurringEvents);
+
+ // Mark selected months as done
+ todo.forEach(function (monthId) { APP.recurringDone.push(monthId); });
+
+ cal.createSchedules(applyUpdates(toAdd));
+ };
+ updateRecurring = function () {
+ try {
+ _updateRecurring();
+ } catch (e) {
+ console.error(e);
}
- return 1 + Math.ceil((firstThursday - tdt) / 604800000);
};
- var updateDateRange = function () {
- var range = APP.calendar._renderRange;
- var start = range.start._date.toLocaleDateString();
- var end = range.end._date.toLocaleDateString();
- var week = ISO8601_week_no(range.start._date);
- var date = [
- h('b.cp-small', Messages._getKey('calendar_weekNumber', [week])),
- h('b', start),
- h('span', ' - '),
- h('b', end),
- ];
- if (APP.calendar._viewName === "day") {
- date = h('b', start);
- } else if (APP.calendar._viewName === "month") {
- var month;
- var mid = new Date(Math.floor(((+range.start._date) + (+range.end._date)) / 2));
- try {
- month = mid.toLocaleString('default', {
- month: 'long',
- year:'numeric'
- });
- month = month.replace(/^./, function (str) { return str.toUpperCase(); });
- date = h('b', month);
- } catch (e) {
- // Use same as week range: first day of month to last day of month
- }
- }
- APP.toolbar.$bottomM.empty().append(h('div', date));
- };
+/*
+UPDATE A RECCURENT EVENT:
+ICS ==> create a new event with the same UID and a RECURRENCE-ID field (with a value equals to the DTSTART of this recurring event)
+*/
+
+
+ var diffDate = Rec.diffDate;
+
var makeCalendar = function (view) {
var store = window.cryptpadStore;
@@ -857,12 +963,6 @@ define([
makeLeftside(cal, $(leftside));
cal.on('beforeCreateSchedule', function(event) {
- // TODO Recurrence (later)
- // On creation, select a recurrence rule (daily / weekly / monthly / more weird rules)
- // then mark it under recurrence rule with a uid (the same for all the recurring events)
- // ie: recurrenceRule: DAILY|{uid}
- // Use template to hide "recurrenceRule" from the detailPopup or at least to use
- // a non technical value
var reminders = APP.notificationsEntries;
var startDate = event.start._date;
@@ -871,13 +971,14 @@ define([
var schedule = {
id: Util.uid(),
calendarId: event.calendarId,
- title: Util.fixHTML(event.title),
+ title: event.title,
category: "time",
- location: Util.fixHTML(event.location),
+ location: event.location,
start: +startDate,
isAllDay: event.isAllDay,
end: +endDate,
reminders: reminders,
+ recurrenceRule: APP.recurrenceRule
};
newEvent(schedule, function (err) {
@@ -891,35 +992,146 @@ define([
cal.on('beforeUpdateSchedule', function(event) {
var changes = event.changes || {};
delete changes.state;
+
if (changes.end) { changes.end = +new Date(changes.end._date); }
if (changes.start) { changes.start = +new Date(changes.start._date); }
var old = event.schedule;
+ var id = old.id.split('|')[0];
- var oldReminders = Util.find(APP.calendars, [old.calendarId, 'content', 'content', old.id, 'reminders']);
- var reminders = APP.notificationsEntries;
- if (JSONSortify(oldReminders || []) !== JSONSortify(reminders)) {
- changes.reminders = reminders;
+ var originalEvent = Util.find(APP.calendars, [old.calendarId, 'content', 'content', id]);
+
+ var ev = APP.calendar.getSchedule(old.id, old.calendarId);
+ var evOrig = APP.calendar.getSchedule(id, old.calendarId);
+
+ var isOrigin = id === old.id;
+ var wasRecurrent = Boolean(originalEvent.recurrenceRule);
+
+ if (event.calendar) { // Don't update reminders and recurrence with drag&drop event
+ var oldReminders = ev.raw.reminders || originalEvent.reminders;
+ var reminders = APP.notificationsEntries;
+ if (JSONSortify(oldReminders || []) !== JSONSortify(reminders)) {
+ changes.reminders = reminders;
+ }
+
+ var oldRec = ev.recurrenceRule;
+ var rec = APP.recurrenceRule;
+ if (JSONSortify(oldRec || '') !== JSONSortify(rec)) {
+ changes.recurrenceRule = rec;
+ }
}
- updateEvent({
- ev: old,
- changes: changes
- }, function (err) {
- if (err) {
- console.error(err);
- return void UI.warn(err);
+
+ if (!event.triggerEventName || event.triggerEventName !== "click") {
+ APP.recurrenceRule = ev.recurrenceRule;
+ }
+
+ var afterConfirm = function () {
+ var raw = (ev && ev.raw) || {};
+ var rawData = { // Exact start and end of the selected event
+ start: raw.start || ev.start,
+ end: raw.end || ev.end,
+ isOrigin: isOrigin
+ };
+ if (['one', 'from'].includes(APP.editType)) {
+ if (changes.start) {
+ changes.start = diffDate(raw.start || ev.start, changes.start);
+ }
+ if (changes.end) {
+ changes.end = diffDate(raw.end || ev.end, changes.end);
+ }
}
- cal.updateSchedule(old.id, old.calendarId, changes);
+
+ old.id = id;
+ updateEvent({
+ ev: old,
+ changes: changes,
+ rawData: rawData,
+ type: {
+ which: APP.editType,
+ when: raw.start || ev.start
+ }
+ }, function (err) {
+ if (err) {
+ console.error(err);
+ return void UI.warn(err);
+ }
+ //cal.updateSchedule(old.id, old.calendarId, changes);
+ });
+ };
+
+
+ // Confirm modal: select which recurring events to update
+ if (!Object.keys(changes).length) { return void afterConfirm(); }
+ if (!wasRecurrent) { return void afterConfirm(); }
+
+ var list = ['one','from','all'];
+ if (isOrigin) { list = ['one', 'all']; }
+ if ((changes.start || changes.end) && !isOrigin) {
+ list = list.filter(function (item) {
+ return item !== "all";
+ });
+ }
+
+ var radioEls = list.map(function (k, i) {
+ return UI.createRadio('cp-calendar-rec-edit', 'cp-calendar-rec-edit-'+k,
+ Messages['calendar_rec_edit_'+k], !i, {input:{ 'data-value':k }});
});
+ var p = h('p', Messages.calendar_rec_edit);
+ var warn = h('div.alert.alert-warning');
+ var content = h('div', [
+ warn,
+ p,
+ radioEls
+ ]);
+ UI.confirm(content, function (yes) {
+ if (!yes) { return; }
+ var r = $(content).find('input[name="cp-calendar-rec-edit"]:checked')
+ .data('value');
+ APP.editType = r;
+ afterConfirm();
+ });
+ $(content).closest('.alertify').on('mousedown', function (e) {
+ e.stopPropagation();
+ });
+
+ var $p = $(p);
+ var $warn = $(warn);
+ var $radio = $(radioEls);
+ var recurrenceWarn = function () {
+ if (typeof(changes.recurrenceRule) === "undefined") {
+ $p.show();
+ return $warn.hide();
+ }
+ $warn.show();
+ $p.hide();
+ var val = $radio.find('input[name="cp-calendar-rec-edit"]:checked')
+ .data('value');
+
+
+ if (!changes.recurrenceRule) { // Rule was deleted
+ if (!val || val === "all") {
+ return $warn.text(Messages._getKey('calendar_rec_warn_delall', [
+ new Date(evOrig.start).toLocaleDateString()
+ ]));
+ }
+ return $warn.text(Messages.calendar_rec_warn_del);
+ }
+ if (!val || val === "all") {
+ return $warn.text(Messages._getKey('calendar_rec_warn_updateall', [
+ new Date(evOrig.start).toLocaleDateString()
+ ]));
+ }
+ return $warn.text(Messages.calendar_rec_warn_update);
+ };
+ recurrenceWarn();
+ $radio.find('input[type="radio"]').on('change', recurrenceWarn);
});
cal.on('beforeDeleteSchedule', function(event) {
- var data = event.schedule;
deleteEvent(event.schedule, function (err) {
if (err) {
console.error(err);
return void UI.warn(err);
}
- cal.deleteSchedule(data.id, data.calendarId);
});
});
@@ -930,7 +1142,6 @@ define([
});
updateDateRange();
-
renderCalendar();
// Toolbar
@@ -965,6 +1176,7 @@ define([
var mode = $(this).attr('data-value');
cal.changeView(mode);
updateDateRange();
+ updateRecurring();
store.put('calendarView', mode, function () {});
});
APP.toolbar.$bottomR.append($block);
@@ -983,24 +1195,620 @@ define([
var goLeft = h('button.fa.fa-chevron-left');
var goRight = h('button.fa.fa-chevron-right');
var goToday = h('button', Messages.calendar_today);
+ var goDate = h('button.fa.fa-calendar');
$(goLeft).click(function () {
cal.prev();
updateDateRange();
+ updateRecurring();
});
$(goRight).click(function () {
cal.next();
updateDateRange();
+ updateRecurring();
});
$(goToday).click(function () {
- cal.today();
+ APP.moveToDate(+new Date());
+ //cal.today();
updateDateRange();
+ updateRecurring();
+ });
+ $(goDate).click(function () {
+ var f = Flatpickr(goDate, {
+ enableTime: false,
+ defaultDate: APP.calendar.getDate()._date,
+ //dateFormat: dateFormat,
+ onChange: function (date) {
+ date[0].setHours(12);
+ f.destroy();
+ APP.moveToDate(+date[0]);
+ updateDateRange();
+ updateRecurring();
+ },
+ onClose: function () {
+ setTimeout(f.destroy);
+ }
+ });
+ f.open();
});
APP.toolbar.$bottomL.append(h('div.cp-calendar-browse', [
- goLeft, goToday, goRight
+ goLeft, goToday, goRight, goDate
]));
};
+
+ var WEEKDAYS = getWeekDays(true);
+ var listItems = function (_arr) {
+ var arr = _arr.slice();
+ if (arr.length === 1) {
+ return arr[0];
+ }
+ var shift = function () {
+ var i = arr.shift();
+ if (i === -1) { return Messages.calendar_month_last; }
+ return i;
+ };
+ var str = shift();
+ var i = 0;
+ while (arr.length > 1 && i < 367) {
+ str = Messages._getKey('calendar_list', [str, shift()]);
+ i++;
+ }
+ str = Messages._getKey('calendar_list_end', [str, shift()]);
+ return str;
+ };
+ var translate = function (rule) {
+ var str = "";
+ if (!rule || !rule.freq) { return; }
+ var tmp = new Date();
+
+ // Freq, interval
+ str = Messages._getKey('calendar_str_'+rule.freq, [rule.interval || 1]);
+
+ var m = rule.by && rule.by.month;
+ var d = rule.by && rule.by.day;
+ var md = rule.by && rule.by.monthday;
+
+ var ord = false;
+ if (d) {
+ d = d.map(function (str) {
+ var nth = str.slice(0, -2);
+ nth = (nth === '-1') ? 'last' : nth;
+ ord = Boolean(nth);
+ var day = str.slice(-2);
+ var n = Rec.DAYORDER.indexOf(day);
+ var dayStr = WEEKDAYS[n];
+ if (nth) { return Messages['calendar_nth_'+nth] + " " + dayStr; }
+ return dayStr;
+ });
+ }
+ if (m) {
+ m = m.map(function (n) {
+ tmp.setMonth(n-1);
+ return tmp.toLocaleDateString(getDateLanguage(), { month: 'long' });
+ });
+ }
+
+ // Until / count
+ var end = "";
+ if (rule.count) {
+ end += " " + Messages._getKey('calendar_str_for', [rule.count]);
+ }
+ if (rule.until) {
+ end += " " + Messages._getKey('calendar_str_until', [
+ new Date(rule.until).toLocaleDateString(getDateLanguage(), {
+ month: "long",
+ day: "numeric",
+ year: "numeric"
+ })
+ ]);
+ }
+
+ var filters = [];
+ // nth day (of month)
+ if (rule.freq === "yearly" && m && m.length === 1 && d && d.length === 1
+ && Object.keys(rule.by).length === 2) {
+ str += " " + Messages._getKey('calendar_str_nthdayofmonth', [
+ d[0],
+ m[0]
+ ]);
+ } else if (rule.freq === "monthly" && d && d.length === 1 && ord
+ && Object.keys(rule.by).length === 1) {
+ str += " " + Messages._getKey('calendar_str_monthday', [
+ d[0]
+ ]);
+ } else if (rule.freq === "monthly" && md && Object.keys(rule.by).length === 1) {
+ str += " " + Messages._getKey('calendar_str_monthday', [listItems(md)]);
+ } else if (rule.freq === "weekly" && d && Object.keys(rule.by).length === 1) {
+ str += " " + Messages._getKey('calendar_str_day', [listItems(d)]);
+ } else if (rule.by) {
+ filters = Object.keys(rule.by).map(function (k) {
+ var val = rule.by[k];
+ if (k === "month") { val = m; }
+ if (k === "day") { val = d; }
+ return Messages._getKey('calendar_str_filter_'+k, [listItems(val)]);
+ });
+ }
+
+ str += end;
+
+ return {
+ str: Messages._getKey('calendar_rec_every_date', [str]),
+ filters: filters
+ };
+ };
+
+ var getMonthlyPattern = function (date, yearly) {
+ var d = new Date(+date);
+ var day = d.getDay();
+ var monthday = d.getDate();
+
+ // Check nth day
+ var s = new Date(+d);
+ s.setDate(1);
+ while (s.getDay() !== day) { s.setDate(s.getDate() + 1); }
+ var nth = ((monthday - s.getDate()) / 7) + 1;
+
+ // Check last day
+ var m = d.getMonth();
+ d.setDate(d.getDate() + 7);
+ var last = d.getMonth() !== m;
+
+ var dayCode = Rec.DAYORDER[day];
+ var dayStr = WEEKDAYS[day];
+ var monthStr = date.toLocaleDateString(getDateLanguage(), { month: 'long' });
+
+ var key = yearly ? "yearly" : "monthly";
+ return {
+ nth: nth + dayCode,
+ str: Messages._getKey('calendar_rec_'+key+'_nth', [
+ Messages['calendar_nth_'+nth],
+ dayStr,
+ monthStr
+ ]),
+ last: last ? '-1' + dayStr : undefined,
+ lastStr: Messages._getKey('calendar_rec_'+key+'_nth', [
+ Messages['calendar_nth_last'],
+ dayStr,
+ monthStr
+ ]),
+ // Messages.calendar_rec_yearly_nth
+ // Messages.calendar_rec_monthly_nth
+ };
+ };
+
+ var getRecurrenceInput = function (date) {
+ APP.recurrenceRule = '';
+
+ var obj = APP.editModalData;
+ if (obj.id) { // Edit mode, recover recurrence data
+ var cal = obj.selectedCal.id;
+ var calData = APP.calendars[cal];
+ if (calData) {
+ var ev = APP.calendar.getSchedule(obj.id, cal);
+ APP.recurrenceRule = ev.recurrenceRule || '';
+ }
+ }
+ var updatedOn = APP.recurrenceRule && APP.recurrenceRule._next;
+ if (updatedOn) { delete APP.recurrenceRule._next; }
+ APP.wasRecurrent = Boolean(APP.recurrenceRule);
+
+// XXX TEST
+/*
+APP.recurrenceRule = {
+ freq: 'yearly',
+ interval: 2,
+ count: 30,
+ until: 1924902000000,
+ by: {
+ month: [1, 3, 5, 7, 9, 11],
+ weekno: [1, 11, 21, 31, 41, 51],
+ day: ["MO","TU","WE","TH","FR"]
+ }
+};
+*/
+
+ var basicStr = {};
+
+ var options = [{
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-recurrence',
+ 'data-value': '',
+ 'href': '#',
+ },
+ content: Messages.calendar_rec_no
+ }];
+ // Basic recurrence
+ ['daily', 'weekly', 'monthly', 'yearly'].forEach(function (rec) {
+ basicStr[rec] = JSONSortify({freq: rec});
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-recurrence',
+ 'data-value': basicStr[rec],
+ 'href': '#',
+ },
+ content: Messages._getKey('calendar_rec_' + rec, [
+ getWeekDays(true)[date.getDay()],
+ date.getDate(),
+ date.toLocaleDateString(getDateLanguage(), {month:"long", day:"2-digit"})
+ ])
+ });
+ });
+ // Weekdays / Weekend
+ var isWeekend = [0,6].includes(date.getDay());
+ var weekValue = isWeekend ? ['SA', 'SU'] : ['MO', 'TU', 'WE', 'TH', 'FR'];
+ basicStr.days = JSONSortify({
+ freq: 'daily',
+ by: { day: weekValue }
+ });
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-recurrence',
+ 'data-value': basicStr.days,
+ 'href': '#',
+ },
+ content: Messages['calendar_rec_' + (isWeekend ? 'weekend' : 'weekdays')]
+ });
+ // Custom
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-recurrence',
+ 'data-value': 'custom',
+ 'href': '#',
+ },
+ content: Messages.calendar_rec_custom
+ });
+
+ var dropdownConfig = {
+ text: Messages.calendar_rec_no,
+ options: options, // Entries displayed in the menu
+ isSelect: true,
+ common: common,
+ buttonCls: 'btn btn-secondary',
+ caretDown: true,
+ };
+ var $block = UIElements.createDropdown(dropdownConfig);
+
+ var translated = h('div.cp-calendar-rec-translated');
+ var $translated = $(translated);
+ var _addTranslation = function () {
+ $translated.empty();
+
+ // Dropdown value
+ var recStr = JSONSortify(APP.recurrenceRule);
+ var set = Object.keys(basicStr).some(function (k) {
+ if (recStr === basicStr[k]) {
+ $block.setValue(basicStr[k]);
+ $translated.empty();
+ return true;
+ }
+ });
+ if (set) { return; }
+ $block.setValue(APP.recurrenceRule ? 'custom' : '');
+
+ // Text value
+
+ var ruleObj = translate(APP.recurrenceRule);
+ if (!ruleObj || !ruleObj.str) { return; }
+ $translated.append(h('div.cp-calendar-rec-translated-str', ruleObj.str));
+
+ if (!ruleObj.filters || !Array.isArray(ruleObj.filters)
+ || !ruleObj.filters.length) { return; }
+ var toAdd = [];
+ toAdd = ruleObj.filters.map(function (str) {
+ return h('li', str);
+ });
+ $translated.append([
+ h('div', Messages.calendar_str_filter),
+ h('ul', toAdd)
+ ]);
+ };
+ var addUpdate = function () {
+ if (!updatedOn) { return; }
+ var d = new Date(updatedOn).toLocaleDateString();
+ $translated.append(h('div', Messages._getKey('calendar_rec_updated', [d])));
+ };
+ var addTranslation = function () {
+ _addTranslation();
+ addUpdate();
+ };
+
+ addTranslation();
+
+
+
+ var showCustom = function () {
+ var rec = APP.recurrenceRule || {};
+
+ var interval = h('input', {
+ type: "number",
+ min: 1,
+ max: 1000,
+ value: rec.interval || 1
+ });
+
+ var options = [];
+ ['daily', 'weekly', 'monthly', 'yearly'].forEach(function (rec) {
+ options.push({
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-calendar-recurrence-freq',
+ 'data-value': rec,
+ 'href': '#',
+ },
+ content: Messages['calendar_rec_freq_' + rec]
+ });
+ });
+ var dropdownConfig = {
+ text: Messages.calendar_rec_freq_daily,
+ options: options, // Entries displayed in the menu
+ isSelect: true,
+ common: common,
+ buttonCls: 'btn btn-secondary',
+ caretDown: true,
+ };
+ var $freq = UIElements.createDropdown(dropdownConfig);
+ $freq.setValue(rec.freq || 'daily');
+
+ var radioNo = UI.createRadio('cp-calendar-rec-until', 'cp-calendar-rec-until-no',
+ Messages.calendar_rec_until_no, !rec.until && !rec.count, {});
+ var pickr;
+ var untilDate = [
+ h('span', Messages.calendar_rec_until_date),
+ pickr = h('input', {readonly:"readonly"})
+ ];
+ var startPickr = Flatpickr(pickr, {
+ enableTime: false,
+ minDate: date,
+ //dateFormat: dateFormat,
+ onChange: function () {
+ //endPickr.set('minDate', startPickr.parseDate(s.value));
+ }
+ });
+ var endDate = new Date(+date);
+ endDate.setMonth(endDate.getMonth() + 1);
+ startPickr.setDate(rec.until ? new Date(rec.until) : endDate);
+ var radioDate = UI.createRadio('cp-calendar-rec-until', 'cp-calendar-rec-until-date',
+ untilDate, Boolean(rec.until), {input:{'data-value':'date'}});
+ var untilCount = [
+ h('span', Messages.calendar_rec_until_count),
+ h('input', {type: "number", value: (rec.count || 5), min: 2}),
+ h('span', Messages.calendar_rec_until_count2),
+ ];
+ var radioCount = UI.createRadio('cp-calendar-rec-until', 'cp-calendar-rec-until-count',
+ untilCount, Boolean(rec.count), {input:{'data-value':'count'}});
+ var untilEls = [radioNo, radioDate, radioCount];
+ $(untilEls).find('.cp-checkmark-label input').click(function () {
+ $(this).closest('.cp-radio').find('input[type="radio"]').prop('checked', true);
+ });
+
+ var repeat = h('div.cp-calendar-rec-inline', [
+ h('span', Messages.calendar_rec_txt),
+ interval,
+ $freq[0]
+ ]);
+ var until = h('div.cp-calendar-rec-block.radio-group', [
+ h('div.cp-calendar-rec-block-title', Messages.calendar_rec_until),
+ untilEls
+ ]);
+
+ var expand = h('div');
+ var $expand = $(expand);
+ var EXPAND = {};
+ EXPAND.daily = function () {};
+ EXPAND.weekly = function () {
+ $expand.attr('class', 'cp-calendar-rec-inline cp-calendar-weekly-pick');
+ var days = getWeekDays();
+ var active = (rec.by && rec.by.day) || [Rec.DAYORDER[date.getDay()]];
+ var line = [], dayCode;
+ for (var i = 1; i < 8; i++) {
+ dayCode = Rec.DAYORDER[i%7];
+ line.push(h('button.btn.no-margin.cp-calendar-pick-el' +
+ (active.includes(dayCode) ? '.btn-primary' : '.btn-default'), {
+ 'data-value': dayCode
+ }, days[i%7]));
+ }
+ $expand.append(line);
+ $expand.find('button').click(function () {
+ var $b = $(this);
+ if ($b.is('.btn-primary')) {
+ return $b.removeClass('btn-primary').addClass('btn-default');
+ }
+ $b.removeClass('btn-default').addClass('btn-primary');
+ });
+ };
+ EXPAND.monthly = function () {
+ $expand.attr('class', 'cp-calendar-rec-block radio-group');
+ // Display one or two radio options accordingly
+ var checked = (rec.by && rec.by.day) || [];
+ var pattern = getMonthlyPattern(date);
+ var radioNth = UI.createRadio('cp-calendar-rec-monthly', 'cp-calendar-rec-monthly-nth',
+ pattern.str, checked.includes(pattern.nth),
+ {input:{'data-value':pattern.nth }});
+ $expand.append(radioNth);
+
+ if (pattern.last) {
+ var radioLast = UI.createRadio('cp-calendar-rec-monthly', 'cp-calendar-rec-monthly-last',
+ pattern.lastStr, checked.includes(pattern.last),
+ {input:{ 'data-value': pattern.last }});
+ $expand.append(radioLast);
+ }
+
+ var active = (rec.by && rec.by.monthday) || [date.getDate()];
+ var lines = [], l, n;
+ for (var i = 0; i < 5; i++) {
+ l = [];
+ for (var j = 1; j < 8; j++) {
+ n = i * 7 + j;
+ if (n > 31) {
+ l.push(h('button.btn.no-margin.cp-calendar-pick-el.lastday' +
+ (active.includes(-1) ? '.btn-primary' : '.btn-default'), {
+ 'data-value': -1
+ }, Messages.calendar_month_last));
+ break;
+ }
+ l.push(h('button.btn.no-margin.cp-calendar-pick-el' +
+ (active.includes(n) ? '.btn-primary' : '.btn-default'), {
+ 'data-value': n
+ }, n));
+ }
+ lines[i] = h('div', l);
+ }
+
+ var pickr = h('div.cp-calendar-monthly-pick', lines);
+ $(pickr).find('button').click(function () {
+ var $b = $(this);
+ if ($b.is('.btn-primary')) {
+ return $b.removeClass('btn-primary').addClass('btn-default');
+ }
+ $b.removeClass('btn-default').addClass('btn-primary');
+ });
+ var radioPickContent = [
+ h('span', Messages.calendar_rec_monthly_pick),
+ pickr
+ ];
+ var radioPick = UI.createRadio('cp-calendar-rec-monthly', 'cp-calendar-rec-monthly-pick',
+ radioPickContent, !checked.length, {input:{'data-value':'pick'}});
+ $expand.append(radioPick);
+
+ $expand.find('.cp-checkmark-label button').click(function () {
+ $(this).closest('.cp-radio').find('input[type="radio"]').prop('checked', true);
+ });
+ };
+ EXPAND.yearly = function () {
+ $expand.attr('class', 'cp-calendar-rec-block radio-group');
+
+ var checked = (rec.by && rec.by.day) || [];
+
+ var radioDate = UI.createRadio('cp-calendar-rec-yearly',
+ 'cp-calendar-rec-yearly-date',
+ Messages._getKey('calendar_rec_every_date', [
+ date.toLocaleDateString(getDateLanguage(), { month: 'long', day: 'numeric'})
+ ]), !checked.length, { 'data-value': '' });
+ $expand.append(radioDate);
+
+ var pattern = getMonthlyPattern(date, true);
+ var radioNth = UI.createRadio('cp-calendar-rec-yearly', 'cp-calendar-rec-yearly-nth',
+ pattern.str, checked.includes(pattern.nth),
+ {input:{ 'data-value': pattern.nth }});
+ $expand.append(radioNth);
+
+ if (pattern.last) {
+ var radioLast = UI.createRadio('cp-calendar-rec-yearly', 'cp-calendar-rec-yearly-last',
+ pattern.lastStr, checked.includes(pattern.last),
+ {input:{ 'data-value': pattern.last }});
+ $expand.append(radioLast);
+ }
+ };
+ EXPAND[rec.freq || "daily"]();
+
+ var currentFreq = rec.freq || 'daily';
+ $freq.onChange.reg(function (prettyVal, val) {
+ if (val === currentFreq || !val) { return; }
+ currentFreq = val;
+ rec = {};
+ $expand.empty();
+ EXPAND[val]();
+ });
+
+
+ var content = [repeat, expand, until];
+
+ var $modal;
+ var modal = UI.dialog.customModal(content, {
+ buttons: [{
+ className: 'cancel',
+ name: Messages.cancel,
+ onClick: function () {
+ if (!APP.recurrenceRule) { $block.setValue(''); }
+ },
+ keys: [27]
+ }, {
+ className: 'primary',
+ name: Messages.settings_save,
+ onClick: function () {
+ var freq = $freq.getValue();
+
+ var rec = APP.recurrenceRule = {
+ freq: freq,
+ interval: Number($(interval).val()) || 1,
+ by: {}
+ };
+ if (rec.interval === 1) { delete rec.interval; }
+
+ var until = $modal.find('input[name="cp-calendar-rec-until"]:checked').data('value');
+ if (until === "count") {
+ rec.count = $(radioCount).find('input[type="number"]').val();
+ } else if (until === "date") {
+ var _date = Flatpickr.parseDate(pickr.value);
+ _date.setDate(_date.getDate()+1);
+ rec.until = +_date - 1;
+ }
+
+ if (freq === "weekly") {
+ rec.by.day = [];
+ $expand.find('button.cp-calendar-pick-el.btn-primary')
+ .each(function (i, el) {
+ rec.by.day.push($(el).data('value'));
+ });
+ if (!rec.by.day.length) { delete rec.by.day; }
+ }
+
+ if (freq === "monthly") {
+ var _m = $expand.find('input[name="cp-calendar-rec-monthly"]:checked').data('value');
+ if (_m === "pick") {
+ rec.by.monthday = [];
+ $expand.find('div.cp-calendar-monthly-pick button.btn-primary')
+ .each(function (i, el) {
+ rec.by.monthday.push($(el).data('value'));
+ });
+ if (!rec.by.monthday.length) { delete rec.by.monthday; }
+ } else {
+ rec.by.day = [_m];
+ }
+ }
+
+ if (freq === "yearly") {
+ var _y = $expand.find('input[name="cp-calendar-rec-yearly"]:checked').data('value');
+ if (_y) {
+ rec.by.month = [date.getMonth()+1];
+ rec.by.day = [_y];
+ }
+ }
+
+ if (!Object.keys(rec.by).length) { delete rec.by; }
+
+ addTranslation();
+ },
+ keys: [13]
+ }]
+ });
+ $modal = $(modal);
+ UI.openCustomModal(modal);
+ $modal.closest('.alertify').on('mousedown', function (e) {
+ e.stopPropagation();
+ });
+ };
+
+ $block.onChange.reg(function(name, val) {
+ if (val === "custom") { return void showCustom(); }
+ APP.recurrenceRule = val;
+ addTranslation();
+ });
+
+ return h('div.cp-calendar-recurrence-container', [
+ h('span.cp-recurrence-label', [
+ h('i.fa.fa-repeat', {'aria-hidden':'true'}),
+ Messages.calendar_rec]),
+ $block[0],
+ translated
+ ]);
+ };
+
var parseNotif = function (minutes) {
var res = {
unit: 'minutes',
@@ -1020,7 +1828,13 @@ define([
var ev = APP.editModalData;
var calId = ev.selectedCal.id;
// DEFAULT HERE [10] ==> 10 minutes before the event
- var oldReminders = Util.find(APP.calendars, [calId, 'content', 'content', ev.id, 'reminders']) || [10];
+ var id = (ev.id && ev.id.split('|')[0]) || undefined;
+ var _ev = APP.calendar.getSchedule(ev.id, calId);
+ var oldReminders = _ev && _ev.raw && _ev.raw.reminders;
+ if (!oldReminders) {
+ oldReminders = Util.find(APP.calendars, [calId, 'content', 'content', id, 'reminders']) || [60];
+ }
+
APP.notificationsEntries = [];
var number = h('input.tui-full-calendar-content', {
type: "number",
@@ -1064,13 +1878,19 @@ define([
var addNotif = h('button.btn.btn-primary-outline.fa.fa-plus');
var $list = $(h('div.cp-calendar-notif-list'));
var listContainer = h('div.cp-calendar-notif-list-container', [
- h('span.cp-notif-label', Messages.calendar_notifications),
+ h('span.cp-notif-label', [
+ h('i.fa.fa-bell', {'aria-hidden':'true'}),
+ Messages.calendar_notifications
+ ]),
$list[0],
h('span.cp-notif-empty', Messages.calendar_noNotification)
]);
+
var addNotification = function (unit, value) {
var unitValue = (unit === "minutes") ? 1 : (unit === "hours" ? 60 : (60*24));
- var del = h('button.btn.btn-danger-outline.small.fa.fa-times');
+ var del = h('button.btn.btn-danger-outline.small.fa.fa-times',
+ {'title': Messages.calendar_removeNotification}
+ );
var minutes = value * unitValue;
if ($list.find('[data-minutes="'+minutes+'"]').length) { return; }
var span = h('span.cp-notif-entry', {
@@ -1182,6 +2002,14 @@ define([
$el.find('.tui-full-calendar-dropdown-button').addClass('btn btn-secondary');
$el.find('.tui-full-calendar-popup-close').addClass('btn btn-cancel fa fa-times cp-calendar-close').empty();
+ var $container = $el.closest('.tui-full-calendar-floating-layer');
+ $container.addClass('cp-calendar-popup-flex');
+ $container.css('display', 'flex').mousedown(function (e) {
+ if ($(e.target).is('.cp-calendar-popup-flex')) {
+ $el.find('.tui-full-calendar-popup-close').click();
+ }
+ });
+
var calendars = APP.calendars || {};
var show = false;
$el.find('.tui-full-calendar-dropdown-menu li').each(function (i, li) {
@@ -1203,31 +2031,33 @@ define([
if (!isUpdate) { $el.find('.tui-full-calendar-dropdown-menu li').first().click(); }
var $button = $el.find('.tui-full-calendar-section-button-save');
+
+ var $startDate = $el.find('#tui-full-calendar-schedule-start-date');
+ var startDate = Flatpickr.parseDate($startDate.val());
+
+ var divRec = getRecurrenceInput(startDate);
+ $button.before(divRec);
+
var div = getNotificationDropdown();
$button.before(div);
+ // Use Flatpickr with or without time depending on allday checkbox
var $cbox = $el.find('#tui-full-calendar-schedule-allday');
- var $start = $el.find('.tui-full-calendar-section-start-date');
- var $dash = $el.find('.tui-full-calendar-section-date-dash');
- var $end = $el.find('.tui-full-calendar-section-end-date');
var allDay = $cbox.is(':checked');
- if (allDay) {
- $start.hide();
- $dash.hide();
- $end.hide();
- }
+ var allDayFormat = 'Y-m-d';
+ var timeFormat = '';
+ var setFormat = function (allDay) {
+ var s = window.CP_startPickr;
+ var e = window.CP_endPickr;
+ if (!timeFormat) { timeFormat = s.config.dateFormat; }
+ s.set('dateFormat', allDay ? allDayFormat : timeFormat);
+ e.set('dateFormat', allDay ? allDayFormat : timeFormat);
+ };
+ setFormat(allDay);
$el.find('.tui-full-calendar-section-allday').click(function () {
setTimeout(function () {
var allDay = $cbox.is(':checked');
- if (allDay) {
- $start.hide();
- $dash.hide();
- $end.hide();
- return;
- }
- $start.show();
- $dash.show();
- $end.show();
+ setFormat(allDay);
});
});
};
@@ -1235,9 +2065,65 @@ define([
var $el = $(el);
$el.find('.tui-full-calendar-popup-edit').addClass('btn btn-primary');
$el.find('.tui-full-calendar-popup-edit .tui-full-calendar-icon').addClass('fa fa-pencil').removeClass('tui-full-calendar-icon');
- $el.find('.tui-full-calendar-popup-delete').addClass('btn btn-danger');
- $el.find('.tui-full-calendar-popup-delete .tui-full-calendar-icon').addClass('fa fa-trash').removeClass('tui-full-calendar-icon');
$el.find('.tui-full-calendar-content').removeClass('tui-full-calendar-content');
+
+ var delButton = h('button.btn.btn-danger', [
+ h('i.fa.fa-trash'),
+ h('span', Messages.kanban_delete)
+ ]);
+ var $del = $el.find('.tui-full-calendar-popup-delete').hide();
+ $del.after(delButton);
+ UI.confirmButton(delButton, {
+ classes: 'danger'
+ }, function () {
+ $del.click();
+ });
+ var $section = $el.find('.tui-full-calendar-section-button');
+ var ev = APP.editModalData;
+ var data = ev.schedule || {};
+ var id = data.id;
+ if (!id) { return; }
+ if (id.indexOf('|') === -1) { return; } // Original event ID doesn't contain |
+
+ if (APP.nextLocationUid) {
+ var uid = APP.nextLocationUid;
+ delete APP.nextLocationUid;
+ var $a = $el.find('#'+uid);
+ $a.click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ common.openUnsafeURL($a.attr('href'));
+ });
+ }
+ // This is a recurring event, add button to stop recurrence now
+ var $b = $(h('button.btn.btn-default', [
+ h('i.fa.fa-times'),
+ h('span', Messages.calendar_rec_stop)
+ ])).insertBefore($section);
+ UI.confirmButton($b[0], { classes: 'btn-default' }, function () {
+ var originalId = id.split('|')[0];
+ var originalEvent = Util.find(APP.calendars,
+ [ev.schedule.calendarId, 'content', 'content', originalId]);
+ var rec = originalEvent.recurrenceRule;
+ if (!rec) { return; }
+ rec.until = (ev.schedule.raw && ev.schedule.raw.start) - 1;
+ data.id = originalId;
+ updateEvent({
+ ev: data,
+ changes: {
+ recurrenceRule: rec
+ },
+ type: {
+ which: 'all'
+ }
+ }, function (err) {
+ if (err) {
+ console.error(err);
+ return void UI.warn(err);
+ }
+ $b.closest('.tui-full-calendar-floating-layer').hide();
+ });
+ });
};
var onPopupRemoved = function () {
var start, end;
@@ -1313,6 +2199,8 @@ define([
}, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
+ } else if (privateData.calendarOpts) {
+ APP.initTime = privateData.calendarOpts.time;
}
store.get('calendarView', makeCalendar);
UI.removeLoadingScreen();
diff --git a/www/calendar/main.js b/www/calendar/main.js
index 11b1ba4bf..6f8e362f7 100644
--- a/www/calendar/main.js
+++ b/www/calendar/main.js
@@ -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({
diff --git a/www/calendar/recurrence.js b/www/calendar/recurrence.js
new file mode 100644
index 000000000..bd3fa0519
--- /dev/null
+++ b/www/calendar/recurrence.js
@@ -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;
+});
diff --git a/www/checkup/main.js b/www/checkup/main.js
index b69c5441e..05049562b 100644
--- a/www/checkup/main.js
+++ b/www/checkup/main.js
@@ -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', [
diff --git a/www/code/inner.js b/www/code/inner.js
index c1dac2df0..562e08556 100644
--- a/www/code/inner.js
+++ b/www/code/inner.js
@@ -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', `