pad.libre-service.eu-etherpad/src/static/js/changesettracker.js

223 lines
8.1 KiB
JavaScript
Raw Normal View History

/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
2011-03-26 14:10:41 +01:00
/**
* Copyright 2009 Google Inc.
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* http://www.apache.org/licenses/LICENSE-2.0
2011-07-07 19:59:34 +02:00
*
2011-03-26 14:10:41 +01:00
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
2020-11-23 19:24:19 +01:00
const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset');
2011-03-26 14:10:41 +01:00
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) {
2011-03-26 14:10:41 +01:00
// latest official text from server
2020-11-23 19:24:19 +01:00
let baseAText = Changeset.makeAText('\n');
2011-03-26 14:10:41 +01:00
// changes applied to baseText that have been submitted
2020-11-23 19:24:19 +01:00
let submittedChangeset = null;
2011-03-26 14:10:41 +01:00
// changes applied to submittedChangeset since it was prepared
2020-11-23 19:24:19 +01:00
let userChangeset = Changeset.identity(1);
2011-03-26 14:10:41 +01:00
// is the changesetTracker enabled
2020-11-23 19:24:19 +01:00
let tracking = false;
2011-03-26 14:10:41 +01:00
// stack state flag so that when we change the rep we don't
// handle the notification recursively. When setting, always
// unset in a "finally" block. When set to true, the setter
// takes change of userChangeset.
2020-11-23 19:24:19 +01:00
let applyingNonUserChanges = false;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let changeCallback = null;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let changeCallbackTimeout = null;
2011-07-07 19:59:34 +02:00
function setChangeCallbackTimeout() {
2011-03-26 14:10:41 +01:00
// can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists
// and if there isn't a timeout already scheduled.
2020-11-23 19:24:19 +01:00
if (changeCallback && changeCallbackTimeout === null) {
changeCallbackTimeout = scheduler.setTimeout(() => {
try {
2011-07-07 19:59:34 +02:00
changeCallback();
2020-11-23 19:24:19 +01:00
} catch (pseudoError) {} finally {
2011-07-07 19:59:34 +02:00
changeCallbackTimeout = null;
}
2011-03-26 14:10:41 +01:00
}, 0);
}
}
2020-11-23 19:24:19 +01:00
let self;
2011-03-26 14:10:41 +01:00
return self = {
2020-11-23 19:24:19 +01:00
isTracking() {
2011-07-07 19:59:34 +02:00
return tracking;
},
2020-11-23 19:24:19 +01:00
setBaseText(text) {
2011-03-26 14:10:41 +01:00
self.setBaseAttributedText(Changeset.makeAText(text), null);
},
2020-11-23 19:24:19 +01:00
setBaseAttributedText(atext, apoolJsonObj) {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
2011-03-26 14:10:41 +01:00
tracking = true;
baseAText = Changeset.cloneAText(atext);
2020-11-23 19:24:19 +01:00
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
2011-07-07 19:59:34 +02:00
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
2011-03-26 14:10:41 +01:00
}
submittedChangeset = null;
userChangeset = Changeset.identity(atext.text.length);
applyingNonUserChanges = true;
2020-11-23 19:24:19 +01:00
try {
2011-03-26 14:10:41 +01:00
callbacks.setDocumentAttributedText(atext);
2020-11-23 19:24:19 +01:00
} finally {
2011-07-07 19:59:34 +02:00
applyingNonUserChanges = false;
2011-03-26 14:10:41 +01:00
}
});
},
2020-11-23 19:24:19 +01:00
composeUserChangeset(c) {
2011-07-07 19:59:34 +02:00
if (!tracking) return;
2011-03-26 14:10:41 +01:00
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
userChangeset = Changeset.compose(userChangeset, c, apool);
setChangeCallbackTimeout();
},
2020-11-23 19:24:19 +01:00
applyChangesToBase(c, optAuthor, apoolJsonObj) {
2011-07-07 19:59:34 +02:00
if (!tracking) return;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
2011-07-07 19:59:34 +02:00
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
2011-03-26 14:10:41 +01:00
}
baseAText = Changeset.applyToAText(c, baseAText, apool);
2020-11-23 19:24:19 +01:00
let c2 = c;
if (submittedChangeset) {
const oldSubmittedChangeset = submittedChangeset;
2011-07-07 19:59:34 +02:00
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset;
2011-03-26 14:10:41 +01:00
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
2020-11-23 19:24:19 +01:00
const postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
2011-03-26 14:10:41 +01:00
applyingNonUserChanges = true;
2020-11-23 19:24:19 +01:00
try {
2011-03-26 14:10:41 +01:00
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
2020-11-23 19:24:19 +01:00
} finally {
2011-07-07 19:59:34 +02:00
applyingNonUserChanges = false;
2011-03-26 14:10:41 +01:00
}
});
},
2020-11-23 19:24:19 +01:00
prepareUserChangeset() {
2011-03-26 14:10:41 +01:00
// If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null.
2020-11-23 19:24:19 +01:00
let toSubmit;
if (submittedChangeset) {
2011-07-07 19:59:34 +02:00
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
2020-11-23 19:24:19 +01:00
} else {
// add forEach function to Array.prototype for IE8
2013-04-17 18:17:14 +02:00
if (!('forEach' in Array.prototype)) {
2020-11-23 19:24:19 +01:00
Array.prototype.forEach = function (action, that /* opt*/) {
for (let i = 0, n = this.length; i < n; i++) if (i in this) action.call(that, this[i], i, this);
2013-04-17 18:17:14 +02:00
};
}
// Get my authorID
2020-11-23 19:24:19 +01:00
const authorId = parent.parent.pad.myUserInfo.userId;
// Sanitize authorship
// We need to replace all author attribs with thisSession.author, in case they copy/pasted or otherwise inserted other peoples changes
2020-11-23 19:24:19 +01:00
if (apool.numToAttrib) {
for (const attr in apool.numToAttrib) {
if (apool.numToAttrib[attr][0] == 'author' && apool.numToAttrib[attr][1] == authorId) var authorAttr = Number(attr).toString(36);
}
// Replace all added 'author' attribs with the value of the current user
2020-11-23 19:24:19 +01:00
var cs = Changeset.unpack(userChangeset);
const iterator = Changeset.opIterator(cs.ops);
let op;
const assem = Changeset.mergingOpAssembler();
while (iterator.hasNext()) {
op = iterator.next();
if (op.opcode == '+') {
var newAttrs = '';
op.attribs.split('*').forEach((attrNum) => {
if (!attrNum) return;
const attr = apool.getAttrib(parseInt(attrNum, 36));
if (!attr) return;
if ('author' == attr[0]) {
// replace that author with the current one
2020-11-23 19:24:19 +01:00
newAttrs += `*${authorAttr}`;
} else { newAttrs += `*${attrNum}`; } // overtake all other attribs as is
});
op.attribs = newAttrs;
}
2020-11-23 19:24:19 +01:00
assem.append(op);
}
assem.endDocument();
2020-11-23 19:24:19 +01:00
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(userChangeset);
}
2011-07-07 19:59:34 +02:00
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
else toSubmit = userChangeset;
2011-03-26 14:10:41 +01:00
}
var cs = null;
2020-11-23 19:24:19 +01:00
if (toSubmit) {
2011-07-07 19:59:34 +02:00
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
2011-03-26 14:10:41 +01:00
2011-07-07 19:59:34 +02:00
cs = toSubmit;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
let wireApool = null;
if (cs) {
const forWire = Changeset.prepareForWire(cs, apool);
2011-07-07 19:59:34 +02:00
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const data = {
2011-07-07 19:59:34 +02:00
changeset: cs,
2020-11-23 19:24:19 +01:00
apool: wireApool,
2011-07-07 19:59:34 +02:00
};
2011-03-26 14:10:41 +01:00
return data;
},
2020-11-23 19:24:19 +01:00
applyPreparedChangesetToBase() {
if (!submittedChangeset) {
2011-07-07 19:59:34 +02:00
// violation of protocol; use prepareUserChangeset first
2020-11-23 19:24:19 +01:00
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
2011-03-26 14:10:41 +01:00
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
2020-11-23 19:24:19 +01:00
setUserChangeNotificationCallback(callback) {
2011-03-26 14:10:41 +01:00
changeCallback = callback;
},
2020-11-23 19:24:19 +01:00
hasUncommittedChanges() {
2011-07-07 19:59:34 +02:00
return !!(submittedChangeset || (!Changeset.isIdentity(userChangeset)));
2020-11-23 19:24:19 +01:00
},
2011-03-26 14:10:41 +01:00
};
}
exports.makeChangesetTracker = makeChangesetTracker;