Merge branch 'rrule' into staging

This commit is contained in:
David Benque 2022-10-21 15:43:24 +01:00
commit b2022f5e73
17 changed files with 2677 additions and 187 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

@ -481,6 +481,18 @@ define([
var missed = content.msg.missed;
var start = msg.start;
var title = Util.fixHTML(msg.title);
content.handler = function () {
var priv = common.getMetadataMgr().getPrivateData();
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
return void window.APP.moveToDate(time);
}
var url = Hash.hashToHref('', 'calendar');
var optsUrl = Hash.getNewPadURL(url, {
time: time
});
common.openURL(optsUrl);
};
content.getFormatText = function () {
var now = +new Date();

View File

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

View File

@ -177,6 +177,15 @@ proxy.mailboxes = {
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
return clientId !== cId;
}));
var uid = hash.slice(9).split('-')[0];
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
if (!d) {
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
d = h[uid] = h[uid] || [];
}
var delay = hash.split('-')[1];
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
return;
}
@ -590,6 +599,9 @@ proxy.mailboxes = {
});
};
mailbox.hideMessage = function (type, msg) {
hideMessage(ctx, type, msg.hash, ctx.clients);
};
mailbox.showMessage = function (type, msg, cId, cb) {
if (type === "reminders" && msg) {
ctx.boxes.reminders.content[msg.hash] = msg.msg;

View File

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

View File

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

View File

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