From d8eebe4e9c2e6ab89155ed68d4ac6267688025dd Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 26 Apr 2021 13:23:59 +0200 Subject: [PATCH 01/27] Never store invalid OO checkpoints --- www/common/onlyoffice/inner.js | 12 ++++++++++++ www/common/sframe-common-outer.js | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 9c42951b6..44e4f0f05 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -454,6 +454,18 @@ define([ var saveToServer = function () { var text = getContent(); + text = undefined; + if (!text) { + setEditable(false, true); + sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ + 'chainpad', + content.channel, + ], function () {}); + UI.alert(Messages.realtime_unrecoverableError, function () { + common.gotoURL(); + }); + return; + } var blob = new Blob([text], {type: 'plain/text'}); var file = getFileType(); blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index dde533e9f..7190fb997 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1533,6 +1533,16 @@ define([ sframeChan.on('Q_CLEAR_CACHE', function (data, cb) { Utils.Cache.clear(cb); }); + sframeChan.on('Q_CLEAR_CACHE_CHANNELS', function (channels, cb) { + if (!Array.isArray(channels)) { return void cb({error: "NOT_AN_ARRAY"}); } + nThen(function (waitFor) { + channels.forEach(function (chan) { + if (chan === "chainpad") { chan = secret.channel; } + console.error(chan); + Utils.Cache.clearChannel(chan, waitFor()); + }); + }).nThen(cb); + }); sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) { Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) { From d11ba24c1ed16a89219e07159615805713fa2301 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 26 Apr 2021 14:25:06 +0200 Subject: [PATCH 02/27] Fix missing calendars for newly created or joined teams --- www/calendar/inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 152acde99..2e01df54f 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -686,8 +686,8 @@ Messages.calendar_noNotification = "None"; var filter = function (teamId) { return Object.keys(APP.calendars || {}).filter(function (id) { var cal = APP.calendars[id] || {}; - var teams = cal.teams || []; - return teams.indexOf(typeof(teamId) !== "undefined" ? teamId : 1) !== -1; + var teams = (cal.teams || []).map(function (tId) { return Number(tId); }); + return teams.indexOf(typeof(teamId) !== "undefined" ? Number(teamId) : 1) !== -1; }); }; var tempCalendars = filter(0); From 74ed15d4fad7ab8caa88d814b0e7febd87562d20 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 12:31:13 +0200 Subject: [PATCH 03/27] Calendar mobile UI --- www/calendar/app-calendar.less | 42 +++++++++++++++++++++++++++++++ www/calendar/inner.js | 45 +++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/www/calendar/app-calendar.less b/www/calendar/app-calendar.less index 88869667f..1ec6c295b 100644 --- a/www/calendar/app-calendar.less +++ b/www/calendar/app-calendar.less @@ -11,6 +11,10 @@ display: flex; flex-flow: column; + .cp-toolbar-bottom-mid > div { + .cp-small { display: none; } + } + #cp-sidebarlayout-container #cp-sidebarlayout-rightside { padding: 0; & > div { @@ -385,5 +389,43 @@ cursor: pointer; border: 1px solid @cp_forms-border; } + + @media (max-width: @browser_media-medium-screen) { + .cp-calendar-newevent { + i { + margin: 0 !important; + } + span { + display: none; + } + } + .tui-full-calendar-dayname-leftmargin, .tui-full-calendar-timegrid-right { + margin-left: 40px !important; + } + .tui-full-calendar-allday-left, .tui-full-calendar-timegrid-left { + width: 40px !important; + } + .tui-full-calendar-dayname > span { + display: flex; + flex-flow: column; + line-height: 0; + justify-content: center; + align-items: center; + height: 100%; + } + .tui-full-calendar-dayname * { + font-size: 11px; + line-height: initial; + height: auto; + } + .cp-toolbar-bottom-mid > div { + :not(:first-child) { + display: none; + } + :first-child { + display: inline-block; + } + } + } } diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 2e01df54f..865fe0a6e 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -78,6 +78,7 @@ Messages.calendar_newEvent = "New event"; Messages.calendar_new = "New calendar"; Messages.calendar_dateRange = "{0} - {1}"; Messages.calendar_dateTimeRange = "{0} {1} - {2}"; +Messages.calendar_weekNumber = "Week {0}"; Messages.calendar_update = "Update"; Messages.calendar_title = "Title"; Messages.calendar_loc = "Location"; @@ -153,13 +154,16 @@ Messages.calendar_noNotification = "None"; return (brightness > 125) ? '#424242' : '#EEEEEE'; }; - var getWeekDays = function () { + var getWeekDays = function (large) { var baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday var weekDays = []; for(var i = 0; i < 7; i++) { weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' })); baseDate.setDate(baseDate.getDate() + 1); } + if (!large) { + weekDays = weekDays.map(function (day) { return day.slice(0,3); }); + } return weekDays.map(function (day) { return day.replace(/^./, function (str) { return str.toUpperCase(); }); }); }; @@ -754,11 +758,26 @@ Messages.calendar_noNotification = "None"; onCalendarsUpdate.fire(); }; + + 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), @@ -791,6 +810,7 @@ Messages.calendar_noNotification = "None"; h('div#cp-sidebarlayout-rightside') ]); + var large = $(window).width() >= 800; var cal = APP.calendar = new Calendar('#cp-sidebarlayout-rightside', { defaultView: view || 'week', // weekly view option taskView: false, @@ -800,15 +820,32 @@ Messages.calendar_noNotification = "None"; calendars: getCalendars(), template: templates, month: { - daynames: getWeekDays(), + daynames: getWeekDays(large), startDayOfWeek: 1, }, week: { - daynames: getWeekDays(), + daynames: getWeekDays(large), startDayOfWeek: 1, } }); + $(window).on('resize', function () { + var _large = $(window).width() >= 800; + if (large !== _large) { + large = _large; + cal.setOptions({ + month: { + daynames: getWeekDays(_large), + startDayOfWeek: 1, + }, + week: { + daynames: getWeekDays(_large), + startDayOfWeek: 1, + } + }); + } + }); + makeLeftside(cal, $(leftside)); cal.on('beforeCreateSchedule', function(event) { @@ -925,7 +962,7 @@ Messages.calendar_noNotification = "None"; APP.toolbar.$bottomR.append($block); // New event button - var newEventBtn = h('button', [ + var newEventBtn = h('button.cp-calendar-newevent', [ h('i.fa.fa-plus'), h('span', Messages.calendar_newEvent) ]); From 0c3d4b0e4977efdc0d5b0d41c7dcbcb653e16263 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:19:24 +0200 Subject: [PATCH 04/27] Remove debugging data --- www/common/onlyoffice/inner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 44e4f0f05..a1add1497 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -454,7 +454,6 @@ define([ var saveToServer = function () { var text = getContent(); - text = undefined; if (!text) { setEditable(false, true); sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ From ffe9f4db0f83f834d7935d94bb06a403544a69be Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:54:19 +0200 Subject: [PATCH 05/27] Improve UX when maxUploadSize is reached in sheets --- www/common/onlyoffice/inner.js | 27 ++++++++++++++++++++++++++- www/common/sframe-common-file.js | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index a1add1497..fa0e28755 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -356,10 +356,32 @@ define([ } }; + Messages.oo_cantMigrate = "This sheet exceeds the maximum upload size and is too large to be migrated."; // XXX var onUploaded = function (ev, data, err) { content.saveLock = undefined; if (err) { console.error(err); + if (content.saveLock === myOOId) { delete content.saveLock; } // Unlock checkpoints + if (APP.migrateModal) { + try { getEditor().asc_setRestriction(true); } catch (e) {} + setEditable(true); + delete content.migration; + APP.migrateModal.closeModal(); + APP.onLocal(); + } + if (isLockedModal.modal && err === "TOO_LARGE") { + if (APP.migrate) { + UI.warn(Messages.oo_cantMigrate); + } + APP.cantCheckpoint = true; + isLockedModal.modal.closeModal(); + delete isLockedModal.modal; + if (content.saveLock === myOOId) { + delete content.saveLock; + } + APP.onLocal(); + return; + } return void UI.alert(Messages.oo_saveError); } // Get the last cp idx @@ -453,6 +475,7 @@ define([ }; var saveToServer = function () { + if (APP.cantCheckpoint) { return; } // TOO_LARGE var text = getContent(); if (!text) { setEditable(false, true); @@ -490,6 +513,8 @@ define([ var noLogin = false; var makeCheckpoint = function (force) { + if (APP.cantCheckpoint) { return; } // TOO_LARGE + var locked = content.saveLock; var lastCp = getLastCp(); @@ -1385,7 +1410,7 @@ define([ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_sheetMigration_loading) ]); - UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); + APP.migrateModal = UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); makeCheckpoint(true); }); // DEPRECATED: from version 3, the queue is sent again during init diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index c7a66adb9..2a6f4fea3 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -133,6 +133,7 @@ define([ $pv.text(Messages.error); queue.inProgress = false; queue.next(); + if (config.onError) { config.onError("TOO_LARGE"); } return void UI.alert(Messages._getKey('upload_tooLargeBrief', [maxSizeStr])); } From 53358b97257d41bd3ce2f9f23113fa7a878610fa Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:55:03 +0200 Subject: [PATCH 06/27] lint compliance --- www/common/onlyoffice/inner.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index fa0e28755..120b07e46 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -356,6 +356,14 @@ define([ } }; + // Add a lock + var isLockedModal = { + content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [ + h('span.fa.fa-spin.fa-spinner'), + h('span', Messages.oo_isLocked) + ])) + }; + Messages.oo_cantMigrate = "This sheet exceeds the maximum upload size and is too large to be migrated."; // XXX var onUploaded = function (ev, data, err) { content.saveLock = undefined; @@ -444,14 +452,6 @@ define([ }; APP.FM = common.createFileManager(fmConfig); - // Add a lock - var isLockedModal = { - content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [ - h('span.fa.fa-spin.fa-spinner'), - h('span', Messages.oo_isLocked) - ])) - }; - var resetData = function (blob, type) { // If a read-only refresh popup was planned, abort it delete APP.refreshPopup; From 4b5477d87bc29b93f688f768d6f2632865482c96 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 27 Apr 2021 13:58:13 +0200 Subject: [PATCH 07/27] Translated using Weblate (English) Currently translated at 100.0% (1185 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index c0ecbb163..18c6feb8a 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1184,5 +1184,6 @@ "broadcast_newCustom": "Message from the administrators", "settings_deleteWarning": "Warning: you are currently subscribed to a premium plan (paid or given by another user). Please cancel your plan before deleting your account as will not be possible without contacting support once your account is deleted.", "settings_deleteContinue": "Delete my account", - "settings_deleteSubscription": "Manage my subscription" + "settings_deleteSubscription": "Manage my subscription", + "footer_roadmap": "Roadmap" } From 700b2f24e010df6799ed34745cd0ed4f2705d7ca Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 14:50:33 +0200 Subject: [PATCH 08/27] Don't pull jkanban.css --- www/kanban/inner.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 3428bd12b..126ffd000 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -30,10 +30,6 @@ define([ 'css!/bower_components/codemirror/lib/codemirror.css', 'css!/bower_components/codemirror/addon/dialog/dialog.css', 'css!/bower_components/codemirror/addon/fold/foldgutter.css', - - - - 'css!/kanban/jkanban.css', 'less!/kanban/app-kanban.less' ], function ( $, From a0cf3eba40697238c5afea1b25ec4bf25f116f11 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 14:51:11 +0200 Subject: [PATCH 09/27] Prevent parse errors in sframe-common-outer --- www/common/sframe-common-outer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 7190fb997..d24f62f9d 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -44,8 +44,8 @@ define([ // loading screen setup. var done = waitFor(); var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } + var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data; + if (!data || data.q !== 'READY') { return; } window.removeEventListener('message', onMsg); var _done = done; done = function () { }; @@ -182,8 +182,8 @@ define([ }; var whenReady = waitFor(function (msg) { if (msg.source !== iframe) { return; } - var data = JSON.parse(msg.data); - if (!data.txid) { return; } + var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data; + if (!data || !data.txid) { return; } // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data From b1cab1df98493198935140e69ecac7a28712c443 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 15:53:39 +0200 Subject: [PATCH 10/27] Move diffDOM --- www/common/diffMarked.js | 2 +- www/lib/diff-dom/diffDOM.js | 1356 +++++++++++++++++++++++++++++++++++ www/pad/inner.js | 2 +- www/poll/render.js | 2 +- 4 files changed, 1359 insertions(+), 3 deletions(-) create mode 100755 www/lib/diff-dom/diffDOM.js diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 71b2185ba..f2fb97f24 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -9,7 +9,7 @@ define([ '/common/media-tag.js', '/customize/messages.js', '/common/highlight/highlight.pack.js', - '/bower_components/diff-dom/diffDOM.js', + '/lib/diff-dom/diffDOM.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/common/highlight/styles/'+ (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css') ],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Messages) { diff --git a/www/lib/diff-dom/diffDOM.js b/www/lib/diff-dom/diffDOM.js new file mode 100755 index 000000000..0a2f22db6 --- /dev/null +++ b/www/lib/diff-dom/diffDOM.js @@ -0,0 +1,1356 @@ +(function() { + "use strict"; + + var diffcount; + + var Diff = function (options) { + var diff = this; + Object.keys(options).forEach(function(option) { + diff[option] = options[option]; + }); + }; + + Diff.prototype = { + toString: function() { + return JSON.stringify(this); + } + + // TODO: compress diff output by replacing these keys with numbers or alike: + /* 'addAttribute' = 0, + 'modifyAttribute' = 1, + 'removeAttribute' = 2, + 'modifyTextElement' = 3, + 'relocateGroup' = 4, + 'removeElement' = 5, + 'addElement' = 6, + 'removeTextElement' = 7, + 'addTextElement' = 8, + 'replaceElement' = 9, + 'modifyValue' = 10, + 'modifyChecked' = 11, + 'modifySelected' = 12, + 'modifyComment' = 13, + 'action' = 14, + 'route' = 15, + 'oldValue' = 16, + 'newValue' = 17, + 'element' = 18, + 'group' = 19, + 'from' = 20, + 'to' = 21, + 'name' = 22, + 'value' = 23, + 'data' = 24, + 'attributes' = 25, + 'nodeName' = 26, + 'childNodes' = 27, + 'checked' = 28, + 'selected' = 29;*/ + }; + + var SubsetMapping = function SubsetMapping(a, b) { + this.oldValue = a; + this.newValue = b; + }; + + SubsetMapping.prototype = { + contains: function contains(subset) { + if (subset.length < this.length) { + return subset.newValue >= this.newValue && subset.newValue < this.newValue + this.length; + } + return false; + }, + toString: function toString() { + return this.length + " element subset, first mapping: old " + this.oldValue + " → new " + this.newValue; + } + }; + + var elementDescriptors = function(el) { + var output = []; + if (el.nodeName !== '#text' && el.nodeName !== '#comment') { + output.push(el.nodeName); + if (el.attributes) { + if (el.attributes['class']) { + output.push(el.nodeName + '.' + el.attributes['class'].replace(/ /g, '.')); + } + if (el.attributes.id) { + output.push(el.nodeName + '#' + el.attributes.id); + } + } + + } + return output; + }; + + var findUniqueDescriptors = function(li) { + var uniqueDescriptors = {}, + duplicateDescriptors = {}; + + li.forEach(function(node) { + elementDescriptors(node).forEach(function(descriptor) { + var inUnique = descriptor in uniqueDescriptors, + inDupes = descriptor in duplicateDescriptors; + if (!inUnique && !inDupes) { + uniqueDescriptors[descriptor] = true; + } else if (inUnique) { + delete uniqueDescriptors[descriptor]; + duplicateDescriptors[descriptor] = true; + } + }); + + }); + + return uniqueDescriptors; + }; + + var uniqueInBoth = function(l1, l2) { + var l1Unique = findUniqueDescriptors(l1), + l2Unique = findUniqueDescriptors(l2), + inBoth = {}; + + Object.keys(l1Unique).forEach(function(key) { + if (l2Unique[key]) { + inBoth[key] = true; + } + }); + + return inBoth; + }; + + var removeDone = function(tree) { + delete tree.outerDone; + delete tree.innerDone; + delete tree.valueDone; + if (tree.childNodes) { + return tree.childNodes.every(removeDone); + } else { + return true; + } + }; + + var isEqual = function(e1, e2) { + + var e1Attributes, e2Attributes; + + if (!['nodeName', 'value', 'checked', 'selected', 'data'].every(function(element) { + if (e1[element] !== e2[element]) { + return false; + } + return true; + })) { + return false; + } + + if (Boolean(e1.attributes) !== Boolean(e2.attributes)) { + return false; + } + + if (Boolean(e1.childNodes) !== Boolean(e2.childNodes)) { + return false; + } + + if (e1.attributes) { + e1Attributes = Object.keys(e1.attributes); + e2Attributes = Object.keys(e2.attributes); + + if (e1Attributes.length !== e2Attributes.length) { + return false; + } + if (!e1Attributes.every(function(attribute) { + if (e1.attributes[attribute] !== e2.attributes[attribute]) { + return false; + } + })) { + return false; + } + } + + if (e1.childNodes) { + if (e1.childNodes.length !== e2.childNodes.length) { + return false; + } + if (!e1.childNodes.every(function(childNode, index) { + return isEqual(childNode, e2.childNodes[index]); + })) { + + return false; + } + + } + + return true; + + }; + + + var roughlyEqual = function(e1, e2, uniqueDescriptors, sameSiblings, preventRecursion) { + var childUniqueDescriptors, nodeList1, nodeList2; + + if (!e1 || !e2) { + return false; + } + + if (e1.nodeName !== e2.nodeName) { + return false; + } + + if (e1.nodeName === '#text') { + // Note that we initially don't care what the text content of a node is, + // the mere fact that it's the same tag and "has text" means it's roughly + // equal, and then we can find out the true text difference later. + return preventRecursion ? true : e1.data === e2.data; + } + + + if (e1.nodeName in uniqueDescriptors) { + return true; + } + + if (e1.attributes && e2.attributes) { + + if (e1.attributes.id && e1.attributes.id === e2.attributes.id) { + var idDescriptor = e1.nodeName + '#' + e1.attributes.id; + if (idDescriptor in uniqueDescriptors) { + return true; + } + } + if (e1.attributes['class'] && e1.attributes['class'] === e2.attributes['class']) { + var classDescriptor = e1.nodeName + '.' + e1.attributes['class'].replace(/ /g, '.'); + if (classDescriptor in uniqueDescriptors) { + return true; + } + } + } + + if (sameSiblings) { + return true; + } + + nodeList1 = e1.childNodes ? e1.childNodes.slice().reverse() : []; + nodeList2 = e2.childNodes ? e2.childNodes.slice().reverse() : []; + + if (nodeList1.length !== nodeList2.length) { + return false; + } + + if (preventRecursion) { + return nodeList1.every(function(element, index) { + return element.nodeName === nodeList2[index].nodeName; + }); + } else { + // note: we only allow one level of recursion at any depth. If 'preventRecursion' + // was not set, we must explicitly force it to true for child iterations. + childUniqueDescriptors = uniqueInBoth(nodeList1, nodeList2); + return nodeList1.every(function(element, index) { + return roughlyEqual(element, nodeList2[index], childUniqueDescriptors, true, true); + }); + } + }; + + + var cloneObj = function(obj) { + // TODO: Do we really need to clone here? Is it not enough to just return the original object? + return JSON.parse(JSON.stringify(obj)); + //return obj; + }; + + /** + * based on https://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Longest_common_substring#JavaScript + */ + var findCommonSubsets = function(c1, c2, marked1, marked2) { + var lcsSize = 0, + index = [], + matches = Array.apply(null, new Array(c1.length + 1)).map(function() { + return []; + }), // set up the matching table + uniqueDescriptors = uniqueInBoth(c1, c2), + // If all of the elements are the same tag, id and class, then we can + // consider them roughly the same even if they have a different number of + // children. This will reduce removing and re-adding similar elements. + subsetsSame = c1.length === c2.length, + origin, ret; + + if (subsetsSame) { + + c1.some(function(element, i) { + var c1Desc = elementDescriptors(element), + c2Desc = elementDescriptors(c2[i]); + if (c1Desc.length !== c2Desc.length) { + subsetsSame = false; + return true; + } + c1Desc.some(function(description, i) { + if (description !== c2Desc[i]) { + subsetsSame = false; + return true; + } + }); + if (!subsetsSame) { + return true; + } + + }); + } + + // fill the matches with distance values + c1.forEach(function(c1Element, c1Index) { + c2.forEach(function(c2Element, c2Index) { + if (!marked1[c1Index] && !marked2[c2Index] && roughlyEqual(c1Element, c2Element, uniqueDescriptors, subsetsSame)) { + matches[c1Index + 1][c2Index + 1] = (matches[c1Index][c2Index] ? matches[c1Index][c2Index] + 1 : 1); + if (matches[c1Index + 1][c2Index + 1] >= lcsSize) { + lcsSize = matches[c1Index + 1][c2Index + 1]; + index = [c1Index + 1, c2Index + 1]; + } + } else { + matches[c1Index + 1][c2Index + 1] = 0; + } + }); + }); + if (lcsSize === 0) { + return false; + } + origin = [index[0] - lcsSize, index[1] - lcsSize]; + ret = new SubsetMapping(origin[0], origin[1]); + ret.length = lcsSize; + + return ret; + }; + + /** + * This should really be a predefined function in Array... + */ + var makeArray = function(n, v) { + return Array.apply(null, new Array(n)).map(function() { + return v; + }); + }; + + /** + * Generate arrays that indicate which node belongs to which subset, + * or whether it's actually an orphan node, existing in only one + * of the two trees, rather than somewhere in both. + * + * So if t1 =
, t2 =
. + * The longest subset is "
" (length 2), so it will group 0. + * The second longest is "" (length 1), so it will be group 1. + * gaps1 will therefore be [1,0,0] and gaps2 [0,0,1]. + * + * If an element is not part of any group, it will stay being 'true', which + * is the initial value. For example: + * t1 =


, t2 =
+ * + * The "

" and "" do only show up in one of the two and will + * therefore be marked by "true". The remaining parts are parts of the + * groups 0 and 1: + * gaps1 = [1, true, 0, 0], gaps2 = [true, 0, 0, 1] + * + */ + var getGapInformation = function(t1, t2, stable) { + + var gaps1 = t1.childNodes ? makeArray(t1.childNodes.length, true) : [], + gaps2 = t2.childNodes ? makeArray(t2.childNodes.length, true) : [], + group = 0; + + // give elements from the same subset the same group number + stable.forEach(function(subset) { + var i, endOld = subset.oldValue + subset.length, + endNew = subset.newValue + subset.length; + for (i = subset.oldValue; i < endOld; i += 1) { + gaps1[i] = group; + } + for (i = subset.newValue; i < endNew; i += 1) { + gaps2[i] = group; + } + group += 1; + }); + + return { + gaps1: gaps1, + gaps2: gaps2 + }; + }; + + /** + * Find all matching subsets, based on immediate child differences only. + */ + var markSubTrees = function(oldTree, newTree) { + // note: the child lists are views, and so update as we update old/newTree + var oldChildren = oldTree.childNodes ? oldTree.childNodes : [], + newChildren = newTree.childNodes ? newTree.childNodes : [], + marked1 = makeArray(oldChildren.length, false), + marked2 = makeArray(newChildren.length, false), + subsets = [], + subset = true, + returnIndex = function() { + return arguments[1]; + }, + markBoth = function(i) { + marked1[subset.oldValue + i] = true; + marked2[subset.newValue + i] = true; + }; + + while (subset) { + subset = findCommonSubsets(oldChildren, newChildren, marked1, marked2); + if (subset) { + subsets.push(subset); + + Array.apply(null, new Array(subset.length)).map(returnIndex).forEach(markBoth); + + } + } + return subsets; + }; + + + function swap(obj, p1, p2) { + (function(_) { + obj[p1] = obj[p2]; + obj[p2] = _; + }(obj[p1])); + } + + + var DiffTracker = function() { + this.list = []; + }; + + DiffTracker.prototype = { + list: false, + add: function(diffs) { + var list = this.list; + diffs.forEach(function(diff) { + list.push(diff); + }); + }, + forEach: function(fn) { + this.list.forEach(fn); + } + }; + + var diffDOM = function(options) { + + var defaults = { + debug: false, + diffcap: 10, // Limit for how many diffs are accepting when debugging. Inactive when debug is false. + maxDepth: false, // False or a numeral. If set to a numeral, limits the level of depth that the the diff mechanism looks for differences. If false, goes through the entire tree. + valueDiffing: true, // Whether to take into consideration the values of forms that differ from auto assigned values (when a user fills out a form). + // syntax: textDiff: function (node, currentValue, expectedValue, newValue) + textDiff: function() { + arguments[0].data = arguments[3]; + return; + }, + // empty functions were benchmarked as running faster than both + // `f && f()` and `if (f) { f(); }` + preVirtualDiffApply: function () {}, + postVirtualDiffApply: function () {}, + preDiffApply: function () {}, + postDiffApply: function () {}, + filterOuterDiff: null + }, + i; + + if (typeof options === "undefined") { + options = {}; + } + + for (i in defaults) { + if (typeof options[i] === "undefined") { + this[i] = defaults[i]; + } else { + this[i] = options[i]; + } + } + + }; + + diffDOM.Diff = Diff; + + diffDOM.prototype = { + + // ===== Create a diff ===== + + diff: function(t1Node, t2Node) { + + var t1 = this.nodeToObj(t1Node), + t2 = this.nodeToObj(t2Node); + + diffcount = 0; + + if (this.debug) { + this.t1Orig = this.nodeToObj(t1Node); + this.t2Orig = this.nodeToObj(t2Node); + } + + this.tracker = new DiffTracker(); + return this.findDiffs(t1, t2); + }, + findDiffs: function(t1, t2) { + var diffs; + do { + if (this.debug) { + diffcount += 1; + if (diffcount > this.diffcap) { + window.diffError = [this.t1Orig, this.t2Orig]; + throw new Error("surpassed diffcap:" + JSON.stringify(this.t1Orig) + " -> " + JSON.stringify(this.t2Orig)); + } + } + diffs = this.findNextDiff(t1, t2, []); + if (diffs.length === 0) { + // Last check if the elements really are the same now. + // If not, remove all info about being done and start over. + // Somtimes a node can be marked as done, but the creation of subsequent diffs means that it has to be changed anyway. + if (!isEqual(t1, t2)) { + removeDone(t1); + diffs = this.findNextDiff(t1, t2, []); + } + } + + if (diffs.length > 0) { + this.tracker.add(diffs); + this.applyVirtual(t1, diffs); + } + } while (diffs.length > 0); + return this.tracker.list; + }, + findNextDiff: function(t1, t2, route) { + var diffs, fdiffs; + + if (this.maxDepth && route.length > this.maxDepth) { + return []; + } + // outer differences? + if (!t1.outerDone) { + diffs = this.findOuterDiff(t1, t2, route); + if (this.filterOuterDiff) { + fdiffs = this.filterOuterDiff(t1, t2, diffs); + if (fdiffs) diffs = fdiffs; + } + if (diffs.length > 0) { + t1.outerDone = true; + return diffs; + } else { + t1.outerDone = true; + } + } + // inner differences? + if (!t1.innerDone) { + diffs = this.findInnerDiff(t1, t2, route); + if (diffs.length > 0) { + return diffs; + } else { + t1.innerDone = true; + } + } + + if (this.valueDiffing && !t1.valueDone) { + // value differences? + diffs = this.findValueDiff(t1, t2, route); + + if (diffs.length > 0) { + t1.valueDone = true; + return diffs; + } else { + t1.valueDone = true; + } + } + + // no differences + return []; + }, + findOuterDiff: function(t1, t2, route) { + + var diffs = [], + attr1, attr2; + + if (t1.nodeName !== t2.nodeName) { + return [new Diff({ + action: 'replaceElement', + oldValue: cloneObj(t1), + newValue: cloneObj(t2), + route: route + })]; + } + + if (t1.data !== t2.data) { + // Comment or text node. + if (t1.nodeName === '#text') { + return [new Diff({ + action: 'modifyTextElement', + route: route, + oldValue: t1.data, + newValue: t2.data + })]; + } else { + return [new Diff({ + action: 'modifyComment', + route: route, + oldValue: t1.data, + newValue: t2.data + })]; + } + + } + + + attr1 = t1.attributes ? Object.keys(t1.attributes).sort() : []; + attr2 = t2.attributes ? Object.keys(t2.attributes).sort() : []; + + attr1.forEach(function(attr) { + var pos = attr2.indexOf(attr); + if (pos === -1) { + diffs.push(new Diff({ + action: 'removeAttribute', + route: route, + name: attr, + value: t1.attributes[attr] + })); + } else { + attr2.splice(pos, 1); + if (t1.attributes[attr] !== t2.attributes[attr]) { + diffs.push(new Diff({ + action: 'modifyAttribute', + route: route, + name: attr, + oldValue: t1.attributes[attr], + newValue: t2.attributes[attr] + })); + } + } + + }); + + + attr2.forEach(function(attr) { + diffs.push(new Diff({ + action: 'addAttribute', + route: route, + name: attr, + value: t2.attributes[attr] + })); + + }); + + return diffs; + }, + nodeToObj: function(node) { + var objNode = {}, dobj = this; + objNode.nodeName = node.nodeName; + if (objNode.nodeName === '#text' || objNode.nodeName === '#comment') { + objNode.data = node.data; + } else { + if (node.attributes && node.attributes.length > 0) { + objNode.attributes = {}; + Array.prototype.slice.call(node.attributes).forEach( + function(attribute) { + objNode.attributes[attribute.name] = attribute.value; + } + ); + } + if (node.childNodes && node.childNodes.length > 0) { + objNode.childNodes = []; + Array.prototype.slice.call(node.childNodes).forEach( + function(childNode) { + objNode.childNodes.push(dobj.nodeToObj(childNode)); + } + ); + } + if (this.valueDiffing) { + if (node.value !== undefined) { + objNode.value = node.value; + } + if (node.checked !== undefined) { + objNode.checked = node.checked; + } + if (node.selected !== undefined) { + objNode.selected = node.selected; + } + } + } + + return objNode; + }, + objToNode: function(objNode, insideSvg) { + var node, dobj = this; + if (objNode.nodeName === '#text') { + node = document.createTextNode(objNode.data); + + } else if (objNode.nodeName === '#comment') { + node = document.createComment(objNode.data); + } else { + if (objNode.nodeName === 'svg' || insideSvg) { + node = document.createElementNS('http://www.w3.org/2000/svg', objNode.nodeName); + insideSvg = true; + } else { + node = document.createElement(objNode.nodeName); + } + if (objNode.attributes) { + Object.keys(objNode.attributes).forEach(function(attribute) { + node.setAttribute(attribute, objNode.attributes[attribute]); + }); + } + if (objNode.childNodes) { + objNode.childNodes.forEach(function(childNode) { + node.appendChild(dobj.objToNode(childNode, insideSvg)); + }); + } + if (this.valueDiffing) { + if (objNode.value) { + node.value = objNode.value; + } + if (objNode.checked) { + node.checked = objNode.checked; + } + if (objNode.selected) { + node.selected = objNode.selected; + } + } + } + return node; + }, + findInnerDiff: function(t1, t2, route) { + + var subtrees = (t1.childNodes && t2.childNodes) ? markSubTrees(t1, t2) : [], + t1ChildNodes = t1.childNodes ? t1.childNodes : [], + t2ChildNodes = t2.childNodes ? t2.childNodes : [], + childNodesLengthDifference, diffs = [], + index = 0, + last, e1, e2, i; + + if (subtrees.length > 0) { + /* One or more groups have been identified among the childnodes of t1 + * and t2. + */ + diffs = this.attemptGroupRelocation(t1, t2, subtrees, route); + if (diffs.length > 0) { + return diffs; + } + } + + /* 0 or 1 groups of similar child nodes have been found + * for t1 and t2. 1 If there is 1, it could be a sign that the + * contents are the same. When the number of groups is below 2, + * t1 and t2 are made to have the same length and each of the + * pairs of child nodes are diffed. + */ + + + last = Math.max(t1ChildNodes.length, t2ChildNodes.length); + if (t1ChildNodes.length !== t2ChildNodes.length) { + childNodesLengthDifference = true; + } + + for (i = 0; i < last; i += 1) { + e1 = t1ChildNodes[i]; + e2 = t2ChildNodes[i]; + + if (childNodesLengthDifference) { + /* t1 and t2 have different amounts of childNodes. Add + * and remove as necessary to obtain the same length */ + if (e1 && !e2) { + if (e1.nodeName === '#text') { + diffs.push(new Diff({ + action: 'removeTextElement', + route: route.concat(index), + value: e1.data + })); + index -= 1; + } else { + diffs.push(new Diff({ + action: 'removeElement', + route: route.concat(index), + element: cloneObj(e1) + })); + index -= 1; + } + + } else if (e2 && !e1) { + if (e2.nodeName === '#text') { + diffs.push(new Diff({ + action: 'addTextElement', + route: route.concat(i), + value: e2.data + })); + } else { + diffs.push(new Diff({ + action: 'addElement', + route: route.concat(i), + element: cloneObj(e2) + })); + } + } + } + /* We are now guaranteed that childNodes e1 and e2 exist, + * and that they can be diffed. + */ + /* Diffs in child nodes should not affect the parent node, + * so we let these diffs be submitted together with other + * diffs. + */ + + if (e1 && e2) { + diffs = diffs.concat(this.findNextDiff(e1, e2, route.concat(index))); + } + + index += 1; + + } + t1.innerDone = true; + return diffs; + + }, + + attemptGroupRelocation: function(t1, t2, subtrees, route) { + /* Either t1.childNodes and t2.childNodes have the same length, or + * there are at least two groups of similar elements can be found. + * attempts are made at equalizing t1 with t2. First all initial + * elements with no group affiliation (gaps=true) are removed (if + * only in t1) or added (if only in t2). Then the creation of a group + * relocation diff is attempted. + */ + + var gapInformation = getGapInformation(t1, t2, subtrees), + gaps1 = gapInformation.gaps1, + gaps2 = gapInformation.gaps2, + shortest = Math.min(gaps1.length, gaps2.length), + destinationDifferent, toGroup, + group, node, similarNode, testI, diffs = [], + index1, index2, j; + + + for (index2 = 0, index1 = 0; index2 < shortest; index1 += 1, index2 += 1) { + if (gaps1[index2] === true) { + node = t1.childNodes[index1]; + if (node.nodeName === '#text') { + if (t2.childNodes[index2].nodeName === '#text' && node.data !== t2.childNodes[index2].data) { + testI = index1; + while (t1.childNodes.length > testI + 1 && t1.childNodes[testI + 1].nodeName === '#text') { + testI += 1; + if (t2.childNodes[index2].data === t1.childNodes[testI].data) { + similarNode = true; + break; + } + } + if (!similarNode) { + diffs.push(new Diff({ + action: 'modifyTextElement', + route: route.concat(index2), + oldValue: node.data, + newValue: t2.childNodes[index2].data + })); + return diffs; + } + } + diffs.push(new Diff({ + action: 'removeTextElement', + route: route.concat(index2), + value: node.data + })); + gaps1.splice(index2, 1); + shortest = Math.min(gaps1.length, gaps2.length); + index2 -= 1; + } else { + diffs.push(new Diff({ + action: 'removeElement', + route: route.concat(index2), + element: cloneObj(node) + })); + gaps1.splice(index2, 1); + shortest = Math.min(gaps1.length, gaps2.length); + index2 -= 1; + } + + } else if (gaps2[index2] === true) { + node = t2.childNodes[index2]; + if (node.nodeName === '#text') { + diffs.push(new Diff({ + action: 'addTextElement', + route: route.concat(index2), + value: node.data + })); + gaps1.splice(index2, 0, true); + shortest = Math.min(gaps1.length, gaps2.length); + index1 -= 1; + } else { + diffs.push(new Diff({ + action: 'addElement', + route: route.concat(index2), + element: cloneObj(node) + })); + gaps1.splice(index2, 0, true); + shortest = Math.min(gaps1.length, gaps2.length); + index1 -= 1; + } + + } else if (gaps1[index2] !== gaps2[index2]) { + if (diffs.length > 0) { + return diffs; + } + // group relocation + group = subtrees[gaps1[index2]]; + toGroup = Math.min(group.newValue, (t1.childNodes.length - group.length)); + if (toGroup !== group.oldValue) { + // Check whether destination nodes are different than originating ones. + destinationDifferent = false; + for (j = 0; j < group.length; j += 1) { + if (!roughlyEqual(t1.childNodes[toGroup + j], t1.childNodes[group.oldValue + j], [], false, true)) { + destinationDifferent = true; + } + } + if (destinationDifferent) { + return [new Diff({ + action: 'relocateGroup', + groupLength: group.length, + from: group.oldValue, + to: toGroup, + route: route + })]; + } + } + } + } + return diffs; + }, + + findValueDiff: function(t1, t2, route) { + // Differences of value. Only useful if the value/selection/checked value + // differs from what is represented in the DOM. For example in the case + // of filled out forms, etc. + var diffs = []; + + if (t1.selected !== t2.selected) { + diffs.push(new Diff({ + action: 'modifySelected', + oldValue: t1.selected, + newValue: t2.selected, + route: route + })); + } + + if ((t1.value || t2.value) && t1.value !== t2.value && t1.nodeName !== 'OPTION') { + diffs.push(new Diff({ + action: 'modifyValue', + oldValue: t1.value, + newValue: t2.value, + route: route + })); + } + if (t1.checked !== t2.checked) { + diffs.push(new Diff({ + action: 'modifyChecked', + oldValue: t1.checked, + newValue: t2.checked, + route: route + })); + } + + return diffs; + }, + + // ===== Apply a virtual diff ===== + + applyVirtual: function(tree, diffs) { + var dobj = this; + if (diffs.length === 0) { + return true; + } + diffs.forEach(function(diff) { + dobj.applyVirtualDiff(tree, diff); + }); + return true; + }, + getFromVirtualRoute: function(tree, route) { + var node = tree, + parentNode, nodeIndex; + + route = route.slice(); + while (route.length > 0) { + if (!node.childNodes) { + return false; + } + nodeIndex = route.splice(0, 1)[0]; + parentNode = node; + node = node.childNodes[nodeIndex]; + } + return { + node: node, + parentNode: parentNode, + nodeIndex: nodeIndex + }; + }, + applyVirtualDiff: function(tree, diff) { + var routeInfo = this.getFromVirtualRoute(tree, diff.route), + node = routeInfo.node, + parentNode = routeInfo.parentNode, + nodeIndex = routeInfo.nodeIndex, + newNode, route, c; + + // pre-diff hook + var info = { + diff: diff, + node: node + }; + + if (this.preVirtualDiffApply(info)) { return true; } + + switch (diff.action) { + case 'addAttribute': + if (!node.attributes) { + node.attributes = {}; + } + + node.attributes[diff.name] = diff.value; + + if (diff.name === 'checked') { + node.checked = true; + } else if (diff.name === 'selected') { + node.selected = true; + } else if (node.nodeName === 'INPUT' && diff.name === 'value') { + node.value = diff.value; + } + + break; + case 'modifyAttribute': + node.attributes[diff.name] = diff.newValue; + if (node.nodeName === 'INPUT' && diff.name === 'value') { + node.value = diff.value; + } + break; + case 'removeAttribute': + + delete node.attributes[diff.name]; + + if (Object.keys(node.attributes).length === 0) { + delete node.attributes; + } + + if (diff.name === 'checked') { + node.checked = false; + } else if (diff.name === 'selected') { + delete node.selected; + } else if (node.nodeName === 'INPUT' && diff.name === 'value') { + delete node.value; + } + + break; + case 'modifyTextElement': + node.data = diff.newValue; + + if (parentNode.nodeName === 'TEXTAREA') { + parentNode.value = diff.newValue; + } + break; + case 'modifyValue': + node.value = diff.newValue; + break; + case 'modifyComment': + node.data = diff.newValue; + break; + case 'modifyChecked': + node.checked = diff.newValue; + break; + case 'modifySelected': + node.selected = diff.newValue; + break; + case 'replaceElement': + newNode = cloneObj(diff.newValue); + newNode.outerDone = true; + newNode.innerDone = true; + newNode.valueDone = true; + parentNode.childNodes[nodeIndex] = newNode; + break; + case 'relocateGroup': + node.childNodes.splice(diff.from, diff.groupLength).reverse() + .forEach(function(movedNode) { + node.childNodes.splice(diff.to, 0, movedNode); + }); + break; + case 'removeElement': + parentNode.childNodes.splice(nodeIndex, 1); + break; + case 'addElement': + route = diff.route.slice(); + c = route.splice(route.length - 1, 1)[0]; + node = this.getFromVirtualRoute(tree, route).node; + newNode = cloneObj(diff.element); + newNode.outerDone = true; + newNode.innerDone = true; + newNode.valueDone = true; + + if (!node.childNodes) { + node.childNodes = []; + } + + if (c >= node.childNodes.length) { + node.childNodes.push(newNode); + } else { + node.childNodes.splice(c, 0, newNode); + } + break; + case 'removeTextElement': + parentNode.childNodes.splice(nodeIndex, 1); + if (parentNode.nodeName === 'TEXTAREA') { + delete parentNode.value; + } + break; + case 'addTextElement': + route = diff.route.slice(); + c = route.splice(route.length - 1, 1)[0]; + newNode = {}; + newNode.nodeName = '#text'; + newNode.data = diff.value; + node = this.getFromVirtualRoute(tree, route).node; + if (!node.childNodes) { + node.childNodes = []; + } + + if (c >= node.childNodes.length) { + node.childNodes.push(newNode); + } else { + node.childNodes.splice(c, 0, newNode); + } + if (node.nodeName === 'TEXTAREA') { + node.value = diff.newValue; + } + break; + default: + console.log('unknown action'); + } + + // capture newNode for the callback + info.newNode = newNode; + this.postVirtualDiffApply(info); + + return; + }, + + + + + // ===== Apply a diff ===== + + apply: function(tree, diffs) { + var dobj = this; + + if (diffs.length === 0) { + return true; + } + diffs.forEach(function(diff) { + if (!dobj.applyDiff(tree, diff)) { + return false; + } + }); + return true; + }, + getFromRoute: function(tree, route) { + route = route.slice(); + var c, node = tree; + while (route.length > 0) { + if (!node.childNodes) { + return false; + } + c = route.splice(0, 1)[0]; + node = node.childNodes[c]; + } + return node; + }, + applyDiff: function(tree, diff) { + var node = this.getFromRoute(tree, diff.route), + newNode, reference, route, c; + + // pre-diff hook + var info = { + diff: diff, + node: node + }; + + if (this.preDiffApply(info)) { return true; } + + switch (diff.action) { + case 'addAttribute': + if (!node || !node.setAttribute) { + return false; + } + node.setAttribute(diff.name, diff.value); + break; + case 'modifyAttribute': + if (!node || !node.setAttribute) { + return false; + } + node.setAttribute(diff.name, diff.newValue); + break; + case 'removeAttribute': + if (!node || !node.removeAttribute) { + return false; + } + node.removeAttribute(diff.name); + break; + case 'modifyTextElement': + if (!node || node.nodeType !== 3) { + return false; + } + this.textDiff(node, node.data, diff.oldValue, diff.newValue); + break; + case 'modifyValue': + if (!node || typeof node.value === 'undefined') { + return false; + } + node.value = diff.newValue; + break; + case 'modifyComment': + if (!node || typeof node.data === 'undefined') { + return false; + } + this.textDiff(node, node.data, diff.oldValue, diff.newValue); + break; + case 'modifyChecked': + if (!node || typeof node.checked === 'undefined') { + return false; + } + node.checked = diff.newValue; + break; + case 'modifySelected': + if (!node || typeof node.selected === 'undefined') { + return false; + } + node.selected = diff.newValue; + break; + case 'replaceElement': + node.parentNode.replaceChild(this.objToNode(diff.newValue, node.namespaceURI === 'http://www.w3.org/2000/svg'), node); + break; + case 'relocateGroup': + Array.apply(null, new Array(diff.groupLength)).map(function() { + return node.removeChild(node.childNodes[diff.from]); + }).forEach(function(childNode, index) { + if (index === 0) { + reference = node.childNodes[diff.to]; + } + node.insertBefore(childNode, reference); + }); + break; + case 'removeElement': + node.parentNode.removeChild(node); + break; + case 'addElement': + route = diff.route.slice(); + c = route.splice(route.length - 1, 1)[0]; + node = this.getFromRoute(tree, route); + node.insertBefore(this.objToNode(diff.element, node.namespaceURI === 'http://www.w3.org/2000/svg'), node.childNodes[c]); + break; + case 'removeTextElement': + if (!node || node.nodeType !== 3) { + return false; + } + node.parentNode.removeChild(node); + break; + case 'addTextElement': + route = diff.route.slice(); + c = route.splice(route.length - 1, 1)[0]; + newNode = document.createTextNode(diff.value); + node = this.getFromRoute(tree, route); + if (!node || !node.childNodes) { + return false; + } + node.insertBefore(newNode, node.childNodes[c]); + break; + default: + console.log('unknown action'); + } + + // if a new node was created, we might be interested in it + // post diff hook + info.newNode = newNode; + this.postDiffApply(info); + + return true; + }, + + // ===== Undo a diff ===== + + undo: function(tree, diffs) { + diffs = diffs.slice(); + var dobj = this; + if (!diffs.length) { + diffs = [diffs]; + } + diffs.reverse(); + diffs.forEach(function(diff) { + dobj.undoDiff(tree, diff); + }); + }, + undoDiff: function(tree, diff) { + + switch (diff.action) { + case 'addAttribute': + diff.action = 'removeAttribute'; + this.applyDiff(tree, diff); + break; + case 'modifyAttribute': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'removeAttribute': + diff.action = 'addAttribute'; + this.applyDiff(tree, diff); + break; + case 'modifyTextElement': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'modifyValue': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'modifyComment': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'modifyChecked': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'modifySelected': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'replaceElement': + swap(diff, 'oldValue', 'newValue'); + this.applyDiff(tree, diff); + break; + case 'relocateGroup': + swap(diff, 'from', 'to'); + this.applyDiff(tree, diff); + break; + case 'removeElement': + diff.action = 'addElement'; + this.applyDiff(tree, diff); + break; + case 'addElement': + diff.action = 'removeElement'; + this.applyDiff(tree, diff); + break; + case 'removeTextElement': + diff.action = 'addTextElement'; + this.applyDiff(tree, diff); + break; + case 'addTextElement': + diff.action = 'removeTextElement'; + this.applyDiff(tree, diff); + break; + default: + console.log('unknown action'); + } + + } + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = diffDOM; + } + exports.diffDOM = diffDOM; + } else { + // `window` in the browser, or `exports` on the server + this.diffDOM = diffDOM; + } + +}.call(this)); diff --git a/www/pad/inner.js b/www/pad/inner.js index 8e3a763bf..47ab5dbe3 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -45,7 +45,7 @@ define([ '/customize/application_config.js', '/common/test.js', - '/bower_components/diff-dom/diffDOM.js', + '/lib/diff-dom/diffDOM.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/customize/src/print.css', diff --git a/www/poll/render.js b/www/poll/render.js index efafd2ee4..9f5980731 100644 --- a/www/poll/render.js +++ b/www/poll/render.js @@ -6,7 +6,7 @@ define([ '/common/common-util.js', '/customize/messages.js', - '/bower_components/diff-dom/diffDOM.js' + '/lib/diff-dom/diffDOM.js', ], function ($, Hyperjson, TextCursor, ChainPad, Util, Messages) { var DiffDOM = window.diffDOM; From 211fd10b62fb1c8b53910f4b06745fae43e4468a Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 15:55:31 +0200 Subject: [PATCH 11/27] Fix diffDOM errors with email encoded HTML (3D attributes) --- www/lib/diff-dom/diffDOM.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/lib/diff-dom/diffDOM.js b/www/lib/diff-dom/diffDOM.js index 0a2f22db6..deca9be40 100755 --- a/www/lib/diff-dom/diffDOM.js +++ b/www/lib/diff-dom/diffDOM.js @@ -690,7 +690,9 @@ } if (objNode.childNodes) { objNode.childNodes.forEach(function(childNode) { - node.appendChild(dobj.objToNode(childNode, insideSvg)); + try { + node.appendChild(dobj.objToNode(childNode, insideSvg)); + } catch (e) { console.debug(e); } }); } if (this.valueDiffing) { From b68b635e6515b9556327a6475e6dd3ec08cfa3c9 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 18:02:24 +0200 Subject: [PATCH 12/27] Remove XXX and fix restricted calendar --- www/calendar/inner.js | 5 ++--- www/common/outer/calendar.js | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 865fe0a6e..66803a188 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -318,7 +318,6 @@ Messages.calendar_noNotification = "None"; } }; - // XXX Note: always create calendars in your own proxy. If you want a team calendar, you can share it with the team later. var editCalendar = function (id) { var isNew = !id; var data = APP.calendars[id]; @@ -453,7 +452,7 @@ Messages.calendar_noNotification = "None"; pathname: "/calendar/", friends: friends, title: title, - password: cal.password, // XXX support passwords + password: cal.password, calendar: { title: title, color: color, @@ -480,7 +479,7 @@ Messages.calendar_noNotification = "None"; var href = Hash.hashToHref(h.editHash || h.viewHash, 'calendar'); Access.getAccessModal(common, { title: title, - password: cal.password, // XXX support passwords + password: cal.password, calendar: { title: title, color: color, diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index 1bb5fd570..33453b6ee 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -114,7 +114,7 @@ define([ } } - // XXX add a limit to make sure we don't go too far in the past? + // XXX add a limit to make sure we don't go too far in the past? ==> 1 week var missed = useLastVisit && ev.start > last && ev.end <= now; if (ev.end <= now && !missed) { // No reminder for past events @@ -283,7 +283,6 @@ define([ var onDeleted = function () { // Remove this calendar from all our teams - // XXX Maybe not? don't remove automatically so that we can tell the user to do so. c.stores.forEach(function (storeId) { var store = getStore(ctx, storeId); if (!store || !store.rpc || !store.proxy.calendars) { return; } @@ -412,6 +411,7 @@ define([ } if (info.error === "ERESTRICTED" ) { c.restricted = true; + setTimeout(update); } cb(info); }); @@ -846,7 +846,7 @@ define([ Calendar.init = function (cfg, waitFor, emit) { var calendar = {}; var store = cfg.store; - if (!store.loggedIn || !store.proxy.edPublic) { return; } // XXX logged in only? + if (!store.loggedIn || !store.proxy.edPublic) { return; } // XXX logged in only? we should al least allow read-only for URL calendars var ctx = { store: store, Store: cfg.Store, From 390e36ea849858e1e667141e6e2a7a560559bc2e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 18:09:45 +0200 Subject: [PATCH 13/27] Replace 'My calendars' with the current user data --- www/calendar/inner.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 66803a188..25b3fc47d 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -70,7 +70,6 @@ Messages.calendar_deleteConfirm = "Are you sure you want to delete this calendar Messages.calendar_deleteTeamConfirm = "Are you sure you want to delete this calendar from this team?"; Messages.calendar_deleteOwned = " It will still be visible for the users it has been shared with."; Messages.calendar_errorNoCalendar = "No editable calendar selected!"; -Messages.calendar_myCalendars = "My calendars"; Messages.calendar_tempCalendar = "Viewing"; Messages.calendar_import = "Import to my calendars"; Messages.calendar_import_temp = "Import this calendar"; @@ -720,8 +719,13 @@ Messages.calendar_noNotification = "None"; } var myCalendars = filter(1); if (myCalendars.length) { + var user = metadataMgr.getUserData(); + var avatar = h('span.cp-avatar'); + var name = user.name || Messages.anonymous; + common.displayAvatar($(avatar), user.avatar, name); APP.$calendars.append(h('div.cp-calendar-team', [ - h('span', Messages.calendar_myCalendars) + avatar, + h('span.cp-name', {title: name}, name) ])); } myCalendars.forEach(function (id) { From 8d8633be00cc4451247e50e5a7bbf404ab48e3d5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 28 Apr 2021 09:39:03 +0530 Subject: [PATCH 14/27] remove hardcoded translation for footer roadmap --- customize.dist/pages.js | 3 +-- www/common/application_config_internal.js | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index f8d7ad15c..e446f9704 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -101,8 +101,7 @@ define([ Pages.privacyLink = footLink(AppConfig.privacy, 'privacy'); Pages.githubLink = footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'); Pages.docsLink = footLink('https://docs.cryptpad.fr', 'docs_link'); - Msg.home_roadmap = "Roadmap"; // XXX - Pages.roadmapLink = footLink(AppConfig.roadmap, 'home_roadmap'); + Pages.roadmapLink = footLink(AppConfig.roadmap, 'footer_roadmap'); Pages.infopageFooter = function () { var terms = footLink('/terms.html', 'footer_tos'); // FIXME this should be configurable like the other legal pages diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index ee4d82288..f1b2f94bf 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -45,10 +45,12 @@ define(function() { */ // config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy'; - /* XXX - * + /* We (the project's developers) include the ability to display a 'Roadmap' in static pages footer. + * This is disabled by default. + * We use this to publish the project's development roadmap, but you can use it however you like. + * To do so, set the following value to an absolute URL. */ - //config.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; // XXX + //config.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; /* Cryptpad apps use a common API to display notifications to users * by default, notifications are hidden after 5 seconds From aee4d443ef73c188ced877a860b29863fef53dcb Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 20 Apr 2021 17:47:28 +0200 Subject: [PATCH 15/27] Display the sync popup sooner in OO --- www/common/onlyoffice/inner.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index eee9f6883..639e69d01 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -463,6 +463,9 @@ define([ }; fixSheets(); + if (!isLockedModal.modal) { + isLockedModal.modal = UI.openCustomModal(isLockedModal.content); + } ooChannel.ready = false; ooChannel.queue = []; data.callback = function () { @@ -2213,6 +2216,9 @@ define([ }; var loadCp = function (cp, keepQueue) { + if (!isLockedModal.modal) { + isLockedModal.modal = UI.openCustomModal(isLockedModal.content); + } loadLastDocument(cp, function () { var file = getFileType(); var type = common.getMetadataMgr().getPrivateData().ooType; @@ -2695,6 +2701,9 @@ define([ var reloadPopup = false; var checkNewCheckpoint = function () { + if (!isLockedModal.modal) { + isLockedModal.modal = UI.openCustomModal(isLockedModal.content); + } var lastCp = getLastCp(); loadLastDocument(lastCp, function (err) { console.error(err); From d3fd52d7215ca644a524d6f0b3c081be9df14e4b Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 23 Apr 2021 10:48:35 +0200 Subject: [PATCH 16/27] Handle corrupted cache in OO chainpad --- www/common/onlyoffice/inner.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 639e69d01..6f1ac6b2a 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -2528,6 +2528,18 @@ define([ toolbar.$drawer.append($properties); }; + var noCache = false; // Prevent reload loops + var onCorruptedCache = function () { + if (noCache) { + UI.errorLoadingScreen(Messages.unableToDisplay, false, function () { + common.gotoURL(''); + }); + } + noCache = true; + var sframeChan = common.getSframeChannel(); + sframeChan.event("EV_CORRUPTED_CACHE"); + }; + config.onReady = function (info) { if (APP.realtime !== info.realtime) { APP.realtime = info.realtime; @@ -2560,11 +2572,8 @@ define([ newDoc = !content.hashes || Object.keys(content.hashes).length === 0; } else if (!privateData.isNewFile) { // This is an empty doc but not a new file: error - // XXX clear cache before reloading - UI.errorLoadingScreen(Messages.unableToDisplay, false, function () { - common.gotoURL(''); - }); - throw new Error("Empty chainpad for a non-empty doc"); + onCorruptedCache(); + return void console.error("Empty chainpad for a non-empty doc"); } else { Title.updateTitle(Title.defaultTitle); } From c57400ab9c1a24c6ea419c1f00e9b319fcd2b602 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:54:19 +0200 Subject: [PATCH 17/27] Improve UX when maxUploadSize is reached in sheets --- www/common/onlyoffice/inner.js | 27 ++++++++++++++++++++++++++- www/common/sframe-common-file.js | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 6f1ac6b2a..1a84d4a41 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -356,10 +356,32 @@ define([ } }; + Messages.oo_cantMigrate = "This sheet exceeds the maximum upload size and is too large to be migrated."; // XXX var onUploaded = function (ev, data, err) { content.saveLock = undefined; if (err) { console.error(err); + if (content.saveLock === myOOId) { delete content.saveLock; } // Unlock checkpoints + if (APP.migrateModal) { + try { getEditor().asc_setRestriction(true); } catch (e) {} + setEditable(true); + delete content.migration; + APP.migrateModal.closeModal(); + APP.onLocal(); + } + if (isLockedModal.modal && err === "TOO_LARGE") { + if (APP.migrate) { + UI.warn(Messages.oo_cantMigrate); + } + APP.cantCheckpoint = true; + isLockedModal.modal.closeModal(); + delete isLockedModal.modal; + if (content.saveLock === myOOId) { + delete content.saveLock; + } + APP.onLocal(); + return; + } return void UI.alert(Messages.oo_saveError); } // Get the last cp idx @@ -453,6 +475,7 @@ define([ }; var saveToServer = function () { + if (APP.cantCheckpoint) { return; } // TOO_LARGE var text = getContent(); var blob = new Blob([text], {type: 'plain/text'}); var file = getFileType(); @@ -479,6 +502,8 @@ define([ var noLogin = false; var makeCheckpoint = function (force) { + if (APP.cantCheckpoint) { return; } // TOO_LARGE + var locked = content.saveLock; var lastCp = getLastCp(); @@ -1388,7 +1413,7 @@ define([ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_sheetMigration_loading) ]); - UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); + APP.migrateModal = UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); makeCheckpoint(true); }); // DEPRECATED: from version 3, the queue is sent again during init diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index c7a66adb9..2a6f4fea3 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -133,6 +133,7 @@ define([ $pv.text(Messages.error); queue.inProgress = false; queue.next(); + if (config.onError) { config.onError("TOO_LARGE"); } return void UI.alert(Messages._getKey('upload_tooLargeBrief', [maxSizeStr])); } From ac875763601f92eff8eeb227e3675170d22a84ae Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 26 Apr 2021 13:23:59 +0200 Subject: [PATCH 18/27] Never store invalid OO checkpoints --- www/common/onlyoffice/inner.js | 12 ++++++++++++ www/common/sframe-common-outer.js | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 1a84d4a41..5a720a60c 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -477,6 +477,18 @@ define([ var saveToServer = function () { if (APP.cantCheckpoint) { return; } // TOO_LARGE var text = getContent(); + text = undefined; + if (!text) { + setEditable(false, true); + sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ + 'chainpad', + content.channel, + ], function () {}); + UI.alert(Messages.realtime_unrecoverableError, function () { + common.gotoURL(); + }); + return; + } var blob = new Blob([text], {type: 'plain/text'}); var file = getFileType(); blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index dde533e9f..7190fb997 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1533,6 +1533,16 @@ define([ sframeChan.on('Q_CLEAR_CACHE', function (data, cb) { Utils.Cache.clear(cb); }); + sframeChan.on('Q_CLEAR_CACHE_CHANNELS', function (channels, cb) { + if (!Array.isArray(channels)) { return void cb({error: "NOT_AN_ARRAY"}); } + nThen(function (waitFor) { + channels.forEach(function (chan) { + if (chan === "chainpad") { chan = secret.channel; } + console.error(chan); + Utils.Cache.clearChannel(chan, waitFor()); + }); + }).nThen(cb); + }); sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) { Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) { From d839481b72db7e0ab1a9795d01f038d1df9d52ed Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:19:24 +0200 Subject: [PATCH 19/27] Remove debugging data --- www/common/onlyoffice/inner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 5a720a60c..fa54dd80c 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -477,7 +477,6 @@ define([ var saveToServer = function () { if (APP.cantCheckpoint) { return; } // TOO_LARGE var text = getContent(); - text = undefined; if (!text) { setEditable(false, true); sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ From d903bf51a354a33d10ff15c92ba1ced695c60ced Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 28 Apr 2021 13:51:50 +0200 Subject: [PATCH 20/27] Translated using Weblate (French) Currently translated at 99.7% (1183 of 1186 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/ --- www/common/translations/messages.fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index cd3c86130..dab1481dd 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1181,5 +1181,7 @@ "admin_maintenanceButton": "Planifier maintenance", "admin_maintenanceHint": "Planifier une maintenance sur cette instance et avertir tous les utilisateurs. Limité à une maintenance active à la fois.", "admin_maintenanceTitle": "Maintenance", - "admin_cat_broadcast": "Annonces" + "admin_cat_broadcast": "Annonces", + "oo_cantMigrate": "Ce tableur dépasse la taille maximale de téléchargement et est trop grand pour être mis à jour.", + "footer_roadmap": "Feuille de route" } From 102c36165d0d4fde16e96dfb5a8abce22e2439c3 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 28 Apr 2021 13:51:50 +0200 Subject: [PATCH 21/27] Translated using Weblate (German) Currently translated at 99.9% (1184 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index c7d28db09..f01a473e2 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -541,7 +541,7 @@ "four04_pageNotFound": "Wir konnten die Seite, die du angefordert hast, nicht finden.", "header_logoTitle": "Zu deinem CryptDrive", "header_homeTitle": "Zur CryptPad-Hauptseite", - "help_genericMore": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere Documentation liest.", + "help_genericMore": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere Dokumentation liest.", "driveReadmeTitle": "Was ist CryptPad?", "edit": "Bearbeiten", "view": "Ansehen", From d39a2db7346b1f3a489593100444f055b4a5b84c Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 28 Apr 2021 13:51:50 +0200 Subject: [PATCH 22/27] Translated using Weblate (English) Currently translated at 100.0% (1186 of 1186 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 18c6feb8a..a725cb012 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1185,5 +1185,6 @@ "settings_deleteWarning": "Warning: you are currently subscribed to a premium plan (paid or given by another user). Please cancel your plan before deleting your account as will not be possible without contacting support once your account is deleted.", "settings_deleteContinue": "Delete my account", "settings_deleteSubscription": "Manage my subscription", - "footer_roadmap": "Roadmap" + "footer_roadmap": "Roadmap", + "oo_cantMigrate": "This sheet exceeds the maximum upload size and is too large to be migrated." } From b812bc27f622bc57693fdb7d8262575d7e7b4e1a Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 28 Apr 2021 17:25:04 +0530 Subject: [PATCH 23/27] remove hardcoded translation --- www/common/onlyoffice/inner.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index fa54dd80c..e99787d9e 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -356,7 +356,6 @@ define([ } }; - Messages.oo_cantMigrate = "This sheet exceeds the maximum upload size and is too large to be migrated."; // XXX var onUploaded = function (ev, data, err) { content.saveLock = undefined; if (err) { From 9d6d5b3ed317f6408b26937c5c79c9e884c5ac19 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 13:55:03 +0200 Subject: [PATCH 24/27] lint compliance --- www/common/onlyoffice/inner.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index e99787d9e..fcffadb52 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -356,6 +356,14 @@ define([ } }; + // Add a lock + var isLockedModal = { + content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [ + h('span.fa.fa-spin.fa-spinner'), + h('span', Messages.oo_isLocked) + ])) + }; + var onUploaded = function (ev, data, err) { content.saveLock = undefined; if (err) { @@ -443,14 +451,6 @@ define([ }; APP.FM = common.createFileManager(fmConfig); - // Add a lock - var isLockedModal = { - content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [ - h('span.fa.fa-spin.fa-spinner'), - h('span', Messages.oo_isLocked) - ])) - }; - var resetData = function (blob, type) { // If a read-only refresh popup was planned, abort it delete APP.refreshPopup; From 1d47d3d94ef5b1ed0ae88a25be42705a6d8929e4 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 23 Apr 2021 11:12:45 +0200 Subject: [PATCH 25/27] Prevent type error from CkEditor widgets --- www/pad/inner.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/www/pad/inner.js b/www/pad/inner.js index b442de0b5..c78811b07 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -984,7 +984,11 @@ define([ displayMediaTags(framework, inner, mediaTagMap); // MEDIATAG: Initialize mediatag widgets inserted in the document by other users - editor.widgets.checkWidgets(); + try { + editor.widgets.checkWidgets(); + } catch (e) { + console.error(e); + } if (framework.isReadOnly()) { var $links = $inner.find('a'); From 980a2369007a3b6eeb4de105bfcf1cf13e3444ec Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 27 Apr 2021 14:51:11 +0200 Subject: [PATCH 26/27] Prevent parse errors in sframe-common-outer --- www/common/sframe-common-outer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 7190fb997..d24f62f9d 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -44,8 +44,8 @@ define([ // loading screen setup. var done = waitFor(); var onMsg = function (msg) { - var data = JSON.parse(msg.data); - if (data.q !== 'READY') { return; } + var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data; + if (!data || data.q !== 'READY') { return; } window.removeEventListener('message', onMsg); var _done = done; done = function () { }; @@ -182,8 +182,8 @@ define([ }; var whenReady = waitFor(function (msg) { if (msg.source !== iframe) { return; } - var data = JSON.parse(msg.data); - if (!data.txid) { return; } + var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data; + if (!data || !data.txid) { return; } // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data From f19c5b9ccae1cb3db65ba3d7da863e5cd9bf32c7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 28 Apr 2021 19:05:40 +0530 Subject: [PATCH 27/27] remove XXX notes --- www/calendar/inner.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 25b3fc47d..3cde25436 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -635,7 +635,6 @@ Messages.calendar_noNotification = "None"; return UIElements.createDropdown(dropdownConfig)[0]; }; var makeCalendarEntry = function (id, teamId) { - // XXX handle RESTRICTED calendars (data.restricted) var data = APP.calendars[id]; var edit; if (data.loading) { @@ -852,7 +851,7 @@ Messages.calendar_noNotification = "None"; makeLeftside(cal, $(leftside)); cal.on('beforeCreateSchedule', function(event) { - // XXX Recurrence (later) + // 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} @@ -1014,7 +1013,7 @@ Messages.calendar_noNotification = "None"; var getNotificationDropdown = function () { var ev = APP.editModalData; var calId = ev.selectedCal.id; - // XXX DEFAULT HERE [10] ==> 10 minutes before the event + // DEFAULT HERE [10] ==> 10 minutes before the event var oldReminders = Util.find(APP.calendars, [calId, 'content', 'content', ev.id, 'reminders']) || [10]; APP.notificationsEntries = []; var number = h('input.tui-full-calendar-content', {