Move all files to esm

This commit is contained in:
SamTV12345 2023-06-25 00:05:01 +02:00
parent 237f7242ec
commit 76a6f665a4
No known key found for this signature in database
GPG key ID: E63EEC7466038043
87 changed files with 23693 additions and 30732 deletions

1
node_modules/ep_etherpad-lite generated vendored
View file

@ -1 +0,0 @@
../src

View file

@ -4,7 +4,15 @@
*/ */
import AttributeMap from '../../static/js/AttributeMap'; import AttributeMap from '../../static/js/AttributeMap';
import {applyToAText, copyAText, makeAText} from '../../static/js/Changeset'; import {
applyToAText, checkRep,
copyAText, deserializeOps,
makeAText,
makeSplice,
opsFromAText,
pack,
smartOpAssembler, unpack
} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage'; import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool'; import {AttributePool} from '../../static/js/AttributePool';
import {Stream} from '../utils/Stream'; import {Stream} from '../utils/Stream';
@ -266,7 +274,7 @@ export class Pad {
(!ins && start > 0 && orig[start - 1] === '\n'); (!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n'; if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return; if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins); const changeset = makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId); await this.appendRevision(changeset, authorId);
} }
@ -366,7 +374,7 @@ export class Pad {
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`); if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = cleanText(context.content); text = cleanText(context.content);
} }
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text); const firstChangeset = makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId); await this.appendRevision(firstChangeset, authorId);
} }
await aCallAll('padLoad', {pad: this}); await aCallAll('padLoad', {pad: this});
@ -490,8 +498,8 @@ export class Pad {
const oldAText = this.atext; const oldAText = this.atext;
// based on Changeset.makeSplice // based on Changeset.makeSplice
const assem = Changeset.smartOpAssembler(); const assem = smartOpAssembler();
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op); for (const op of opsFromAText(oldAText)) assem.append(op);
assem.endDocument(); assem.endDocument();
// although we have instantiated the dstPad with '\n', an additional '\n' is // although we have instantiated the dstPad with '\n', an additional '\n' is
@ -503,7 +511,7 @@ export class Pad {
// create a changeset that removes the previous text and add the newText with // create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad // all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText); const changeset = pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId); dstPad.appendRevision(changeset, authorId);
await aCallAll('padCopy', { await aCallAll('padCopy', {
@ -677,7 +685,7 @@ export class Pad {
} }
}) })
.batch(100).buffer(99); .batch(100).buffer(99);
let atext = Changeset.makeAText('\n'); let atext = makeAText('\n');
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) { for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
try { try {
assert(authorId != null); assert(authorId != null);
@ -688,10 +696,10 @@ export class Pad {
assert(timestamp > 0); assert(timestamp > 0);
assert(changeset != null); assert(changeset != null);
assert.equal(typeof changeset, 'string'); assert.equal(typeof changeset, 'string');
Changeset.checkRep(changeset); checkRep(changeset);
const unpacked = Changeset.unpack(changeset); const unpacked = unpack(changeset);
let text = atext.text; let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) { for (const op of deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) { if (['=', '-'].includes(op.opcode)) {
assert(text.length >= op.chars); assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars); const consumed = text.slice(0, op.chars);
@ -702,7 +710,7 @@ export class Pad {
} }
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString()); assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
} }
atext = Changeset.applyToAText(changeset, atext, pool); atext = applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext); if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err) { } catch (err) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`; err.message = `(pad ${this.id} revision ${r}) ${err.message}`;

1553
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,18 @@
import AttributeMap from "./AttributeMap.js";
import * as Changeset from "./Changeset.js";
import * as ChangesetUtils from "./ChangesetUtils.js";
import * as attributes from "./attributes.js";
import * as _ from "./underscore.js";
'use strict'; 'use strict';
const AttributeMap = require('./AttributeMap');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes');
const _ = require('./underscore');
const lineMarkerAttribute = 'lmkr'; const lineMarkerAttribute = 'lmkr';
// Some of these attributes are kept for compatibility purposes. // Some of these attributes are kept for compatibility purposes.
// Not sure if we need all of them // Not sure if we need all of them
const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start'];
// If one of these attributes are set to the first character of a // If one of these attributes are set to the first character of a
// line it is considered as a line attribute marker i.e. attributes // line it is considered as a line attribute marker i.e. attributes
// set on this marker are applied to the whole line. // set on this marker are applied to the whole line.
// The list attribute is only maintained for compatibility reasons // The list attribute is only maintained for compatibility reasons
const lineAttributes = [lineMarkerAttribute, 'list']; const lineAttributes = [lineMarkerAttribute, 'list'];
/* /*
The Attribute manager builds changesets based on a document The Attribute manager builds changesets based on a document
representation for setting and removing range or line-based attributes. representation for setting and removing range or line-based attributes.
@ -32,32 +27,25 @@ const lineAttributes = [lineMarkerAttribute, 'list'];
- an Attribute pool `apool` - an Attribute pool `apool`
- a SkipList `lines` containing the text lines of the document. - a SkipList `lines` containing the text lines of the document.
*/ */
const AttributeManager = function (rep, applyChangesetCallback) { const AttributeManager = function (rep, applyChangesetCallback) {
this.rep = rep; this.rep = rep;
this.applyChangesetCallback = applyChangesetCallback; this.applyChangesetCallback = applyChangesetCallback;
this.author = ''; this.author = '';
// If the first char in a line has one of the following attributes // If the first char in a line has one of the following attributes
// it will be considered as a line marker // it will be considered as a line marker
}; };
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
AttributeManager.lineAttributes = lineAttributes; AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = _(AttributeManager.prototype).extend({ AttributeManager.prototype = _(AttributeManager.prototype).extend({
applyChangeset(changeset) { applyChangeset(changeset) {
if (!this.applyChangesetCallback) return changeset; if (!this.applyChangesetCallback)
return changeset;
const cs = changeset.toString(); const cs = changeset.toString();
if (!Changeset.isIdentity(cs)) { if (!Changeset.isIdentity(cs)) {
this.applyChangesetCallback(cs); this.applyChangesetCallback(cs);
} }
return changeset; return changeset;
}, },
/* /*
Sets attributes on a range Sets attributes on a range
@param start [row, col] tuple pointing to the start of the range @param start [row, col] tuple pointing to the start of the range
@ -65,14 +53,17 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
@param attribs: an array of attributes @param attribs: an array of attributes
*/ */
setAttributesOnRange(start, end, attribs) { setAttributesOnRange(start, end, attribs) {
if (start[0] < 0) throw new RangeError('selection start line number is negative'); if (start[0] < 0)
if (start[1] < 0) throw new RangeError('selection start column number is negative'); throw new RangeError('selection start line number is negative');
if (end[0] < 0) throw new RangeError('selection end line number is negative'); if (start[1] < 0)
if (end[1] < 0) throw new RangeError('selection end column number is negative'); throw new RangeError('selection start column number is negative');
if (end[0] < 0)
throw new RangeError('selection end line number is negative');
if (end[1] < 0)
throw new RangeError('selection end column number is negative');
if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) { if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) {
throw new RangeError('selection ends before it starts'); throw new RangeError('selection ends before it starts');
} }
// instead of applying the attributes to the whole range at once, we need to apply them // instead of applying the attributes to the whole range at once, we need to apply them
// line by line, to be able to disregard the "*" used as line marker. For more details, // line by line, to be able to disregard the "*" used as line marker. For more details,
// see https://github.com/ether/etherpad-lite/issues/2772 // see https://github.com/ether/etherpad-lite/issues/2772
@ -80,43 +71,42 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
for (let row = start[0]; row <= end[0]; row++) { for (let row = start[0]; row <= end[0]; row++) {
const [startCol, endCol] = this._findRowRange(row, start, end); const [startCol, endCol] = this._findRowRange(row, start, end);
const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs);
// compose changesets of all rows into a single changeset // compose changesets of all rows into a single changeset
// as the range might not be continuous // as the range might not be continuous
// due to the presence of line markers on the rows // due to the presence of line markers on the rows
if (allChangesets) { if (allChangesets) {
allChangesets = Changeset.compose( allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
allChangesets.toString(), rowChangeset.toString(), this.rep.apool); }
} else { else {
allChangesets = rowChangeset; allChangesets = rowChangeset;
} }
} }
return this.applyChangeset(allChangesets); return this.applyChangeset(allChangesets);
}, },
_findRowRange(row, start, end) { _findRowRange(row, start, end) {
if (row < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`); if (row < start[0] || row > end[0])
if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`); throw new RangeError(`line ${row} not in selection`);
if (row >= this.rep.lines.length())
throw new RangeError(`selected line ${row} does not exist`);
// Subtract 1 for the end-of-line '\n' (it is never selected). // Subtract 1 for the end-of-line '\n' (it is never selected).
const lineLength = const lineLength = this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
const markerWidth = this.lineHasMarker(row) ? 1 : 0; const markerWidth = this.lineHasMarker(row) ? 1 : 0;
if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); if (lineLength - markerWidth < 0)
throw new Error(`line ${row} has negative length`);
if (start[1] < 0) throw new RangeError('selection starts at negative column'); if (start[1] < 0)
throw new RangeError('selection starts at negative column');
const startCol = Math.max(markerWidth, row === start[0] ? start[1] : 0); const startCol = Math.max(markerWidth, row === start[0] ? start[1] : 0);
if (startCol > lineLength) throw new RangeError('selection starts after line end'); if (startCol > lineLength)
throw new RangeError('selection starts after line end');
if (end[1] < 0) throw new RangeError('selection ends at negative column'); if (end[1] < 0)
throw new RangeError('selection ends at negative column');
const endCol = Math.max(markerWidth, row === end[0] ? end[1] : lineLength); const endCol = Math.max(markerWidth, row === end[0] ? end[1] : lineLength);
if (endCol > lineLength) throw new RangeError('selection ends after line end'); if (endCol > lineLength)
if (startCol > endCol) throw new RangeError('selection ends before it starts'); throw new RangeError('selection ends after line end');
if (startCol > endCol)
throw new RangeError('selection ends before it starts');
return [startCol, endCol]; return [startCol, endCol];
}, },
/** /**
* Sets attributes on a range, by line * Sets attributes on a range, by line
* @param row the row where range is * @param row the row where range is
@ -127,20 +117,16 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) { _setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange( ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
return builder; return builder;
}, },
/* /*
Returns if the line already has a line marker Returns if the line already has a line marker
@param lineNum: the number of the line @param lineNum: the number of the line
*/ */
lineHasMarker(lineNum) { lineHasMarker(lineNum) {
return lineAttributes.find( return lineAttributes.find((attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
}, },
/* /*
Gets a specified attribute on a line Gets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for @param lineNum: the number of the line to set the attribute for
@ -149,12 +135,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributeOnLine(lineNum, attributeName) { getAttributeOnLine(lineNum, attributeName) {
// get `attributeName` attribute of first char of line // get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return ''; if (!aline)
return '';
const [op] = Changeset.deserializeOps(aline); const [op] = Changeset.deserializeOps(aline);
if (op == null) return ''; if (op == null)
return '';
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
}, },
/* /*
Gets all attributes on a line Gets all attributes on a line
@param lineNum: the number of the line to get the attribute for @param lineNum: the number of the line to get the attribute for
@ -162,12 +149,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnLine(lineNum) { getAttributesOnLine(lineNum) {
// get attributes of first char of line // get attributes of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return []; if (!aline)
return [];
const [op] = Changeset.deserializeOps(aline); const [op] = Changeset.deserializeOps(aline);
if (op == null) return []; if (op == null)
return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
}, },
/* /*
Gets a given attribute on a selection Gets a given attribute on a selection
@param attributeName @param attributeName
@ -176,7 +164,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
*/ */
getAttributeOnSelection(attributeName, prevChar) { getAttributeOnSelection(attributeName, prevChar) {
const rep = this.rep; const rep = this.rep;
if (!(rep.selStart && rep.selEnd)) return; if (!(rep.selStart && rep.selEnd))
return;
// If we're looking for the caret attribute not the selection // If we're looking for the caret attribute not the selection
// has the user already got a selection or is this purely a caret location? // has the user already got a selection or is this purely a caret location?
const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]); const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
@ -188,38 +177,29 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
} }
} }
} }
const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString(); const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString();
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
const hasIt = (attribs) => withItRegex.test(attribs); const hasIt = (attribs) => withItRegex.test(attribs);
const rangeHasAttrib = (selStart, selEnd) => { const rangeHasAttrib = (selStart, selEnd) => {
// if range is collapsed -> no attribs in range // if range is collapsed -> no attribs in range
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0])
return false;
if (selStart[0] !== selEnd[0]) { // -> More than one line selected if (selStart[0] !== selEnd[0]) { // -> More than one line selected
// from selStart to the end of the first line // from selStart to the end of the first line
let hasAttrib = rangeHasAttrib( let hasAttrib = rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
// for all lines in between // for all lines in between
for (let n = selStart[0] + 1; n < selEnd[0]; n++) { for (let n = selStart[0] + 1; n < selEnd[0]; n++) {
hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]); hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]);
} }
// for the last, potentially partial, line // for the last, potentially partial, line
hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]); hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]);
return hasAttrib; return hasAttrib;
} }
// Logic tells us we now have a range on a single line // Logic tells us we now have a range on a single line
const lineNum = selStart[0]; const lineNum = selStart[0];
const start = selStart[1]; const start = selStart[1];
const end = selEnd[1]; const end = selEnd[1];
let hasAttrib = true; let hasAttrib = true;
let indexIntoLine = 0; let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine; const opStartInLine = indexIntoLine;
@ -234,12 +214,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
} }
indexIntoLine = opEndInLine; indexIntoLine = opEndInLine;
} }
return hasAttrib; return hasAttrib;
}; };
return rangeHasAttrib(rep.selStart, rep.selEnd); return rangeHasAttrib(rep.selStart, rep.selEnd);
}, },
/* /*
Gets all attributes at a position containing line number and column Gets all attributes at a position containing line number and column
@param lineNumber starting with zero @param lineNumber starting with zero
@ -250,22 +228,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnPosition(lineNumber, column) { getAttributesOnPosition(lineNumber, column) {
// get all attributes of the line // get all attributes of the line
const aline = this.rep.alines[lineNumber]; const aline = this.rep.alines[lineNumber];
if (!aline) { if (!aline) {
return []; return [];
} }
// we need to sum up how much characters each operations take until the wanted position // we need to sum up how much characters each operations take until the wanted position
let currentPointer = 0; let currentPointer = 0;
for (const currentOperation of Changeset.deserializeOps(aline)) { for (const currentOperation of Changeset.deserializeOps(aline)) {
currentPointer += currentOperation.chars; currentPointer += currentOperation.chars;
if (currentPointer <= column) continue; if (currentPointer <= column)
continue;
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
} }
return []; return [];
}, },
/* /*
Gets all attributes at caret position Gets all attributes at caret position
if the user selected a range, the start of the selection is taken if the user selected a range, the start of the selection is taken
@ -275,7 +250,6 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnCaret() { getAttributesOnCaret() {
return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
}, },
/* /*
Sets a specified attribute on a line Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for @param lineNum: the number of the line to set the attribute for
@ -287,14 +261,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
let loc = [0, 0]; let loc = [0, 0];
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum); const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
if (hasMarker) { if (hasMarker) {
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [ ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
[attributeName, attributeValue], [attributeName, attributeValue],
], this.rep.apool); ], this.rep.apool);
} else { }
else {
// add a line marker // add a line marker
builder.insert('*', [ builder.insert('*', [
['author', this.author], ['author', this.author],
@ -303,10 +276,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
[attributeName, attributeValue], [attributeName, attributeValue],
], this.rep.apool); ], this.rep.apool);
} }
return this.applyChangeset(builder); return this.applyChangeset(builder);
}, },
/** /**
* Removes a specified attribute on a line * Removes a specified attribute on a line
* @param lineNum the number of the affected line * @param lineNum the number of the affected line
@ -317,38 +288,32 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum); const hasMarker = this.lineHasMarker(lineNum);
let found = false; let found = false;
const attribs = this.getAttributesOnLine(lineNum).map((attrib) => { const attribs = this.getAttributesOnLine(lineNum).map((attrib) => {
if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) { if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) {
found = true; found = true;
return [attrib[0], '']; return [attrib[0], ''];
} else if (attrib[0] === 'author') { }
else if (attrib[0] === 'author') {
// update last author to make changes to line attributes on this line // update last author to make changes to line attributes on this line
return [attrib[0], this.author]; return [attrib[0], this.author];
} }
return attrib; return attrib;
}); });
if (!found) { if (!found) {
return; return;
} }
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1]) const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1])
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); .map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
// if we have marker and any of attributes don't need to have marker. we need delete it // if we have marker and any of attributes don't need to have marker. we need delete it
if (hasMarker && !countAttribsWithMarker) { if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else {
ChangesetUtils.buildKeepRange(
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
} }
else {
ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}
return this.applyChangeset(builder); return this.applyChangeset(builder);
}, },
/* /*
Toggles a line attribute for the specified line number Toggles a line attribute for the specified line number
If a line attribute with the specified name exists with any value it will be removed If a line attribute with the specified name exists with any value it will be removed
@ -362,15 +327,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
? this.removeAttributeOnLine(lineNum, attributeName) ? this.removeAttributeOnLine(lineNum, attributeName)
: this.setAttributeOnLine(lineNum, attributeName, attributeValue); : this.setAttributeOnLine(lineNum, attributeName, attributeValue);
}, },
hasAttributeOnSelectionOrCaretPosition(attributeName) { hasAttributeOnSelectionOrCaretPosition(attributeName) {
const hasSelection = ( const hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1]));
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])
);
let hasAttrib; let hasAttrib;
if (hasSelection) { if (hasSelection) {
hasAttrib = this.getAttributeOnSelection(attributeName); hasAttrib = this.getAttributeOnSelection(attributeName);
} else { }
else {
const attributesOnCaretPosition = this.getAttributesOnCaret(); const attributesOnCaretPosition = this.getAttributesOnCaret();
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
hasAttrib = allAttribs.includes(attributeName); hasAttrib = allAttribs.includes(attributeName);
@ -378,5 +341,4 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
return hasAttrib; return hasAttrib;
}, },
}); });
export default AttributeManager;
module.exports = AttributeManager;

View file

@ -1,13 +1,10 @@
import * as attributes from "./attributes.js";
'use strict'; 'use strict';
const attributes = require('./attributes');
/** /**
* A `[key, value]` pair of strings describing a text attribute. * A `[key, value]` pair of strings describing a text attribute.
* *
* @typedef {[string, string]} Attribute * @typedef {[string, string]} Attribute
*/ */
/** /**
* A concatenated sequence of zero or more attribute identifiers, each one represented by an * A concatenated sequence of zero or more attribute identifiers, each one represented by an
* asterisk followed by a base-36 encoded attribute number. * asterisk followed by a base-36 encoded attribute number.
@ -16,7 +13,6 @@ const attributes = require('./attributes');
* *
* @typedef {string} AttributeString * @typedef {string} AttributeString
*/ */
/** /**
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs. * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
*/ */
@ -31,7 +27,6 @@ class AttributeMap extends Map {
static fromString(str, pool) { static fromString(str, pool) {
return new AttributeMap(pool).updateFromString(str); return new AttributeMap(pool).updateFromString(str);
} }
/** /**
* @param {AttributePool} pool - Attribute pool. * @param {AttributePool} pool - Attribute pool.
*/ */
@ -40,7 +35,6 @@ class AttributeMap extends Map {
/** @public */ /** @public */
this.pool = pool; this.pool = pool;
} }
/** /**
* @param {string} k - Attribute name. * @param {string} k - Attribute name.
* @param {string} v - Attribute value. * @param {string} v - Attribute value.
@ -52,11 +46,9 @@ class AttributeMap extends Map {
this.pool.putAttrib([k, v]); this.pool.putAttrib([k, v]);
return super.set(k, v); return super.set(k, v);
} }
toString() { toString() {
return attributes.attribsToString(attributes.sort([...this]), this.pool); return attributes.attribsToString(attributes.sort([...this]), this.pool);
} }
/** /**
* @param {Iterable<Attribute>} entries - [key, value] pairs to insert into this map. * @param {Iterable<Attribute>} entries - [key, value] pairs to insert into this map.
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the * @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
@ -69,13 +61,13 @@ class AttributeMap extends Map {
v = v == null ? '' : String(v); v = v == null ? '' : String(v);
if (!v && emptyValueIsDelete) { if (!v && emptyValueIsDelete) {
this.delete(k); this.delete(k);
} else { }
else {
this.set(k, v); this.set(k, v);
} }
} }
return this; return this;
} }
/** /**
* @param {AttributeString} str - The attribute string identifying the attributes to insert into * @param {AttributeString} str - The attribute string identifying the attributes to insert into
* this map. * this map.
@ -87,5 +79,4 @@ class AttributeMap extends Map {
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete); return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
} }
} }
export default AttributeMap;
module.exports = AttributeMap;

View file

@ -1,52 +1,28 @@
'use strict'; 'use strict';
export const buildRemoveRange = (rep, builder, start, end) => {
/**
* This module contains several helper Functions to build Changesets
* based on a SkipList
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
exports.buildRemoveRange = (rep, builder, start, end) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]); const endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0]) { if (end[0] > start[0]) {
builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]); builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
builder.remove(end[1]); builder.remove(end[1]);
} else { }
else {
builder.remove(end[1] - start[1]); builder.remove(end[1] - start[1]);
} }
}; };
export const buildKeepRange = (rep, builder, start, end, attribs, pool) => {
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]); const endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0]) { if (end[0] > start[0]) {
builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool); builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
builder.keep(end[1], 0, attribs, pool); builder.keep(end[1], 0, attribs, pool);
} else { }
else {
builder.keep(end[1] - start[1], 0, attribs, pool); builder.keep(end[1] - start[1], 0, attribs, pool);
} }
}; };
export const buildKeepToStartOfRange = (rep, builder, start) => {
exports.buildKeepToStartOfRange = (rep, builder, start) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]); builder.keep(startLineOffset, start[0]);
builder.keep(start[1]); builder.keep(start[1]);
}; };

View file

@ -1,7 +1,6 @@
import { padutils } from "./pad_utils.js";
'use strict'; 'use strict';
const { padutils: { warnDeprecated } } = { padutils };
const {padutils: {warnDeprecated}} = require('./pad_utils');
/** /**
* Represents a chat message stored in the database and transmitted among users. Plugins can extend * Represents a chat message stored in the database and transmitted among users. Plugins can extend
* the object with additional properties. * the object with additional properties.
@ -13,13 +12,14 @@ class ChatMessage {
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept // The userId property was renamed to authorId, and userName was renamed to displayName. Accept
// the old names in case the db record was written by an older version of Etherpad. // the old names in case the db record was written by an older version of Etherpad.
obj = Object.assign({}, obj); // Don't mutate the caller's object. obj = Object.assign({}, obj); // Don't mutate the caller's object.
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId; if ('userId' in obj && !('authorId' in obj))
obj.authorId = obj.userId;
delete obj.userId; delete obj.userId;
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName; if ('userName' in obj && !('displayName' in obj))
obj.displayName = obj.userName;
delete obj.userName; delete obj.userName;
return Object.assign(new ChatMessage(), obj); return Object.assign(new ChatMessage(), obj);
} }
/** /**
* @param {?string} [text] - Initial value of the `text` property. * @param {?string} [text] - Initial value of the `text` property.
* @param {?string} [authorId] - Initial value of the `authorId` property. * @param {?string} [authorId] - Initial value of the `authorId` property.
@ -32,21 +32,18 @@ class ChatMessage {
* @type {?string} * @type {?string}
*/ */
this.text = text; this.text = text;
/** /**
* The user's author ID. * The user's author ID.
* *
* @type {?string} * @type {?string}
*/ */
this.authorId = authorId; this.authorId = authorId;
/** /**
* The message's timestamp, as milliseconds since epoch. * The message's timestamp, as milliseconds since epoch.
* *
* @type {?number} * @type {?number}
*/ */
this.time = time; this.time = time;
/** /**
* The user's display name. * The user's display name.
* *
@ -54,7 +51,6 @@ class ChatMessage {
*/ */
this.displayName = null; this.displayName = null;
} }
/** /**
* Alias of `authorId`, for compatibility with old plugins. * Alias of `authorId`, for compatibility with old plugins.
* *
@ -69,7 +65,6 @@ class ChatMessage {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
this.authorId = val; this.authorId = val;
} }
/** /**
* Alias of `displayName`, for compatibility with old plugins. * Alias of `displayName`, for compatibility with old plugins.
* *
@ -84,7 +79,6 @@ class ChatMessage {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
this.displayName = val; this.displayName = val;
} }
// TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that // TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that
// doesn't support authorId and displayName. // doesn't support authorId and displayName.
toJSON() { toJSON() {
@ -94,5 +88,4 @@ class ChatMessage {
return obj; return obj;
} }
} }
export default ChatMessage;
module.exports = ChatMessage;

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,12 @@
// Autosize 1.13 - jQuery plugin for textareas // Autosize 1.13 - jQuery plugin for textareas
// (c) 2012 Jack Moore - jacklmoore.com // (c) 2012 Jack Moore - jacklmoore.com
// license: www.opensource.org/licenses/mit-license.php // license: www.opensource.org/licenses/mit-license.php
(function ($) { (function ($) {
var var defaults = {
defaults = {
className: 'autosizejs', className: 'autosizejs',
append: "", append: "",
callback: false callback: false
}, }, hidden = 'hidden', borderBox = 'border-box', lineHeight = 'lineHeight', copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
hidden = 'hidden',
borderBox = 'border-box',
lineHeight = 'lineHeight',
copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
// line-height is omitted because IE7/IE8 doesn't return the correct value. // line-height is omitted because IE7/IE8 doesn't return the correct value.
copyStyle = [ copyStyle = [
'fontFamily', 'fontFamily',
@ -23,53 +17,31 @@
'textTransform', 'textTransform',
'wordSpacing', 'wordSpacing',
'textIndent' 'textIndent'
], ], oninput = 'oninput', onpropertychange = 'onpropertychange', test = $(copy)[0];
oninput = 'oninput',
onpropertychange = 'onpropertychange',
test = $(copy)[0];
// For testing support in old FireFox // For testing support in old FireFox
test.setAttribute(oninput, "return"); test.setAttribute(oninput, "return");
if ($.isFunction(test[oninput]) || onpropertychange in test) { if ($.isFunction(test[oninput]) || onpropertychange in test) {
// test that line-height can be accurately copied to avoid // test that line-height can be accurately copied to avoid
// incorrect value reporting in old IE and old Opera // incorrect value reporting in old IE and old Opera
$(test).css(lineHeight, '99px'); $(test).css(lineHeight, '99px');
if ($(test).css(lineHeight) === '99px') { if ($(test).css(lineHeight) === '99px') {
copyStyle.push(lineHeight); copyStyle.push(lineHeight);
} }
$.fn.autosize = function (options) { $.fn.autosize = function (options) {
options = $.extend({}, defaults, options || {}); options = $.extend({}, defaults, options || {});
return this.each(function () { return this.each(function () {
var var ta = this, $ta = $(ta), mirror, minHeight = $ta.height(), maxHeight = parseInt($ta.css('maxHeight'), 10), active, i = copyStyle.length, resize, boxOffset = 0, value = ta.value, callback = $.isFunction(options.callback);
ta = this,
$ta = $(ta),
mirror,
minHeight = $ta.height(),
maxHeight = parseInt($ta.css('maxHeight'), 10),
active,
i = copyStyle.length,
resize,
boxOffset = 0,
value = ta.value,
callback = $.isFunction(options.callback);
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox) { if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox) {
boxOffset = $ta.outerHeight() - $ta.height(); boxOffset = $ta.outerHeight() - $ta.height();
} }
if ($ta.data('mirror') || $ta.data('ismirror')) { if ($ta.data('mirror') || $ta.data('ismirror')) {
// if autosize has already been applied, exit. // if autosize has already been applied, exit.
// if autosize is being applied to a mirror element, exit. // if autosize is being applied to a mirror element, exit.
return; return;
} else { }
else {
mirror = $(copy).data('ismirror', true).addClass(options.className)[0]; mirror = $(copy).data('ismirror', true).addClass(options.className)[0];
resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal'; resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal';
$ta.data('mirror', $(mirror)).css({ $ta.data('mirror', $(mirror)).css({
overflow: hidden, overflow: hidden,
overflowY: hidden, overflowY: hidden,
@ -77,15 +49,12 @@
resize: resize resize: resize
}); });
} }
// Opera returns '-1px' when max-height is set to 'none'. // Opera returns '-1px' when max-height is set to 'none'.
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4; maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
// Using mainly bare JS in this function because it is going // Using mainly bare JS in this function because it is going
// to fire very often while typing, and needs to very efficient. // to fire very often while typing, and needs to very efficient.
function adjust() { function adjust() {
var height, overflow, original; var height, overflow, original;
// the active flag keeps IE from tripping all over itself. Otherwise // the active flag keeps IE from tripping all over itself. Otherwise
// actions in the adjust function will cause IE to call adjust again. // actions in the adjust function will cause IE to call adjust again.
if (!active) { if (!active) {
@ -93,35 +62,30 @@
mirror.value = ta.value + options.append; mirror.value = ta.value + options.append;
mirror.style.overflowY = ta.style.overflowY; mirror.style.overflowY = ta.style.overflowY;
original = parseInt(ta.style.height, 10); original = parseInt(ta.style.height, 10);
// Update the width in case the original textarea width has changed // Update the width in case the original textarea width has changed
mirror.style.width = $ta.css('width'); mirror.style.width = $ta.css('width');
// Needed for IE to reliably return the correct scrollHeight // Needed for IE to reliably return the correct scrollHeight
mirror.scrollTop = 0; mirror.scrollTop = 0;
// Set a very high value for scrollTop to be sure the // Set a very high value for scrollTop to be sure the
// mirror is scrolled all the way to the bottom. // mirror is scrolled all the way to the bottom.
mirror.scrollTop = 9e4; mirror.scrollTop = 9e4;
height = mirror.scrollTop; height = mirror.scrollTop;
overflow = hidden; overflow = hidden;
if (height > maxHeight) { if (height > maxHeight) {
height = maxHeight; height = maxHeight;
overflow = 'scroll'; overflow = 'scroll';
} else if (height < minHeight) { }
else if (height < minHeight) {
height = minHeight; height = minHeight;
} }
height += boxOffset; height += boxOffset;
ta.style.overflowY = overflow; ta.style.overflowY = overflow;
if (original !== height) { if (original !== height) {
ta.style.height = height + 'px'; ta.style.height = height + 'px';
if (callback) { if (callback) {
options.callback.call(ta); options.callback.call(ta);
} }
} }
// This small timeout gives IE a chance to draw it's scrollbar // This small timeout gives IE a chance to draw it's scrollbar
// before adjust can be run again (prevents an infinite loop). // before adjust can be run again (prevents an infinite loop).
setTimeout(function () { setTimeout(function () {
@ -129,7 +93,6 @@
}, 1); }, 1);
} }
} }
// mirror is a duplicate textarea located off-screen that // mirror is a duplicate textarea located off-screen that
// is automatically updated to contain the same text as the // is automatically updated to contain the same text as the
// original textarea. mirror always has a height of 0. // original textarea. mirror always has a height of 0.
@ -138,43 +101,39 @@
while (i--) { while (i--) {
mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]); mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]);
} }
$('body').append(mirror); $('body').append(mirror);
if (onpropertychange in ta) { if (onpropertychange in ta) {
if (oninput in ta) { if (oninput in ta) {
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions, // Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those occassions. There is no way that I // so binding to onkeyup to catch most of those occassions. There is no way that I
// know of to detect something like 'cut' in IE9. // know of to detect something like 'cut' in IE9.
ta[oninput] = ta.onkeyup = adjust; ta[oninput] = ta.onkeyup = adjust;
} else { }
else {
// IE7 / IE8 // IE7 / IE8
ta[onpropertychange] = adjust; ta[onpropertychange] = adjust;
} }
} else { }
else {
// Modern Browsers // Modern Browsers
ta[oninput] = adjust; ta[oninput] = adjust;
// The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed. // The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed.
// This is a hack to get Chrome to reflow it's text. // This is a hack to get Chrome to reflow it's text.
ta.value = ''; ta.value = '';
ta.value = value; ta.value = value;
} }
$(window).resize(adjust); $(window).resize(adjust);
// Allow for manual triggering if needed. // Allow for manual triggering if needed.
$ta.bind('autosize', adjust); $ta.bind('autosize', adjust);
// Call adjust in case the textarea already contains text. // Call adjust in case the textarea already contains text.
adjust(); adjust();
}); });
}; };
} else { }
else {
// Makes no changes for older browsers (FireFox3- and Safari4-) // Makes no changes for older browsers (FireFox3- and Safari4-)
$.fn.autosize = function (callback) { $.fn.autosize = function (callback) {
return this; return this;
}; };
} }
}(jQuery)); }(jQuery));

View file

@ -2,23 +2,13 @@
v0.1 (c) Kyle Simpson v0.1 (c) Kyle Simpson
MIT License MIT License
*/ */
(function (global) { (function (global) {
if (typeof global.JSON == "undefined" || !global.JSON) { if (typeof global.JSON == "undefined" || !global.JSON) {
global.JSON = {}; global.JSON = {};
} }
global.JSON.minify = function (json) { global.JSON.minify = function (json) {
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g, in_string = false, in_multiline_comment = false, in_singleline_comment = false, tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc;
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
in_string = false,
in_multiline_comment = false,
in_singleline_comment = false,
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
;
tokenizer.lastIndex = 0; tokenizer.lastIndex = 0;
while (tmp = tokenizer.exec(json)) { while (tmp = tokenizer.exec(json)) {
lc = RegExp.leftContext; lc = RegExp.leftContext;
rc = RegExp.rightContext; rc = RegExp.rightContext;
@ -30,7 +20,6 @@
new_str[ns++] = tmp2; new_str[ns++] = tmp2;
} }
from = tokenizer.lastIndex; from = tokenizer.lastIndex;
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) { if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.match(/(\\)*$/); tmp2 = lc.match(/(\\)*$/);
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string

View file

@ -1,15 +1,13 @@
'use strict'; 'use strict';
/* global socketio */ /* global socketio */
$(document).ready(() => { $(document).ready(() => {
const socket = socketio.connect('..', '/pluginfw/installer'); const socket = socketio.connect('..', '/pluginfw/installer');
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason === 'io server disconnect') socket.connect(); if (reason === 'io server disconnect')
socket.connect();
}); });
const search = (searchTerm, limit) => { const search = (searchTerm, limit) => {
if (search.searchTerm !== searchTerm) { if (search.searchTerm !== searchTerm) {
search.offset = 0; search.offset = 0;
@ -26,7 +24,6 @@ $(document).ready(() => {
sortDir: search.sortDir, sortDir: search.sortDir,
}); });
search.offset += limit; search.offset += limit;
$('#search-progress').show(); $('#search-progress').show();
search.messages.show('fetching'); search.messages.show('fetching');
search.searching = true; search.searching = true;
@ -50,7 +47,6 @@ $(document).ready(() => {
$(`.search-results .messages .${msg} *`).hide(); $(`.search-results .messages .${msg} *`).hide();
}, },
}; };
const installed = { const installed = {
progress: { progress: {
show: (plugin, msg) => { show: (plugin, msg) => {
@ -77,11 +73,9 @@ $(document).ready(() => {
}, },
list: [], list: [],
}; };
const displayPluginList = (plugins, container, template) => { const displayPluginList = (plugins, container, template) => {
plugins.forEach((plugin) => { plugins.forEach((plugin) => {
const row = template.clone(); const row = template.clone();
for (const attr in plugin) { for (const attr in plugin) {
if (attr === 'name') { // Hack to rewrite URLS into name if (attr === 'name') { // Hack to rewrite URLS into name
const link = $('<a>') const link = $('<a>')
@ -91,7 +85,8 @@ $(document).ready(() => {
.attr('target', '_blank') .attr('target', '_blank')
.text(plugin.name.substr(3)); .text(plugin.name.substr(3));
row.find('.name').append(link); row.find('.name').append(link);
} else { }
else {
row.find(`.${attr}`).text(plugin[attr]); row.find(`.${attr}`).text(plugin[attr]);
} }
} }
@ -102,23 +97,21 @@ $(document).ready(() => {
}); });
updateHandlers(); updateHandlers();
}; };
const sortPluginList = (plugins, property, /* ASC?*/ dir) => plugins.sort((a, b) => { const sortPluginList = (plugins, property, /* ASC?*/ dir) => plugins.sort((a, b) => {
if (a[property] < b[property]) return dir ? -1 : 1; if (a[property] < b[property])
if (a[property] > b[property]) return dir ? 1 : -1; return dir ? -1 : 1;
if (a[property] > b[property])
return dir ? 1 : -1;
// a must be equal to b // a must be equal to b
return 0; return 0;
}); });
const updateHandlers = () => { const updateHandlers = () => {
// Search // Search
$('#search-query').unbind('keyup').keyup(() => { $('#search-query').unbind('keyup').keyup(() => {
search($('#search-query').val()); search($('#search-query').val());
}); });
// Prevent form submit // Prevent form submit
$('#search-query').parent().bind('submit', () => false); $('#search-query').parent().bind('submit', () => false);
// update & install // update & install
$('.do-install, .do-update').unbind('click').click(function (e) { $('.do-install, .do-update').unbind('click').click(function (e) {
const $row = $(e.target).closest('tr'); const $row = $(e.target).closest('tr');
@ -126,13 +119,13 @@ $(document).ready(() => {
if ($(this).hasClass('do-install')) { if ($(this).hasClass('do-install')) {
$row.remove().appendTo('#installed-plugins'); $row.remove().appendTo('#installed-plugins');
installed.progress.show(plugin, 'Installing'); installed.progress.show(plugin, 'Installing');
} else { }
else {
installed.progress.show(plugin, 'Updating'); installed.progress.show(plugin, 'Updating');
} }
socket.emit('install', plugin); socket.emit('install', plugin);
installed.messages.hide('nothing-installed'); installed.messages.hide('nothing-installed');
}); });
// uninstall // uninstall
$('.do-uninstall').unbind('click').click((e) => { $('.do-uninstall').unbind('click').click((e) => {
const $row = $(e.target).closest('tr'); const $row = $(e.target).closest('tr');
@ -141,7 +134,6 @@ $(document).ready(() => {
installed.progress.show(pluginName, 'Uninstalling'); installed.progress.show(pluginName, 'Uninstalling');
installed.list = installed.list.filter((plugin) => plugin.name !== pluginName); installed.list = installed.list.filter((plugin) => plugin.name !== pluginName);
}); });
// Sort // Sort
$('.sort.up').unbind('click').click(function () { $('.sort.up').unbind('click').click(function () {
search.sortBy = $(this).attr('data-label').toLowerCase(); search.sortBy = $(this).attr('data-label').toLowerCase();
@ -158,19 +150,17 @@ $(document).ready(() => {
search.results = []; search.results = [];
}); });
}; };
socket.on('results:search', (data) => { socket.on('results:search', (data) => {
if (!data.results.length) search.end = true; if (!data.results.length)
if (data.query.offset === 0) search.results = []; search.end = true;
if (data.query.offset === 0)
search.results = [];
search.messages.hide('nothing-found'); search.messages.hide('nothing-found');
search.messages.hide('fetching'); search.messages.hide('fetching');
$('#search-query').removeAttr('disabled'); $('#search-query').removeAttr('disabled');
console.log('got search results', data); console.log('got search results', data);
// add to results // add to results
search.results = search.results.concat(data.results); search.results = search.results.concat(data.results);
// Update sorting head // Update sorting head
$('.sort') $('.sort')
.removeClass('up down') .removeClass('up down')
@ -178,54 +168,46 @@ $(document).ready(() => {
$(`.search-results thead th[data-label=${data.query.sortBy}]`) $(`.search-results thead th[data-label=${data.query.sortBy}]`)
.removeClass('none') .removeClass('none')
.addClass(data.query.sortDir ? 'up' : 'down'); .addClass(data.query.sortDir ? 'up' : 'down');
// re-render search results // re-render search results
const searchWidget = $('.search-results'); const searchWidget = $('.search-results');
searchWidget.find('.results *').remove(); searchWidget.find('.results *').remove();
if (search.results.length > 0) { if (search.results.length > 0) {
displayPluginList( displayPluginList(search.results, searchWidget.find('.results'), searchWidget.find('.template tr'));
search.results, searchWidget.find('.results'), searchWidget.find('.template tr')); }
} else { else {
search.messages.show('nothing-found'); search.messages.show('nothing-found');
} }
search.messages.hide('fetching'); search.messages.hide('fetching');
$('#search-progress').hide(); $('#search-progress').hide();
search.searching = false; search.searching = false;
}); });
socket.on('results:installed', (data) => { socket.on('results:installed', (data) => {
installed.messages.hide('fetching'); installed.messages.hide('fetching');
installed.messages.hide('nothing-installed'); installed.messages.hide('nothing-installed');
installed.list = data.installed; installed.list = data.installed;
sortPluginList(installed.list, 'name', /* ASC?*/ true); sortPluginList(installed.list, 'name', /* ASC?*/ true);
// filter out epl // filter out epl
installed.list = installed.list.filter((plugin) => plugin.name !== 'ep_etherpad-lite'); installed.list = installed.list.filter((plugin) => plugin.name !== 'ep_etherpad-lite');
// remove all installed plugins (leave plugins that are still being installed) // remove all installed plugins (leave plugins that are still being installed)
installed.list.forEach((plugin) => { installed.list.forEach((plugin) => {
$(`#installed-plugins .${plugin.name}`).remove(); $(`#installed-plugins .${plugin.name}`).remove();
}); });
if (installed.list.length > 0) { if (installed.list.length > 0) {
displayPluginList(installed.list, $('#installed-plugins'), $('#installed-plugin-template')); displayPluginList(installed.list, $('#installed-plugins'), $('#installed-plugin-template'));
socket.emit('checkUpdates'); socket.emit('checkUpdates');
} else { }
else {
installed.messages.show('nothing-installed'); installed.messages.show('nothing-installed');
} }
}); });
socket.on('results:updatable', (data) => { socket.on('results:updatable', (data) => {
data.updatable.forEach((pluginName) => { data.updatable.forEach((pluginName) => {
const actions = $(`#installed-plugins > tr.${pluginName} .actions`); const actions = $(`#installed-plugins > tr.${pluginName} .actions`);
actions.find('.do-update').remove(); actions.find('.do-update').remove();
actions.append( actions.append($('<input>').addClass('do-update').attr('type', 'button').attr('value', 'Update'));
$('<input>').addClass('do-update').attr('type', 'button').attr('value', 'Update'));
}); });
updateHandlers(); updateHandlers();
}); });
socket.on('finished:install', (data) => { socket.on('finished:install', (data) => {
if (data.error) { if (data.error) {
if (data.code === 'EPEERINVALID') { if (data.code === 'EPEERINVALID') {
@ -234,38 +216,30 @@ $(document).ready(() => {
alert(`An error occurred while installing ${data.plugin} \n${data.error}`); alert(`An error occurred while installing ${data.plugin} \n${data.error}`);
$(`#installed-plugins .${data.plugin}`).remove(); $(`#installed-plugins .${data.plugin}`).remove();
} }
socket.emit('getInstalled'); socket.emit('getInstalled');
// update search results // update search results
search.offset = 0; search.offset = 0;
search(search.searchTerm, search.results.length); search(search.searchTerm, search.results.length);
search.results = []; search.results = [];
}); });
socket.on('finished:uninstall', (data) => { socket.on('finished:uninstall', (data) => {
if (data.error) { if (data.error) {
alert(`An error occurred while uninstalling the ${data.plugin} \n${data.error}`); alert(`An error occurred while uninstalling the ${data.plugin} \n${data.error}`);
} }
// remove plugin from installed list // remove plugin from installed list
$(`#installed-plugins .${data.plugin}`).remove(); $(`#installed-plugins .${data.plugin}`).remove();
socket.emit('getInstalled'); socket.emit('getInstalled');
// update search results // update search results
search.offset = 0; search.offset = 0;
search(search.searchTerm, search.results.length); search(search.searchTerm, search.results.length);
search.results = []; search.results = [];
}); });
socket.on('connect', () => { socket.on('connect', () => {
updateHandlers(); updateHandlers();
socket.emit('getInstalled'); socket.emit('getInstalled');
search.searchTerm = null; search.searchTerm = null;
search($('#search-query').val()); search($('#search-query').val());
}); });
// check for updates every 5mins // check for updates every 5mins
setInterval(() => { setInterval(() => {
socket.emit('checkUpdates'); socket.emit('checkUpdates');

View file

@ -1,18 +1,15 @@
'use strict'; 'use strict';
$(document).ready(() => { $(document).ready(() => {
const socket = window.socketio.connect('..', '/settings'); const socket = window.socketio.connect('..', '/settings');
socket.on('connect', () => { socket.on('connect', () => {
socket.emit('load'); socket.emit('load');
}); });
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason === 'io server disconnect') socket.connect(); if (reason === 'io server disconnect')
socket.connect();
}); });
socket.on('settings', (settings) => { socket.on('settings', (settings) => {
/* Check whether the settings.json is authorized to be viewed */ /* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') { if (settings.results === 'NOT_ALLOWED') {
@ -21,49 +18,46 @@ $(document).ready(() => {
$('.err-message').html('Settings json is not authorized to be viewed in Admin page!!'); $('.err-message').html('Settings json is not authorized to be viewed in Admin page!!');
return; return;
} }
/* Check to make sure the JSON is clean before proceeding */ /* Check to make sure the JSON is clean before proceeding */
if (isJSONClean(settings.results)) { if (isJSONClean(settings.results)) {
$('.settings').append(settings.results); $('.settings').append(settings.results);
$('.settings').focus(); $('.settings').focus();
$('.settings').autosize(); $('.settings').autosize();
} else { }
else {
alert('Invalid JSON'); alert('Invalid JSON');
} }
}); });
/* When the admin clicks save Settings check the JSON then send the JSON back to the server */ /* When the admin clicks save Settings check the JSON then send the JSON back to the server */
$('#saveSettings').on('click', () => { $('#saveSettings').on('click', () => {
const editedSettings = $('.settings').val(); const editedSettings = $('.settings').val();
if (isJSONClean(editedSettings)) { if (isJSONClean(editedSettings)) {
// JSON is clean so emit it to the server // JSON is clean so emit it to the server
socket.emit('saveSettings', $('.settings').val()); socket.emit('saveSettings', $('.settings').val());
} else { }
else {
alert('Invalid JSON'); alert('Invalid JSON');
$('.settings').focus(); $('.settings').focus();
} }
}); });
/* Tell Etherpad Server to restart */ /* Tell Etherpad Server to restart */
$('#restartEtherpad').on('click', () => { $('#restartEtherpad').on('click', () => {
socket.emit('restartServer'); socket.emit('restartServer');
}); });
socket.on('saveprogress', (progress) => { socket.on('saveprogress', (progress) => {
$('#response').show(); $('#response').show();
$('#response').text(progress); $('#response').text(progress);
$('#response').fadeOut('slow'); $('#response').fadeOut('slow');
}); });
}); });
const isJSONClean = (data) => { const isJSONClean = (data) => {
let cleanSettings = JSON.minify(data); let cleanSettings = JSON.minify(data);
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}' // this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}'); cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
try { try {
return typeof jQuery.parseJSON(cleanSettings) === 'object'; return typeof jQuery.parseJSON(cleanSettings) === 'object';
} catch (e) { }
catch (e) {
return false; // the JSON failed to be parsed return false; // the JSON failed to be parsed
} }
}; };

View file

@ -1,56 +1,23 @@
'use strict'; 'use strict';
const checkAttribNum = (n) => {
// Low-level utilities for manipulating attribute strings. For a high-level API, see AttributeMap. if (typeof n !== 'number')
throw new TypeError(`not a number: ${n}`);
/** if (n < 0)
* A `[key, value]` pair of strings describing a text attribute. throw new Error(`attribute number is negative: ${n}`);
* if (n !== Math.trunc(n))
* @typedef {[string, string]} Attribute throw new Error(`attribute number is not an integer: ${n}`);
*/ };
export const decodeAttribString = function* (str) {
/**
* A concatenated sequence of zero or more attribute identifiers, each one represented by an
* asterisk followed by a base-36 encoded attribute number.
*
* Examples: '', '*0', '*3*j*z*1q'
*
* @typedef {string} AttributeString
*/
/**
* Converts an attribute string into a sequence of attribute identifier numbers.
*
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
* changesets.
*
* @param {AttributeString} str - Attribute string.
* @yields {number} The attribute numbers (to look up in the associated pool), in the order they
* appear in `str`.
* @returns {Generator<number>}
*/
exports.decodeAttribString = function* (str) {
const re = /\*([0-9a-z]+)|./gy; const re = /\*([0-9a-z]+)|./gy;
let match; let match;
while ((match = re.exec(str)) != null) { while ((match = re.exec(str)) != null) {
const [m, n] = match; const [m, n] = match;
if (n == null) throw new Error(`invalid character in attribute string: ${m}`); if (n == null)
throw new Error(`invalid character in attribute string: ${m}`);
yield Number.parseInt(n, 36); yield Number.parseInt(n, 36);
} }
}; };
export const encodeAttribString = (attribNums) => {
const checkAttribNum = (n) => {
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
};
/**
* Inverse of `decodeAttribString`.
*
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
* @returns {AttributeString}
*/
exports.encodeAttribString = (attribNums) => {
let str = ''; let str = '';
for (const n of attribNums) { for (const n of attribNums) {
checkAttribNum(n); checkAttribNum(n);
@ -58,73 +25,21 @@ exports.encodeAttribString = (attribNums) => {
} }
return str; return str;
}; };
export const attribsFromNums = function* (attribNums, pool) {
/**
* Converts a sequence of attribute numbers into a sequence of attributes.
*
* @param {Iterable<number>} attribNums - Attribute numbers to look up in the pool.
* @param {AttributePool} pool - Attribute pool.
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
* @returns {Generator<Attribute>}
*/
exports.attribsFromNums = function* (attribNums, pool) {
for (const n of attribNums) { for (const n of attribNums) {
checkAttribNum(n); checkAttribNum(n);
const attrib = pool.getAttrib(n); const attrib = pool.getAttrib(n);
if (attrib == null) throw new Error(`attribute ${n} does not exist in pool`); if (attrib == null)
throw new Error(`attribute ${n} does not exist in pool`);
yield attrib; yield attrib;
} }
}; };
export const attribsToNums = function* (attribs, pool) {
/** for (const attrib of attribs)
* Inverse of `attribsFromNums`. yield pool.putAttrib(attrib);
*
* @param {Iterable<Attribute>} attribs - Attributes. Any attributes not already in `pool` are
* inserted into `pool`. No checking is performed to ensure that the attributes are in the
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
* required.)
* @param {AttributePool} pool - Attribute pool.
* @yields {number} The attribute number of each attribute in `attribs`, in order.
* @returns {Generator<number>}
*/
exports.attribsToNums = function* (attribs, pool) {
for (const attrib of attribs) yield pool.putAttrib(attrib);
}; };
export const attribsFromString = function* (str, pool) {
/**
* Convenience function that is equivalent to `attribsFromNums(decodeAttribString(str), pool)`.
*
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
* changesets.
*
* @param {AttributeString} str - Attribute string.
* @param {AttributePool} pool - Attribute pool.
* @yields {Attribute} The attributes identified in `str`, in order.
* @returns {Generator<Attribute>}
*/
exports.attribsFromString = function* (str, pool) {
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool); yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
}; };
export const attribsToString = (attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
/** export const sort = (attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
* Inverse of `attribsFromString`.
*
* @param {Iterable<Attribute>} attribs - Attributes. The attributes to insert into the pool (if
* necessary) and encode. No checking is performed to ensure that the attributes are in the
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
* required.)
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeString}
*/
exports.attribsToString =
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
/**
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
* unspecified.
*
* @param {Attribute[]} attribs - Attributes to sort in place.
* @returns {Attribute[]} `attribs` (for chaining).
*/
exports.sort =
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));

View file

@ -1,23 +1,17 @@
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */ /* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */
'use strict'; 'use strict';
// Set up an error handler to display errors that happen during page load. This handler will be // Set up an error handler to display errors that happen during page load. This handler will be
// overridden with a nicer handler by setupGlobalExceptionHandler() in pad_utils.js. // overridden with a nicer handler by setupGlobalExceptionHandler() in pad_utils.js.
(() => { (() => {
const originalHandler = window.onerror; const originalHandler = window.onerror;
window.onerror = (...args) => { window.onerror = (...args) => {
const [msg, url, line, col, err] = args; const [msg, url, line, col, err] = args;
// Purge the existing HTML and styles for a consistent view. // Purge the existing HTML and styles for a consistent view.
document.body.textContent = ''; document.body.textContent = '';
for (const el of document.querySelectorAll('head style, head link[rel="stylesheet"]')) { for (const el of document.querySelectorAll('head style, head link[rel="stylesheet"]')) {
el.remove(); el.remove();
} }
const box = document.body; const box = document.body;
box.textContent = ''; box.textContent = '';
const summary = document.createElement('p'); const summary = document.createElement('p');
@ -40,9 +34,7 @@
const stack = document.createElement('pre'); const stack = document.createElement('pre');
stackBlock.appendChild(stack); stackBlock.appendChild(stack);
stack.appendChild(document.createTextNode(err.stack || err.toString())); stack.appendChild(document.createTextNode(err.stack || err.toString()));
if (typeof originalHandler === 'function')
if (typeof originalHandler === 'function') originalHandler(...args); originalHandler(...args);
}; };
})(); })();
// @license-end

View file

@ -1,11 +1,18 @@
import { makeCSSManager as makeCSSManager$0 } from "./cssmanager.js";
import { domline as domline$0 } from "./domline.js";
import AttribPool from "./AttributePool.js";
import * as Changeset from "./Changeset.js";
import * as attributes from "./attributes.js";
import { linestylefilter as linestylefilter$0 } from "./linestylefilter.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
import * as _ from "./underscore.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,43 +28,33 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const makeCSSManager = { makeCSSManager: makeCSSManager$0 }.makeCSSManager;
const makeCSSManager = require('./cssmanager').makeCSSManager; const domline = { domline: domline$0 }.domline;
const domline = require('./domline').domline; const linestylefilter = { linestylefilter: linestylefilter$0 }.linestylefilter;
const AttribPool = require('./AttributePool'); const colorutils = { colorutils: colorutils$0 }.colorutils;
const Changeset = require('./Changeset');
const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter;
const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore');
const hooks = require('./pluginfw/hooks');
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => { const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
let goToRevisionIfEnabledCount = 0; let goToRevisionIfEnabledCount = 0;
let changesetLoader = undefined; let changesetLoader = undefined;
const debugLog = (...args) => { const debugLog = (...args) => {
try { try {
if (window.console) console.log(...args); if (window.console)
} catch (e) { console.log(...args);
if (window.console) console.log('error printing: ', e); }
catch (e) {
if (window.console)
console.log('error printing: ', e);
} }
}; };
const padContents = { const padContents = {
currentRevision: clientVars.collab_client_vars.rev, currentRevision: clientVars.collab_client_vars.rev,
currentTime: clientVars.collab_client_vars.time, currentTime: clientVars.collab_client_vars.time,
currentLines: currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null, currentDivs: null,
// to be filled in once the dom loads // to be filled in once the dom loads
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool), apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines( alines: Changeset.splitAttributionLines(clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text),
// generates a jquery element containing HTML for a line // generates a jquery element containing HTML for a line
lineToElement(line, aline) { lineToElement(line, aline) {
const element = document.createElement('div'); const element = document.createElement('div');
@ -70,38 +67,32 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
element.id = Math.random(); element.id = Math.random();
return $(element); return $(element);
}, },
// splice the lines // splice the lines
splice(start, numRemoved, ...newLines) { splice(start, numRemoved, ...newLines) {
// remove spliced-out lines from DOM // remove spliced-out lines from DOM
for (let i = start; i < start + numRemoved && i < this.currentDivs.length; i++) { for (let i = start; i < start + numRemoved && i < this.currentDivs.length; i++) {
this.currentDivs[i].remove(); this.currentDivs[i].remove();
} }
// remove spliced-out line divs from currentDivs array // remove spliced-out line divs from currentDivs array
this.currentDivs.splice(start, numRemoved); this.currentDivs.splice(start, numRemoved);
const newDivs = []; const newDivs = [];
for (let i = 0; i < newLines.length; i++) { for (let i = 0; i < newLines.length; i++) {
newDivs.push(this.lineToElement(newLines[i], this.alines[start + i])); newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
} }
// grab the div just before the first one // grab the div just before the first one
let startDiv = this.currentDivs[start - 1] || null; let startDiv = this.currentDivs[start - 1] || null;
// insert the div elements into the correct place, in the correct order // insert the div elements into the correct place, in the correct order
for (let i = 0; i < newDivs.length; i++) { for (let i = 0; i < newDivs.length; i++) {
if (startDiv) { if (startDiv) {
startDiv.after(newDivs[i]); startDiv.after(newDivs[i]);
} else { }
else {
$('#innerdocbody').prepend(newDivs[i]); $('#innerdocbody').prepend(newDivs[i]);
} }
startDiv = newDivs[i]; startDiv = newDivs[i];
} }
// insert new divs into currentDivs array // insert new divs into currentDivs array
this.currentDivs.splice(start, 0, ...newDivs); this.currentDivs.splice(start, 0, ...newDivs);
// call currentLines.splice, to keep the currentLines array up to date // call currentLines.splice, to keep the currentLines array up to date
this.currentLines.splice(start, numRemoved, ...newLines); this.currentLines.splice(start, numRemoved, ...newLines);
}, },
@ -113,36 +104,35 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
length() { length() {
return this.currentLines.length; return this.currentLines.length;
}, },
getActiveAuthors() { getActiveAuthors() {
const authorIds = new Set(); const authorIds = new Set();
for (const aline of this.alines) { for (const aline of this.alines) {
for (const op of Changeset.deserializeOps(aline)) { for (const op of Changeset.deserializeOps(aline)) {
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) { for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
if (k !== 'author') continue; if (k !== 'author')
if (v) authorIds.add(v); continue;
if (v)
authorIds.add(v);
} }
} }
} }
return [...authorIds].sort(); return [...authorIds].sort();
}, },
}; };
const applyChangeset = (changeset, revision, preventSliderMovement, timeDelta) => { const applyChangeset = (changeset, revision, preventSliderMovement, timeDelta) => {
// disable the next 'gotorevision' call handled by a timeslider update // disable the next 'gotorevision' call handled by a timeslider update
if (!preventSliderMovement) { if (!preventSliderMovement) {
goToRevisionIfEnabledCount++; goToRevisionIfEnabledCount++;
BroadcastSlider.setSliderPosition(revision); BroadcastSlider.setSliderPosition(revision);
} }
const oldAlines = padContents.alines.slice(); const oldAlines = padContents.alines.slice();
try { try {
// must mutate attribution lines before text lines // must mutate attribution lines before text lines
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool); Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
} catch (e) { }
catch (e) {
debugLog(e); debugLog(e);
} }
// scroll to the area that is changed before the lines are mutated // scroll to the area that is changed before the lines are mutated
if ($('#options-followContents').is(':checked') || if ($('#options-followContents').is(':checked') ||
$('#options-followContents').prop('checked')) { $('#options-followContents').prop('checked')) {
@ -163,7 +153,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops); const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
lineChanged = op != null && op.opcode === '=' ? op.lines : 0; lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
} }
const goToLineNumber = (lineNumber) => { const goToLineNumber = (lineNumber) => {
// Sets the Y scrolling of the browser to go to this line // Sets the Y scrolling of the browser to go to this line
const line = $('#innerdocbody').find(`div:nth-child(${lineNumber + 1})`); const line = $('#innerdocbody').find(`div:nth-child(${lineNumber + 1})`);
@ -172,47 +161,40 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
// Chrome 55 - 59 bugfix // Chrome 55 - 59 bugfix
if (ecb.scrollTo) { if (ecb.scrollTo) {
ecb.scrollTo({ top: newY, behavior: 'auto' }); ecb.scrollTo({ top: newY, behavior: 'auto' });
} else { }
else {
$('#editorcontainerbox').scrollTop(newY); $('#editorcontainerbox').scrollTop(newY);
} }
}; };
goToLineNumber(lineChanged); goToLineNumber(lineChanged);
} }
Changeset.mutateTextLines(changeset, padContents); Changeset.mutateTextLines(changeset, padContents);
padContents.currentRevision = revision; padContents.currentRevision = revision;
padContents.currentTime += timeDelta * 1000; padContents.currentTime += timeDelta * 1000;
updateTimer(); updateTimer();
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors); BroadcastSlider.setAuthors(authors);
}; };
const loadedNewChangeset = (changesetForward, changesetBackward, revision, timeDelta) => { const loadedNewChangeset = (changesetForward, changesetBackward, revision, timeDelta) => {
const revisionInfo = window.revisionInfo; const revisionInfo = window.revisionInfo;
const broadcasting = (BroadcastSlider.getSliderPosition() === revisionInfo.latest); const broadcasting = (BroadcastSlider.getSliderPosition() === revisionInfo.latest);
revisionInfo.addChangeset( revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
revision, revision + 1, changesetForward, changesetBackward, timeDelta);
BroadcastSlider.setSliderLength(revisionInfo.latest); BroadcastSlider.setSliderLength(revisionInfo.latest);
if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta); if (broadcasting)
applyChangeset(changesetForward, revision + 1, false, timeDelta);
}; };
/* /*
At this point, we must be certain that the changeset really does map from At this point, we must be certain that the changeset really does map from
the current revision to the specified revision. Any mistakes here will the current revision to the specified revision. Any mistakes here will
cause the whole slider to get out of sync. cause the whole slider to get out of sync.
*/ */
const updateTimer = () => { const updateTimer = () => {
const zpad = (str, length) => { const zpad = (str, length) => {
str = `${str}`; str = `${str}`;
while (str.length < length) str = `0${str}`; while (str.length < length)
str = `0${str}`;
return str; return str;
}; };
const date = new Date(padContents.currentTime); const date = new Date(padContents.currentTime);
const dateFormat = () => { const dateFormat = () => {
const month = zpad(date.getMonth() + 1, 2); const month = zpad(date.getMonth() + 1, 2);
@ -230,8 +212,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
seconds, seconds,
})); }));
}; };
$('#timer').html(dateFormat()); $('#timer').html(dateFormat());
const revisionDate = html10n.get('timeslider.saved', { const revisionDate = html10n.get('timeslider.saved', {
day: date.getDate(), day: date.getDate(),
@ -253,17 +233,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
}); });
$('#revision_date').html(revisionDate); $('#revision_date').html(revisionDate);
}; };
updateTimer(); updateTimer();
const goToRevision = (newRevision) => { const goToRevision = (newRevision) => {
padContents.targetRevision = newRevision; padContents.targetRevision = newRevision;
const path = window.revisionInfo.getPath(padContents.currentRevision, newRevision); const path = window.revisionInfo.getPath(padContents.currentRevision, newRevision);
hooks.aCallAll('goToRevisionEvent', { hooks.aCallAll('goToRevisionEvent', {
rev: newRevision, rev: newRevision,
}); });
if (path.status === 'complete') { if (path.status === 'complete') {
const cs = path.changesets; const cs = path.changesets;
let changeset = cs[0]; let changeset = cs[0];
@ -272,52 +248,46 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
changeset = Changeset.compose(changeset, cs[i], padContents.apool); changeset = Changeset.compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i]; timeDelta += path.times[i];
} }
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); if (changeset)
} else if (path.status === 'partial') { applyChangeset(changeset, path.rev, true, timeDelta);
}
else if (path.status === 'partial') {
// callback is called after changeset information is pulled from server // callback is called after changeset information is pulled from server
// this may never get called, if the changeset has already been loaded // this may never get called, if the changeset has already been loaded
const update = (start, end) => { const update = (start, end) => {
// if we've called goToRevision in the time since, don't goToRevision // if we've called goToRevision in the time since, don't goToRevision
goToRevision(padContents.targetRevision); goToRevision(padContents.targetRevision);
}; };
// do our best with what we have... // do our best with what we have...
const cs = path.changesets; const cs = path.changesets;
let changeset = cs[0]; let changeset = cs[0];
let timeDelta = path.times[0]; let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) { for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool); changeset = Changeset.compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i]; timeDelta += path.times[i];
} }
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta); if (changeset)
applyChangeset(changeset, path.rev, true, timeDelta);
// Loading changeset history for new revision // Loading changeset history for new revision
loadChangesetsForRevision(newRevision, update); loadChangesetsForRevision(newRevision, update);
// Loading changeset history for old revision (to make diff between old and new revision) // Loading changeset history for old revision (to make diff between old and new revision)
loadChangesetsForRevision(padContents.currentRevision - 1); loadChangesetsForRevision(padContents.currentRevision - 1);
} }
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors); BroadcastSlider.setAuthors(authors);
}; };
const loadChangesetsForRevision = (revision, callback) => { const loadChangesetsForRevision = (revision, callback) => {
if (BroadcastSlider.getSliderLength() > 10000) { if (BroadcastSlider.getSliderLength() > 10000) {
const start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10 const start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10
changesetLoader.queueUp(start, 100); changesetLoader.queueUp(start, 100);
} }
if (BroadcastSlider.getSliderLength() > 1000) { if (BroadcastSlider.getSliderLength() > 1000) {
const start = (Math.floor((revision) / 1000) * 1000); // (start from -1, go to 19) + 1 const start = (Math.floor((revision) / 1000) * 1000); // (start from -1, go to 19) + 1
changesetLoader.queueUp(start, 10); changesetLoader.queueUp(start, 10);
} }
const start = (Math.floor((revision) / 100) * 100); const start = (Math.floor((revision) / 100) * 100);
changesetLoader.queueUp(start, 1, callback); changesetLoader.queueUp(start, 1, callback);
}; };
changesetLoader = { changesetLoader = {
running: false, running: false,
resolved: [], resolved: [],
@ -326,7 +296,8 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
requestQueue3: [], requestQueue3: [],
reqCallbacks: [], reqCallbacks: [],
queueUp(revision, width, callback) { queueUp(revision, width, callback) {
if (revision < 0) revision = 0; if (revision < 0)
revision = 0;
// if(this.requestQueue.indexOf(revision) != -1) // if(this.requestQueue.indexOf(revision) != -1)
// return; // already in the queue. // return; // already in the queue.
if (this.resolved.indexOf(`${revision}_${width}`) !== -1) { if (this.resolved.indexOf(`${revision}_${width}`) !== -1) {
@ -334,13 +305,10 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
return; return;
} }
this.resolved.push(`${revision}_${width}`); this.resolved.push(`${revision}_${width}`);
const requestQueue = width === 1 ? this.requestQueue3
const requestQueue =
width === 1 ? this.requestQueue3
: width === 10 ? this.requestQueue2 : width === 10 ? this.requestQueue2
: this.requestQueue1; : this.requestQueue1;
requestQueue.push( requestQueue.push({
{
rev: revision, rev: revision,
res: width, res: width,
callback, callback,
@ -351,29 +319,24 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
} }
}, },
loadFromQueue() { loadFromQueue() {
const requestQueue = const requestQueue = this.requestQueue1.length > 0 ? this.requestQueue1
this.requestQueue1.length > 0 ? this.requestQueue1
: this.requestQueue2.length > 0 ? this.requestQueue2 : this.requestQueue2.length > 0 ? this.requestQueue2
: this.requestQueue3.length > 0 ? this.requestQueue3 : this.requestQueue3.length > 0 ? this.requestQueue3
: null; : null;
if (!requestQueue) { if (!requestQueue) {
this.running = false; this.running = false;
return; return;
} }
const request = requestQueue.pop(); const request = requestQueue.pop();
const granularity = request.res; const granularity = request.res;
const callback = request.callback; const callback = request.callback;
const start = request.rev; const start = request.rev;
const requestID = Math.floor(Math.random() * 100000); const requestID = Math.floor(Math.random() * 100000);
sendSocketMsg('CHANGESET_REQ', { sendSocketMsg('CHANGESET_REQ', {
start, start,
granularity, granularity,
requestID, requestID,
}); });
this.reqCallbacks[requestID] = callback; this.reqCallbacks[requestID] = callback;
}, },
handleSocketResponse(message) { handleSocketResponse(message) {
@ -381,7 +344,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const granularity = message.data.granularity; const granularity = message.data.granularity;
const callback = this.reqCallbacks[message.data.requestID]; const callback = this.reqCallbacks[message.data.requestID];
delete this.reqCallbacks[message.data.requestID]; delete this.reqCallbacks[message.data.requestID];
this.handleResponse(message.data, start, granularity, callback); this.handleResponse(message.data, start, granularity, callback);
setTimeout(() => this.loadFromQueue(), 10); setTimeout(() => this.loadFromQueue(), 10);
}, },
@ -390,52 +352,46 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
for (let i = 0; i < data.forwardsChangesets.length; i++) { for (let i = 0; i < data.forwardsChangesets.length; i++) {
const astart = start + i * granularity - 1; // rev -1 is a blank single line const astart = start + i * granularity - 1; // rev -1 is a blank single line
let aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision let aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1; if (aend > data.actualEndNum - 1)
aend = data.actualEndNum - 1;
// debugLog("adding changeset:", astart, aend); // debugLog("adding changeset:", astart, aend);
const forwardcs = const forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool); const backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
const backwardcs =
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
} }
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); if (callback)
callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
}, },
handleMessageFromServer(obj) { handleMessageFromServer(obj) {
if (obj.type === 'COLLABROOM') { if (obj.type === 'COLLABROOM') {
obj = obj.data; obj = obj.data;
if (obj.type === 'NEW_CHANGES') { if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool( const changeset = Changeset.moveOpsToNewPool(obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); let changesetBack = Changeset.inverse(obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
let changesetBack = Changeset.inverse(
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
} else if (obj.type === 'NEW_AUTHORDATA') { }
else if (obj.type === 'NEW_AUTHORDATA') {
const authorMap = {}; const authorMap = {};
authorMap[obj.author] = obj.data; authorMap[obj.author] = obj.data;
receiveAuthorData(authorMap); receiveAuthorData(authorMap);
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors); BroadcastSlider.setAuthors(authors);
} else if (obj.type === 'NEW_SAVEDREV') { }
else if (obj.type === 'NEW_SAVEDREV') {
const savedRev = obj.savedRev; const savedRev = obj.savedRev;
BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev); BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
} }
hooks.callAll(`handleClientTimesliderMessage_${obj.type}`, { payload: obj }); hooks.callAll(`handleClientTimesliderMessage_${obj.type}`, { payload: obj });
} else if (obj.type === 'CHANGESET_REQ') { }
else if (obj.type === 'CHANGESET_REQ') {
this.handleSocketResponse(obj); this.handleSocketResponse(obj);
} else { }
else {
debugLog(`Unknown message type: ${obj.type}`); debugLog(`Unknown message type: ${obj.type}`);
} }
}, },
}; };
// to start upon window load, just push a function onto this array // to start upon window load, just push a function onto this array
// window['onloadFuncts'].push(setUpSocket); // window['onloadFuncts'].push(setUpSocket);
// window['onloadFuncts'].push(function () // window['onloadFuncts'].push(function ()
@ -449,22 +405,19 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
$('#innerdocbody').append(div); $('#innerdocbody').append(div);
} }
}); });
// this is necessary to keep infinite loops of events firing, // this is necessary to keep infinite loops of events firing,
// since goToRevision changes the slider position // since goToRevision changes the slider position
const goToRevisionIfEnabled = (...args) => { const goToRevisionIfEnabled = (...args) => {
if (goToRevisionIfEnabledCount > 0) { if (goToRevisionIfEnabledCount > 0) {
goToRevisionIfEnabledCount--; goToRevisionIfEnabledCount--;
} else { }
else {
goToRevision(...args); goToRevision(...args);
} }
}; };
BroadcastSlider.onSlider(goToRevisionIfEnabled); BroadcastSlider.onSlider(goToRevisionIfEnabled);
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet); const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const authorData = {}; const authorData = {};
const receiveAuthorData = (newAuthorData) => { const receiveAuthorData = (newAuthorData) => {
for (const [author, data] of Object.entries(newAuthorData)) { for (const [author, data] of Object.entries(newAuthorData)) {
const bgcolor = typeof data.colorId === 'number' const bgcolor = typeof data.colorId === 'number'
@ -478,10 +431,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
authorData[author] = data; authorData[author] = data;
} }
}; };
receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData); receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
return changesetLoader; return changesetLoader;
}; };
export { loadBroadcastJS };
exports.loadBroadcastJS = loadBroadcastJS;

View file

@ -1,11 +1,9 @@
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -24,13 +22,11 @@
// revision info is a skip list whos entries represent a particular revision // revision info is a skip list whos entries represent a particular revision
// of the document. These revisions are connected together by various // of the document. These revisions are connected together by various
// changesets, or deltas, between any two revisions. // changesets, or deltas, between any two revisions.
const loadBroadcastRevisionsJS = () => { const loadBroadcastRevisionsJS = () => {
function Revision(revNum) { function Revision(revNum) {
this.rev = revNum; this.rev = revNum;
this.changesets = []; this.changesets = [];
} }
Revision.prototype.addChangeset = function (destIndex, changeset, timeDelta) { Revision.prototype.addChangeset = function (destIndex, changeset, timeDelta) {
const changesetWrapper = { const changesetWrapper = {
deltaRev: destIndex - this.rev, deltaRev: destIndex - this.rev,
@ -40,7 +36,6 @@ const loadBroadcastRevisionsJS = () => {
this.changesets.push(changesetWrapper); this.changesets.push(changesetWrapper);
this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev)); this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev));
}; };
const revisionInfo = {}; const revisionInfo = {};
revisionInfo.addChangeset = function (fromIndex, toIndex, changeset, backChangeset, timeDelta) { revisionInfo.addChangeset = function (fromIndex, toIndex, changeset, backChangeset, timeDelta) {
const startRevision = this[fromIndex] || this.createNew(fromIndex); const startRevision = this[fromIndex] || this.createNew(fromIndex);
@ -48,18 +43,14 @@ const loadBroadcastRevisionsJS = () => {
startRevision.addChangeset(toIndex, changeset, timeDelta); startRevision.addChangeset(toIndex, changeset, timeDelta);
endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta); endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
}; };
revisionInfo.latest = clientVars.collab_client_vars.rev || -1; revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
revisionInfo.createNew = function (index) { revisionInfo.createNew = function (index) {
this[index] = new Revision(index); this[index] = new Revision(index);
if (index > this.latest) { if (index > this.latest) {
this.latest = index; this.latest = index;
} }
return this[index]; return this[index];
}; };
// assuming that there is a path from fromIndex to toIndex, and that the links // assuming that there is a path from fromIndex to toIndex, and that the links
// are laid out in a skip-list format // are laid out in a skip-list format
revisionInfo.getPath = function (fromIndex, toIndex) { revisionInfo.getPath = function (fromIndex, toIndex) {
@ -72,16 +63,12 @@ const loadBroadcastRevisionsJS = () => {
while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) { while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) {
let couldNotContinue = false; let couldNotContinue = false;
const oldRev = elem.rev; const oldRev = elem.rev;
for (let i = reverse ? elem.changesets.length - 1 : 0; reverse ? i >= 0 : i < elem.changesets.length; i += reverse ? -1 : 1) {
for (let i = reverse ? elem.changesets.length - 1 : 0;
reverse ? i >= 0 : i < elem.changesets.length;
i += reverse ? -1 : 1) {
if (((elem.changesets[i].deltaRev < 0) && !reverse) || if (((elem.changesets[i].deltaRev < 0) && !reverse) ||
((elem.changesets[i].deltaRev > 0) && reverse)) { ((elem.changesets[i].deltaRev > 0) && reverse)) {
couldNotContinue = true; couldNotContinue = true;
break; break;
} }
if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) || if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) ||
((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) { ((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) {
const topush = elem.changesets[i]; const topush = elem.changesets[i];
@ -92,14 +79,13 @@ const loadBroadcastRevisionsJS = () => {
break; break;
} }
} }
if (couldNotContinue || oldRev === elem.rev)
if (couldNotContinue || oldRev === elem.rev) break; break;
} }
} }
let status = 'partial'; let status = 'partial';
if (elem.rev === toIndex) status = 'complete'; if (elem.rev === toIndex)
status = 'complete';
return { return {
fromRev: fromIndex, fromRev: fromIndex,
rev: elem.rev, rev: elem.rev,
@ -111,5 +97,4 @@ const loadBroadcastRevisionsJS = () => {
}; };
window.revisionInfo = revisionInfo; window.revisionInfo = revisionInfo;
}; };
export { loadBroadcastRevisionsJS };
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;

View file

@ -1,121 +1,76 @@
import * as _ from "./underscore.js";
import { padmodals as padmodals$0 } from "./pad_modals.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
'use strict'; 'use strict';
/** const padmodals = { padmodals: padmodals$0 }.padmodals;
* This code is mostly from the old Etherpad. Please help us to comment this code. const colorutils = { colorutils: colorutils$0 }.colorutils;
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
const _ = require('./underscore');
const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils;
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let BroadcastSlider; let BroadcastSlider;
// Hack to ensure timeslider i18n values are in // Hack to ensure timeslider i18n values are in
$("[data-key='timeslider_returnToPad'] > a > span").html( $("[data-key='timeslider_returnToPad'] > a > span").html(html10n.get('timeslider.toolbar.returnbutton'));
html10n.get('timeslider.toolbar.returnbutton')); (() => {
(() => { // wrap this code in its own namespace
let sliderLength = 1000; let sliderLength = 1000;
let sliderPos = 0; let sliderPos = 0;
let sliderActive = false; let sliderActive = false;
const slidercallbacks = []; const slidercallbacks = [];
const savedRevisions = []; const savedRevisions = [];
let sliderPlaying = false; let sliderPlaying = false;
const _callSliderCallbacks = (newval) => { const _callSliderCallbacks = (newval) => {
sliderPos = newval; sliderPos = newval;
for (let i = 0; i < slidercallbacks.length; i++) { for (let i = 0; i < slidercallbacks.length; i++) {
slidercallbacks[i](newval); slidercallbacks[i](newval);
} }
}; };
const updateSliderElements = () => { const updateSliderElements = () => {
for (let i = 0; i < savedRevisions.length; i++) { for (let i = 0; i < savedRevisions.length; i++) {
const position = parseInt(savedRevisions[i].attr('pos')); const position = parseInt(savedRevisions[i].attr('pos'));
savedRevisions[i].css( savedRevisions[i].css('left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
} }
$('#ui-slider-handle').css( $('#ui-slider-handle').css('left', sliderPos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
'left', sliderPos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
}; };
const addSavedRevision = (position, info) => { const addSavedRevision = (position, info) => {
const newSavedRevision = $('<div></div>'); const newSavedRevision = $('<div></div>');
newSavedRevision.addClass('star'); newSavedRevision.addClass('star');
newSavedRevision.attr('pos', position); newSavedRevision.attr('pos', position);
newSavedRevision.css( newSavedRevision.css('left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
$('#ui-slider-bar').append(newSavedRevision); $('#ui-slider-bar').append(newSavedRevision);
newSavedRevision.mouseup((evt) => { newSavedRevision.mouseup((evt) => {
BroadcastSlider.setSliderPosition(position); BroadcastSlider.setSliderPosition(position);
}); });
savedRevisions.push(newSavedRevision); savedRevisions.push(newSavedRevision);
}; };
/* Begin small 'API' */ /* Begin small 'API' */
const onSlider = (callback) => { const onSlider = (callback) => {
slidercallbacks.push(callback); slidercallbacks.push(callback);
}; };
const getSliderPosition = () => sliderPos; const getSliderPosition = () => sliderPos;
const setSliderPosition = (newpos) => { const setSliderPosition = (newpos) => {
newpos = Number(newpos); newpos = Number(newpos);
if (newpos < 0 || newpos > sliderLength) return; if (newpos < 0 || newpos > sliderLength)
return;
if (!newpos) { if (!newpos) {
newpos = 0; // stops it from displaying NaN if newpos isn't set newpos = 0; // stops it from displaying NaN if newpos isn't set
} }
window.location.hash = `#${newpos}`; window.location.hash = `#${newpos}`;
$('#ui-slider-handle').css( $('#ui-slider-handle').css('left', newpos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
'left', newpos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
$('a.tlink').map(function () { $('a.tlink').map(function () {
$(this).attr('href', $(this).attr('thref').replace('%revision%', newpos)); $(this).attr('href', $(this).attr('thref').replace('%revision%', newpos));
}); });
$('#revision_label').html(html10n.get('timeslider.version', { version: newpos })); $('#revision_label').html(html10n.get('timeslider.version', { version: newpos }));
$('#leftstar, #leftstep').toggleClass('disabled', newpos === 0); $('#leftstar, #leftstep').toggleClass('disabled', newpos === 0);
$('#rightstar, #rightstep').toggleClass('disabled', newpos === sliderLength); $('#rightstar, #rightstep').toggleClass('disabled', newpos === sliderLength);
sliderPos = newpos; sliderPos = newpos;
_callSliderCallbacks(newpos); _callSliderCallbacks(newpos);
}; };
const getSliderLength = () => sliderLength; const getSliderLength = () => sliderLength;
const setSliderLength = (newlength) => { const setSliderLength = (newlength) => {
sliderLength = newlength; sliderLength = newlength;
updateSliderElements(); updateSliderElements();
}; };
// just take over the whole slider screen with a reconnect message // just take over the whole slider screen with a reconnect message
const showReconnectUI = () => { const showReconnectUI = () => {
padmodals.showModal('disconnected'); padmodals.showModal('disconnected');
}; };
const setAuthors = (authors) => { const setAuthors = (authors) => {
const authorsList = $('#authorsList'); const authorsList = $('#authorsList');
authorsList.empty(); authorsList.empty();
@ -126,36 +81,37 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
if (author) { if (author) {
const authorColor = clientVars.colorPalette[author.colorId] || author.colorId; const authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
if (author.name) { if (author.name) {
if (numNamed !== 0) authorsList.append(', '); if (numNamed !== 0)
const textColor = authorsList.append(', ');
colorutils.textColorFromBackgroundColor(authorColor, clientVars.skinName); const textColor = colorutils.textColorFromBackgroundColor(authorColor, clientVars.skinName);
$('<span />') $('<span />')
.text(author.name || 'unnamed') .text(author.name || 'unnamed')
.css('background-color', authorColor) .css('background-color', authorColor)
.css('color', textColor) .css('color', textColor)
.addClass('author') .addClass('author')
.appendTo(authorsList); .appendTo(authorsList);
numNamed++; numNamed++;
} else { }
else {
numAnonymous++; numAnonymous++;
if (authorColor) colorsAnonymous.push(authorColor); if (authorColor)
colorsAnonymous.push(authorColor);
} }
} }
}); });
if (numAnonymous > 0) { if (numAnonymous > 0) {
const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', { num: numAnonymous }); const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', { num: numAnonymous });
if (numNamed !== 0) { if (numNamed !== 0) {
authorsList.append(` + ${anonymousAuthorString}`); authorsList.append(` + ${anonymousAuthorString}`);
} else { }
else {
authorsList.append(anonymousAuthorString); authorsList.append(anonymousAuthorString);
} }
if (colorsAnonymous.length > 0) { if (colorsAnonymous.length > 0) {
authorsList.append(' ('); authorsList.append(' (');
_.each(colorsAnonymous, (color, i) => { _.each(colorsAnonymous, (color, i) => {
if (i > 0) authorsList.append(' '); if (i > 0)
authorsList.append(' ');
$('<span>&nbsp;</span>') $('<span>&nbsp;</span>')
.css('background-color', color) .css('background-color', color)
.addClass('author author-anonymous') .addClass('author author-anonymous')
@ -168,7 +124,6 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
authorsList.append(html10n.get('timeslider.toolbar.authorsList')); authorsList.append(html10n.get('timeslider.toolbar.authorsList'));
} }
}; };
const playButtonUpdater = () => { const playButtonUpdater = () => {
if (sliderPlaying) { if (sliderPlaying) {
if (getSliderPosition() + 1 > sliderLength) { if (getSliderPosition() + 1 > sliderLength) {
@ -177,23 +132,21 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
return; return;
} }
setSliderPosition(getSliderPosition() + 1); setSliderPosition(getSliderPosition() + 1);
setTimeout(playButtonUpdater, 100); setTimeout(playButtonUpdater, 100);
} }
}; };
const playpause = () => { const playpause = () => {
$('#playpause_button_icon').toggleClass('pause'); $('#playpause_button_icon').toggleClass('pause');
if (!sliderPlaying) { if (!sliderPlaying) {
if (getSliderPosition() === sliderLength) setSliderPosition(0); if (getSliderPosition() === sliderLength)
setSliderPosition(0);
sliderPlaying = true; sliderPlaying = true;
playButtonUpdater(); playButtonUpdater();
} else { }
else {
sliderPlaying = false; sliderPlaying = false;
} }
}; };
BroadcastSlider = { BroadcastSlider = {
onSlider, onSlider,
getSliderPosition, getSliderPosition,
@ -206,41 +159,41 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
showReconnectUI, showReconnectUI,
setAuthors, setAuthors,
}; };
// assign event handlers to html UI elements after page load // assign event handlers to html UI elements after page load
fireWhenAllScriptsAreLoaded.push(() => { fireWhenAllScriptsAreLoaded.push(() => {
$(document).keyup((e) => { $(document).keyup((e) => {
if (!e) e = window.event; if (!e)
e = window.event;
const code = e.keyCode || e.which; const code = e.keyCode || e.which;
if (code === 37) { // left if (code === 37) { // left
if (e.shiftKey) { if (e.shiftKey) {
$('#leftstar').click(); $('#leftstar').click();
} else { }
else {
$('#leftstep').click(); $('#leftstep').click();
} }
} else if (code === 39) { // right }
else if (code === 39) { // right
if (e.shiftKey) { if (e.shiftKey) {
$('#rightstar').click(); $('#rightstar').click();
} else { }
else {
$('#rightstep').click(); $('#rightstep').click();
} }
} else if (code === 32) { // spacebar }
else if (code === 32) { // spacebar
$('#playpause_button_icon').trigger('click'); $('#playpause_button_icon').trigger('click');
} }
}); });
// Resize // Resize
$(window).resize(() => { $(window).resize(() => {
updateSliderElements(); updateSliderElements();
}); });
// Slider click // Slider click
$('#ui-slider-bar').mousedown((evt) => { $('#ui-slider-bar').mousedown((evt) => {
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left)); $('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left));
$('#ui-slider-handle').trigger(evt); $('#ui-slider-handle').trigger(evt);
}); });
// Slider dragging // Slider dragging
$('#ui-slider-handle').mousedown(function (evt) { $('#ui-slider-handle').mousedown(function (evt) {
this.startLoc = evt.clientX; this.startLoc = evt.clientX;
@ -249,37 +202,41 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
$(document).mousemove((evt2) => { $(document).mousemove((evt2) => {
$(this).css('pointer', 'move'); $(this).css('pointer', 'move');
let newloc = this.currentLoc + (evt2.clientX - this.startLoc); let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
if (newloc < 0) newloc = 0; if (newloc < 0)
newloc = 0;
const maxPos = $('#ui-slider-bar').width() - 2; const maxPos = $('#ui-slider-bar').width() - 2;
if (newloc > maxPos) newloc = maxPos; if (newloc > maxPos)
newloc = maxPos;
const version = Math.floor(newloc * sliderLength / maxPos); const version = Math.floor(newloc * sliderLength / maxPos);
$('#revision_label').html(html10n.get('timeslider.version', { version })); $('#revision_label').html(html10n.get('timeslider.version', { version }));
$(this).css('left', newloc); $(this).css('left', newloc);
if (getSliderPosition() !== version) _callSliderCallbacks(version); if (getSliderPosition() !== version)
_callSliderCallbacks(version);
}); });
$(document).mouseup((evt2) => { $(document).mouseup((evt2) => {
$(document).unbind('mousemove'); $(document).unbind('mousemove');
$(document).unbind('mouseup'); $(document).unbind('mouseup');
sliderActive = false; sliderActive = false;
let newloc = this.currentLoc + (evt2.clientX - this.startLoc); let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
if (newloc < 0) newloc = 0; if (newloc < 0)
newloc = 0;
const maxPos = $('#ui-slider-bar').width() - 2; const maxPos = $('#ui-slider-bar').width() - 2;
if (newloc > maxPos) newloc = maxPos; if (newloc > maxPos)
newloc = maxPos;
$(this).css('left', newloc); $(this).css('left', newloc);
setSliderPosition(Math.floor(newloc * sliderLength / maxPos)); setSliderPosition(Math.floor(newloc * sliderLength / maxPos));
if (parseInt($(this).css('left')) < 2) { if (parseInt($(this).css('left')) < 2) {
$(this).css('left', '2px'); $(this).css('left', '2px');
} else { }
else {
this.currentLoc = parseInt($(this).css('left')); this.currentLoc = parseInt($(this).css('left'));
} }
}); });
}); });
// play/pause toggling // play/pause toggling
$('#playpause_button_icon').click((evt) => { $('#playpause_button_icon').click((evt) => {
BroadcastSlider.playpause(); BroadcastSlider.playpause();
}); });
// next/prev saved revision and changeset // next/prev saved revision and changeset
$('.stepper').click(function (evt) { $('.stepper').click(function (evt) {
switch ($(this).attr('id')) { switch ($(this).attr('id')) {
@ -293,7 +250,8 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let nextStar = 0; // default to first revision in document let nextStar = 0; // default to first revision in document
for (let i = 0; i < savedRevisions.length; i++) { for (let i = 0; i < savedRevisions.length; i++) {
const pos = parseInt(savedRevisions[i].attr('pos')); const pos = parseInt(savedRevisions[i].attr('pos'));
if (pos < getSliderPosition() && nextStar < pos) nextStar = pos; if (pos < getSliderPosition() && nextStar < pos)
nextStar = pos;
} }
setSliderPosition(nextStar); setSliderPosition(nextStar);
break; break;
@ -302,17 +260,16 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let nextStar = sliderLength; // default to last revision in document let nextStar = sliderLength; // default to last revision in document
for (let i = 0; i < savedRevisions.length; i++) { for (let i = 0; i < savedRevisions.length; i++) {
const pos = parseInt(savedRevisions[i].attr('pos')); const pos = parseInt(savedRevisions[i].attr('pos'));
if (pos > getSliderPosition() && nextStar > pos) nextStar = pos; if (pos > getSliderPosition() && nextStar > pos)
nextStar = pos;
} }
setSliderPosition(nextStar); setSliderPosition(nextStar);
break; break;
} }
} }
}); });
if (clientVars) { if (clientVars) {
$('#timeslider-wrapper').show(); $('#timeslider-wrapper').show();
if (window.location.hash.length > 1) { if (window.location.hash.length > 1) {
const hashRev = Number(window.location.hash.substr(1)); const hashRev = Number(window.location.hash.substr(1));
if (!isNaN(hashRev)) { if (!isNaN(hashRev)) {
@ -320,23 +277,17 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
setTimeout(() => { setSliderPosition(hashRev); }, 1); setTimeout(() => { setSliderPosition(hashRev); }, 1);
} }
} }
setSliderLength(clientVars.collab_client_vars.rev); setSliderLength(clientVars.collab_client_vars.rev);
setSliderPosition(clientVars.collab_client_vars.rev); setSliderPosition(clientVars.collab_client_vars.rev);
_.each(clientVars.savedRevisions, (revision) => { _.each(clientVars.savedRevisions, (revision) => {
addSavedRevision(revision.revNum, revision); addSavedRevision(revision.revNum, revision);
}); });
} }
}); });
})(); })();
BroadcastSlider.onSlider((loc) => { BroadcastSlider.onSlider((loc) => {
$('#viewlatest').html( $('#viewlatest').html(`${loc === BroadcastSlider.getSliderLength() ? 'Viewing' : 'View'} latest content`);
`${loc === BroadcastSlider.getSliderLength() ? 'Viewing' : 'View'} latest content`);
}); });
return BroadcastSlider; return BroadcastSlider;
}; };
export { loadBroadcastSliderJS };
exports.loadBroadcastSliderJS = loadBroadcastSliderJS;

View file

@ -1,11 +1,129 @@
'use strict'; 'use strict';
const createSelectionRange = (range) => {
// One rep.line(div) can be broken in more than one line in the browser. const clonedRange = range.cloneRange();
// This function is useful to get the caret position of the line as // we set the selection start and end to avoid error when user selects a text bigger than
// is represented by the browser // the viewport height and uses the arrow keys to expand the selection. In this particular
exports.getPosition = () => { // case is necessary to know where the selections ends because both edges of the selection
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
clonedRange.setStart(range.endContainer, range.endOffset);
clonedRange.setEnd(range.endContainer, range.endOffset);
return clonedRange;
};
const getPositionOfRepLineAtOffset = (node, offset) => {
// it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node);
}
while (node.length === 0 && node.nextSibling) {
node = node.nextSibling;
}
const newRange = new Range();
newRange.setStart(node, offset);
newRange.setEnd(node, offset);
const linePosition = getPositionOfElementOrSelection(newRange);
newRange.detach(); // performance sake
return linePosition;
};
const getPositionOfElementOrSelection = (element) => {
const rect = element.getBoundingClientRect();
const linePosition = {
bottom: rect.bottom,
height: rect.height,
top: rect.top,
};
return linePosition;
};
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const firstRootNode = getFirstRootChildNode(lineNode);
// to get the position of the node we get the position of the first char
const positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
return positionOfFirstRootNode.top === caretLineTop;
};
// find the first root node, usually it is a text node
const getFirstRootChildNode = (node) => {
if (!node.firstChild) {
return node;
}
else {
return getFirstRootChildNode(node.firstChild);
}
};
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we get the position of the line in the last char of it
const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition;
};
const getLastRootChildNode = (node) => {
if (!node.lastChild) {
return {
node,
length: node.length,
};
}
else {
return getLastRootChildNode(node.lastChild);
}
};
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we take a rep line and get the position of the last char of it
const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition.top === caretLineTop;
};
const getPreviousVisibleLine = (line, rep) => {
const firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
}
else if (isLineVisible(line, rep)) {
return line;
}
else {
return getPreviousVisibleLine(line - 1, rep);
}
};
const getNextVisibleLine = (line, rep) => {
const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
}
else if (isLineVisible(line, rep)) {
return line;
}
else {
return getNextVisibleLine(line + 1, rep);
}
};
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const firstRootChildNode = getFirstRootChildNode(lineNode);
// we can get the position of the line, getting the position of the first char of the rep line
const firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
return firstRootChildNodePosition;
};
const getSelectionRange = () => {
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
if (selection && selection.type !== 'None' && selection.rangeCount > 0) {
return selection.getRangeAt(0);
}
else {
return null;
}
};
export const getPosition = () => {
const range = getSelectionRange(); const range = getSelectionRange();
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null; if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody')
return null;
// When there's a <br> or any element that has no height, we can't get the dimension of the // When there's a <br> or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get // element where the caret is. As we can't get the element height, we create a text node to get
// the dimensions on the position. // the dimensions on the position.
@ -17,187 +135,31 @@ exports.getPosition = () => {
shadowCaret.remove(); shadowCaret.remove();
return line; return line;
}; };
export const getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
const createSelectionRange = (range) => {
const clonedRange = range.cloneRange();
// we set the selection start and end to avoid error when user selects a text bigger than
// the viewport height and uses the arrow keys to expand the selection. In this particular
// case is necessary to know where the selections ends because both edges of the selection
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
clonedRange.setStart(range.endContainer, range.endOffset);
clonedRange.setEnd(range.endContainer, range.endOffset);
return clonedRange;
};
const getPositionOfRepLineAtOffset = (node, offset) => {
// it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node);
}
while (node.length === 0 && node.nextSibling) {
node = node.nextSibling;
}
const newRange = new Range();
newRange.setStart(node, offset);
newRange.setEnd(node, offset);
const linePosition = getPositionOfElementOrSelection(newRange);
newRange.detach(); // performance sake
return linePosition;
};
const getPositionOfElementOrSelection = (element) => {
const rect = element.getBoundingClientRect();
const linePosition = {
bottom: rect.bottom,
height: rect.height,
top: rect.top,
};
return linePosition;
};
// here we have two possibilities:
// [1] the line before the caret line has the same type, so both of them has the same margin,
// padding height, etc. So, we can use the caret line to make calculation necessary to know
// where is the top of the previous line
// [2] the line before is part of another rep line. It's possible this line has different margins
// height. So we have to get the exactly position of the line
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1] let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep); const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
// the caret is in the beginning of a rep line, so the previous browser line // the caret is in the beginning of a rep line, so the previous browser line
// is the last line browser line of the a rep line // is the last line browser line of the a rep line
if (isCaretLineFirstBrowserLine) { // [2] if (isCaretLineFirstBrowserLine) { // [2]
const lineBeforeCaretLine = rep.selStart[0] - 1; const lineBeforeCaretLine = rep.selStart[0] - 1;
const firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep); const firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep);
const linePosition = const linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
previousLineTop = linePosition.top; previousLineTop = linePosition.top;
} }
return previousLineTop; return previousLineTop;
}; };
export const getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const firstRootNode = getFirstRootChildNode(lineNode);
// to get the position of the node we get the position of the first char
const positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
return positionOfFirstRootNode.top === caretLineTop;
};
// find the first root node, usually it is a text node
const getFirstRootChildNode = (node) => {
if (!node.firstChild) {
return node;
} else {
return getFirstRootChildNode(node.firstChild);
}
};
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we get the position of the line in the last char of it
const lastRootChildNodePosition =
getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition;
};
const getLastRootChildNode = (node) => {
if (!node.lastChild) {
return {
node,
length: node.length,
};
} else {
return getLastRootChildNode(node.lastChild);
}
};
// here we have two possibilities:
// [1] The next line is part of the same rep line of the caret line, so we have the same dimensions.
// So, we can use the caret line to calculate the bottom of the line.
// [2] the next line is part of another rep line.
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1] let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
const isCaretLineLastBrowserLine = const isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
// the caret is at the end of a rep line, so we can get the next browser line dimension // the caret is at the end of a rep line, so we can get the next browser line dimension
// using the position of the first char of the next rep line // using the position of the first char of the next rep line
if (isCaretLineLastBrowserLine) { // [2] if (isCaretLineLastBrowserLine) { // [2]
const nextLineAfterCaretLine = rep.selStart[0] + 1; const nextLineAfterCaretLine = rep.selStart[0] + 1;
const firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep); const firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep);
const linePosition = const linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
nextLineBottom = linePosition.bottom; nextLineBottom = linePosition.bottom;
} }
return nextLineBottom; return nextLineBottom;
}; };
export { getPreviousVisibleLine };
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => { export { getNextVisibleLine };
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we take a rep line and get the position of the last char of it
const lastRootChildNodePosition =
getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition.top === caretLineTop;
};
const getPreviousVisibleLine = (line, rep) => {
const firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
} else if (isLineVisible(line, rep)) {
return line;
} else {
return getPreviousVisibleLine(line - 1, rep);
}
};
exports.getPreviousVisibleLine = getPreviousVisibleLine;
const getNextVisibleLine = (line, rep) => {
const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
} else if (isLineVisible(line, rep)) {
return line;
} else {
return getNextVisibleLine(line + 1, rep);
}
};
exports.getNextVisibleLine = getNextVisibleLine;
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const firstRootChildNode = getFirstRootChildNode(lineNode);
// we can get the position of the line, getting the position of the first char of the rep line
const firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
return firstRootChildNodePosition;
};
const getSelectionRange = () => {
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
if (selection && selection.type !== 'None' && selection.rangeCount > 0) {
return selection.getRangeAt(0);
} else {
return null;
}
};

View file

@ -1,31 +1,7 @@
import AttributeMap from "./AttributeMap.js";
import AttributePool from "./AttributePool.js";
import * as Changeset from "./Changeset.js";
'use strict'; 'use strict';
/**
* 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
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const AttributeMap = require('./AttributeMap');
const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset');
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server // latest official text from server
let baseAText = Changeset.makeAText('\n'); let baseAText = Changeset.makeAText('\n');
@ -40,11 +16,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// unset in a "finally" block. When set to true, the setter // unset in a "finally" block. When set to true, the setter
// takes change of userChangeset. // takes change of userChangeset.
let applyingNonUserChanges = false; let applyingNonUserChanges = false;
let changeCallback = null; let changeCallback = null;
let changeCallbackTimeout = null; let changeCallbackTimeout = null;
const setChangeCallbackTimeout = () => { const setChangeCallbackTimeout = () => {
// can call this multiple times per call-stack, because // can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists // we only schedule a call to changeCallback if it exists
@ -53,15 +26,16 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
changeCallbackTimeout = scheduler.setTimeout(() => { changeCallbackTimeout = scheduler.setTimeout(() => {
try { try {
changeCallback(); changeCallback();
} catch (pseudoError) { }
catch (pseudoError) {
// as empty as my soul // as empty as my soul
} finally { }
finally {
changeCallbackTimeout = null; changeCallbackTimeout = null;
} }
}, 0); }, 0);
} }
}; };
let self; let self;
return self = { return self = {
isTracking: () => tracking, isTracking: () => tracking,
@ -81,49 +55,47 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
applyingNonUserChanges = true; applyingNonUserChanges = true;
try { try {
callbacks.setDocumentAttributedText(atext); callbacks.setDocumentAttributedText(atext);
} finally { }
finally {
applyingNonUserChanges = false; applyingNonUserChanges = false;
} }
}); });
}, },
composeUserChangeset: (c) => { composeUserChangeset: (c) => {
if (!tracking) return; if (!tracking)
if (applyingNonUserChanges) return; return;
if (Changeset.isIdentity(c)) return; if (applyingNonUserChanges)
return;
if (Changeset.isIdentity(c))
return;
userChangeset = Changeset.compose(userChangeset, c, apool); userChangeset = Changeset.compose(userChangeset, c, apool);
setChangeCallbackTimeout(); setChangeCallbackTimeout();
}, },
applyChangesToBase: (c, optAuthor, apoolJsonObj) => { applyChangesToBase: (c, optAuthor, apoolJsonObj) => {
if (!tracking) return; if (!tracking)
return;
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => { aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
if (apoolJsonObj) { if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj); const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool); c = Changeset.moveOpsToNewPool(c, wireApool, apool);
} }
baseAText = Changeset.applyToAText(c, baseAText, apool); baseAText = Changeset.applyToAText(c, baseAText, apool);
let c2 = c; let c2 = c;
if (submittedChangeset) { if (submittedChangeset) {
const oldSubmittedChangeset = submittedChangeset; const oldSubmittedChangeset = submittedChangeset;
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool); submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool); c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
} }
const preferInsertingAfterUserChanges = true; const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset; const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow( userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool); const postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor); const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true; applyingNonUserChanges = true;
try { try {
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret); callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
} finally { }
finally {
applyingNonUserChanges = false; applyingNonUserChanges = false;
} }
}); });
@ -136,15 +108,14 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// submission must have been canceled, prepare new changeset // submission must have been canceled, prepare new changeset
// that includes old submittedChangeset // that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool); toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
} else { }
else {
// Get my authorID // Get my authorID
const authorId = parent.parent.pad.myUserInfo.userId; const authorId = parent.parent.pad.myUserInfo.userId;
// Sanitize authorship: Replace all author attributes with this user's author ID in case the // Sanitize authorship: Replace all author attributes with this user's author ID in case the
// text was copied from another author. // text was copied from another author.
const cs = Changeset.unpack(userChangeset); const cs = Changeset.unpack(userChangeset);
const assem = Changeset.mergingOpAssembler(); const assem = Changeset.mergingOpAssembler();
for (const op of Changeset.deserializeOps(cs.ops)) { for (const op of Changeset.deserializeOps(cs.ops)) {
if (op.opcode === '+') { if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, apool); const attribs = AttributeMap.fromString(op.attribs, apool);
@ -159,16 +130,15 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
assem.endDocument(); assem.endDocument();
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank); userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(userChangeset); Changeset.checkRep(userChangeset);
if (Changeset.isIdentity(userChangeset))
if (Changeset.isIdentity(userChangeset)) toSubmit = null; toSubmit = null;
else toSubmit = userChangeset; else
toSubmit = userChangeset;
} }
let cs = null; let cs = null;
if (toSubmit) { if (toSubmit) {
submittedChangeset = toSubmit; submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit)); userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
cs = toSubmit; cs = toSubmit;
} }
let wireApool = null; let wireApool = null;
@ -177,7 +147,6 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
wireApool = forWire.pool.toJsonable(); wireApool = forWire.pool.toJsonable();
cs = forWire.translated; cs = forWire.translated;
} }
const data = { const data = {
changeset: cs, changeset: cs,
apool: wireApool, apool: wireApool,
@ -199,5 +168,4 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))), hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
}; };
}; };
export { makeChangesetTracker };
exports.makeChangesetTracker = makeChangesetTracker;

View file

@ -1,31 +1,16 @@
import ChatMessage from "./ChatMessage.js";
import { padutils as padutils$0 } from "./pad_utils.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
import Tinycon from "tinycon/tinycon";
import * as hooks from "./pluginfw/hooks.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
'use strict'; 'use strict';
/** const padutils = { padutils: padutils$0 }.padutils;
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) const padcookie = { padcookie: padcookie$0 }.padcookie;
* const padeditor = { padeditor: padeditor$0 }.padeditor;
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const ChatMessage = require('./ChatMessage');
const padutils = require('./pad_utils').padutils;
const padcookie = require('./pad_cookie').padcookie;
const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor;
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463 // Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
export const chat = (() => {
exports.chat = (() => {
let isStuck = false; let isStuck = false;
let userAndChat = false; let userAndChat = false;
let chatMentions = 0; let chatMentions = 0;
@ -58,7 +43,6 @@ exports.chat = (() => {
$('#chatbox, .sticky-container').toggleClass('stickyChat', isStuck); $('#chatbox, .sticky-container').toggleClass('stickyChat', isStuck);
$('#chatbox').css('display', 'flex'); $('#chatbox').css('display', 'flex');
}, 0); }, 0);
padcookie.setPref('chatAlwaysVisible', isStuck); padcookie.setPref('chatAlwaysVisible', isStuck);
$('#options-stickychat').prop('checked', isStuck); $('#options-stickychat').prop('checked', isStuck);
}, },
@ -70,7 +54,8 @@ exports.chat = (() => {
$('#options-chatandusers').prop('checked', true); $('#options-chatandusers').prop('checked', true);
$('#options-stickychat').prop('disabled', 'disabled'); $('#options-stickychat').prop('disabled', 'disabled');
userAndChat = true; userAndChat = true;
} else { }
else {
$('#options-stickychat').prop('disabled', false); $('#options-stickychat').prop('disabled', false);
userAndChat = false; userAndChat = false;
} }
@ -84,7 +69,8 @@ exports.chat = (() => {
if ($('#options-stickychat').prop('checked')) { if ($('#options-stickychat').prop('checked')) {
this.stickToScreen(); this.stickToScreen();
$('#options-stickychat').prop('checked', false); $('#options-stickychat').prop('checked', false);
} else { }
else {
$('#chatcounter').text('0'); $('#chatcounter').text('0');
$('#chaticon').addClass('visible'); $('#chaticon').addClass('visible');
$('#chatbox').removeClass('visible'); $('#chatbox').removeClass('visible');
@ -96,16 +82,15 @@ exports.chat = (() => {
this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) { this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) {
// if we use a slow animate here we can have a race condition // if we use a slow animate here we can have a race condition
// when a users focus can not be moved away from the last message recieved. // when a users focus can not be moved away from the last message recieved.
$('#chattext').animate( $('#chattext').animate({ scrollTop: $('#chattext')[0].scrollHeight }, { duration: 400, queue: false });
{scrollTop: $('#chattext')[0].scrollHeight},
{duration: 400, queue: false});
this.lastMessage = $('#chattext > p').eq(-1); this.lastMessage = $('#chattext > p').eq(-1);
} }
} }
}, },
async send() { async send() {
const text = $('#chatinput').val(); const text = $('#chatinput').val();
if (text.replace(/\s+/, '').length === 0) return; if (text.replace(/\s+/, '').length === 0)
return;
const message = new ChatMessage(text); const message = new ChatMessage(text);
await hooks.aCallAll('chatSendMessage', Object.freeze({ message })); await hooks.aCallAll('chatSendMessage', Object.freeze({ message }));
this._pad.collabClient.sendMessage({ type: 'CHAT_MESSAGE', message }); this._pad.collabClient.sendMessage({ type: 'CHAT_MESSAGE', message });
@ -115,7 +100,6 @@ exports.chat = (() => {
msg = ChatMessage.fromObject(msg); msg = ChatMessage.fromObject(msg);
// correct the time // correct the time
msg.time += this._pad.clientTimeOffset; msg.time += this._pad.clientTimeOffset;
if (!msg.authorId) { if (!msg.authorId) {
/* /*
* If, for a bug or a database corruption, the message coming from the * If, for a bug or a database corruption, the message coming from the
@ -123,16 +107,14 @@ exports.chat = (() => {
* let's be defensive and replace it with "unknown". * let's be defensive and replace it with "unknown".
*/ */
msg.authorId = 'unknown'; msg.authorId = 'unknown';
console.warn( console.warn('The "authorId" field of a chat message coming from the server was not present. ' +
'The "authorId" field of a chat message coming from the server was not present. ' +
'Replacing with "unknown". This may be a bug or a database corruption.'); 'Replacing with "unknown". This may be a bug or a database corruption.');
} }
const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => { const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-'; if (c === '.')
return '-';
return `z${c.charCodeAt(0)}z`; return `z${c.charCodeAt(0)}z`;
})}`; })}`;
// the hook args // the hook args
const ctx = { const ctx = {
authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'), authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'),
@ -145,32 +127,28 @@ exports.chat = (() => {
timeStr: (() => { timeStr: (() => {
let minutes = `${new Date(msg.time).getMinutes()}`; let minutes = `${new Date(msg.time).getMinutes()}`;
let hours = `${new Date(msg.time).getHours()}`; let hours = `${new Date(msg.time).getHours()}`;
if (minutes.length === 1) minutes = `0${minutes}`; if (minutes.length === 1)
if (hours.length === 1) hours = `0${hours}`; minutes = `0${minutes}`;
if (hours.length === 1)
hours = `0${hours}`;
return `${hours}:${minutes}`; return `${hours}:${minutes}`;
})(), })(),
duration: 4000, duration: 4000,
}; };
// is the users focus already in the chatbox? // is the users focus already in the chatbox?
const alreadyFocused = $('#chatinput').is(':focus'); const alreadyFocused = $('#chatinput').is(':focus');
// does the user already have the chatbox open? // does the user already have the chatbox open?
const chatOpen = $('#chatbox').hasClass('visible'); const chatOpen = $('#chatbox').hasClass('visible');
// does this message contain this user's name? (is the current user mentioned?) // does this message contain this user's name? (is the current user mentioned?)
const wasMentioned = const wasMentioned = msg.authorId !== window.clientVars.userId &&
msg.authorId !== window.clientVars.userId &&
ctx.authorName !== html10n.get('pad.userlist.unnamed') && ctx.authorName !== html10n.get('pad.userlist.unnamed') &&
normalize(ctx.text).includes(normalize(ctx.authorName)); normalize(ctx.text).includes(normalize(ctx.authorName));
// If the user was mentioned, make the message sticky // If the user was mentioned, make the message sticky
if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) {
chatMentions++; chatMentions++;
Tinycon.setBubble(chatMentions); Tinycon.setBubble(chatMentions);
ctx.sticky = true; ctx.sticky = true;
} }
await hooks.aCallAll('chatNewMessage', ctx); await hooks.aCallAll('chatNewMessage', ctx);
const cls = authorClass(ctx.author); const cls = authorClass(ctx.author);
const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('<p>') const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('<p>')
@ -187,17 +165,17 @@ exports.chat = (() => {
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not
// introduce an XSS vulnerability by adding unescaped user input. // introduce an XSS vulnerability by adding unescaped user input.
.append($('<div>').html(ctx.text).contents()); .append($('<div>').html(ctx.text).contents());
if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton'); if (isHistoryAdd)
else $('#chattext').append(chatMsg); chatMsg.insertAfter('#chatloadmessagesbutton');
else
$('#chattext').append(chatMsg);
chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e)); chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e));
// should we increment the counter?? // should we increment the counter??
if (increment && !isHistoryAdd) { if (increment && !isHistoryAdd) {
// Update the counter of unread messages // Update the counter of unread messages
let count = Number($('#chatcounter').text()); let count = Number($('#chatcounter').text());
count++; count++;
$('#chatcounter').text(count); $('#chatcounter').text(count);
if (!chatOpen && ctx.duration > 0) { if (!chatOpen && ctx.duration > 0) {
const text = $('<p>') const text = $('<p>')
.append($('<span>').addClass('author-name').text(ctx.authorName)) .append($('<span>').addClass('author-name').text(ctx.authorName))
@ -214,7 +192,8 @@ exports.chat = (() => {
}); });
} }
} }
if (!isHistoryAdd) this.scrollDown(); if (!isHistoryAdd)
this.scrollDown();
}, },
init(pad) { init(pad) {
this._pad = pad; this._pad = pad;
@ -234,7 +213,6 @@ exports.chat = (() => {
chatMentions = 0; chatMentions = 0;
Tinycon.setBubble(0); Tinycon.setBubble(0);
}); });
const self = this; const self = this;
$('body:not(#chatinput)').on('keypress', function (evt) { $('body:not(#chatinput)').on('keypress', function (evt) {
if (evt.altKey && evt.which === 67) { if (evt.altKey && evt.which === 67) {
@ -245,7 +223,6 @@ exports.chat = (() => {
evt.preventDefault(); evt.preventDefault();
} }
}); });
$('#chatinput').keypress((evt) => { $('#chatinput').keypress((evt) => {
// if the user typed enter, fire the send // if the user typed enter, fire the send
if (evt.key === 'Enter' && !evt.shiftKey) { if (evt.key === 'Enter' && !evt.shiftKey) {
@ -253,19 +230,15 @@ exports.chat = (() => {
this.send(); this.send();
} }
}); });
// initial messages are loaded in pad.js' _afterHandshake // initial messages are loaded in pad.js' _afterHandshake
$('#chatcounter').text(0); $('#chatcounter').text(0);
$('#chatloadmessagesbutton').click(() => { $('#chatloadmessagesbutton').click(() => {
const start = Math.max(this.historyPointer - 20, 0); const start = Math.max(this.historyPointer - 20, 0);
const end = this.historyPointer; const end = this.historyPointer;
if (start === end)
if (start === end) return; // nothing to load return; // nothing to load
$('#chatloadmessagesbutton').css('display', 'none'); $('#chatloadmessagesbutton').css('display', 'none');
$('#chatloadmessagesball').css('display', 'block'); $('#chatloadmessagesball').css('display', 'block');
pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end }); pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end });
this.historyPointer = start; this.historyPointer = start;
}); });

View file

@ -1,11 +1,12 @@
import { chat as chat$0 } from "./chat.js";
import * as hooks from "./pluginfw/hooks.js";
import browser from "./vendors/browser.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,23 +22,17 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const chat = { chat: chat$0 }.chat;
const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks');
const browser = require('./vendors/browser');
// Dependency fill on init. This exists for `pad.socket` only. // Dependency fill on init. This exists for `pad.socket` only.
// TODO: bind directly to the socket. // TODO: bind directly to the socket.
let pad = undefined; let pad = undefined;
const getSocket = () => pad && pad.socket; const getSocket = () => pad && pad.socket;
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited. /** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
ACE's ready callback does not need to have fired yet. ACE's ready callback does not need to have fired yet.
"serverVars" are from calling doc.getCollabClientVars() on the server. */ "serverVars" are from calling doc.getCollabClientVars() on the server. */
const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) => { const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) => {
const editor = ace2editor; const editor = ace2editor;
pad = _pad; // Inject pad to avoid a circular dependency. pad = _pad; // Inject pad to avoid a circular dependency.
let rev = serverVars.rev; let rev = serverVars.rev;
let committing = false; let committing = false;
let stateMessage; let stateMessage;
@ -45,14 +40,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
let lastCommitTime = 0; let lastCommitTime = 0;
let initialStartConnectTime = 0; let initialStartConnectTime = 0;
let commitDelay = 500; let commitDelay = 500;
const userId = initialUserInfo.userId; const userId = initialUserInfo.userId;
// var socket; // var socket;
const userSet = {}; // userId -> userInfo const userSet = {}; // userId -> userInfo
userSet[userId] = initialUserInfo; userSet[userId] = initialUserInfo;
let isPendingRevision = false; let isPendingRevision = false;
const callbacks = { const callbacks = {
onUserJoin: () => { }, onUserJoin: () => { },
onUserLeave: () => { }, onUserLeave: () => { },
@ -72,7 +64,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
} }
}); });
} }
const handleUserChanges = () => { const handleUserChanges = () => {
if (editor.getInInternationalComposition()) { if (editor.getInInternationalComposition()) {
// handleUserChanges() will be called again once composition ends so there's no need to set up // handleUserChanges() will be called again once composition ends so there's no need to set up
@ -83,32 +74,32 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
if ((!getSocket()) || channelState === 'CONNECTING') { if ((!getSocket()) || channelState === 'CONNECTING') {
if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) { if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) {
setChannelState('DISCONNECTED', 'initsocketfail'); setChannelState('DISCONNECTED', 'initsocketfail');
} else { }
else {
// check again in a bit // check again in a bit
setTimeout(handleUserChanges, 1000); setTimeout(handleUserChanges, 1000);
} }
return; return;
} }
if (committing) { if (committing) {
if (now - lastCommitTime > 20000) { if (now - lastCommitTime > 20000) {
// a commit is taking too long // a commit is taking too long
setChannelState('DISCONNECTED', 'slowcommit'); setChannelState('DISCONNECTED', 'slowcommit');
} else if (now - lastCommitTime > 5000) { }
else if (now - lastCommitTime > 5000) {
callbacks.onConnectionTrouble('SLOW'); callbacks.onConnectionTrouble('SLOW');
} else { }
else {
// run again in a few seconds, to detect a disconnect // run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000); setTimeout(handleUserChanges, 3000);
} }
return; return;
} }
const earliestCommit = lastCommitTime + commitDelay; const earliestCommit = lastCommitTime + commitDelay;
if (now < earliestCommit) { if (now < earliestCommit) {
setTimeout(handleUserChanges, earliestCommit - now); setTimeout(handleUserChanges, earliestCommit - now);
return; return;
} }
let sentMessage = false; let sentMessage = false;
// Check if there are any pending revisions to be received from server. // Check if there are any pending revisions to be received from server.
// Allow only if there are no pending revisions to be received from server // Allow only if there are no pending revisions to be received from server
@ -127,48 +118,42 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
sentMessage = true; sentMessage = true;
callbacks.onInternalAction('commitPerformed'); callbacks.onInternalAction('commitPerformed');
} }
} else { }
else {
// run again in a few seconds, to check if there was a reconnection attempt // run again in a few seconds, to check if there was a reconnection attempt
setTimeout(handleUserChanges, 3000); setTimeout(handleUserChanges, 3000);
} }
if (sentMessage) { if (sentMessage) {
// run again in a few seconds, to detect a disconnect // run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000); setTimeout(handleUserChanges, 3000);
} }
}; };
const acceptCommit = () => { const acceptCommit = () => {
editor.applyPreparedChangesetToBase(); editor.applyPreparedChangesetToBase();
setStateIdle(); setStateIdle();
try { try {
callbacks.onInternalAction('commitAcceptedByServer'); callbacks.onInternalAction('commitAcceptedByServer');
callbacks.onConnectionTrouble('OK'); callbacks.onConnectionTrouble('OK');
} catch (err) { /* intentionally ignored */ } }
catch (err) { /* intentionally ignored */ }
handleUserChanges(); handleUserChanges();
}; };
const setUpSocket = () => { const setUpSocket = () => {
setChannelState('CONNECTED'); setChannelState('CONNECTED');
doDeferredActions(); doDeferredActions();
initialStartConnectTime = Date.now(); initialStartConnectTime = Date.now();
}; };
const sendMessage = (msg) => { const sendMessage = (msg) => {
getSocket().json.send( getSocket().json.send({
{
type: 'COLLABROOM', type: 'COLLABROOM',
component: 'pad', component: 'pad',
data: msg, data: msg,
}); });
}; };
const serverMessageTaskQueue = new class { const serverMessageTaskQueue = new class {
constructor() { constructor() {
this._promiseChain = Promise.resolve(); this._promiseChain = Promise.resolve();
} }
async enqueue(fn) { async enqueue(fn) {
const taskPromise = this._promiseChain.then(fn); const taskPromise = this._promiseChain.then(fn);
// Use .catch() to prevent rejections from halting the queue. // Use .catch() to prevent rejections from halting the queue.
@ -178,14 +163,15 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
return await taskPromise; return await taskPromise;
} }
}(); }();
const handleMessageFromServer = (evt) => { const handleMessageFromServer = (evt) => {
if (!getSocket()) return; if (!getSocket())
if (!evt.data) return; return;
if (!evt.data)
return;
const wrapper = evt; const wrapper = evt;
if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM') return; if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM')
return;
const msg = wrapper.data; const msg = wrapper.data;
if (msg.type === 'NEW_CHANGES') { if (msg.type === 'NEW_CHANGES') {
serverMessageTaskQueue.enqueue(async () => { serverMessageTaskQueue.enqueue(async () => {
// Avoid updating the DOM while the user is composing a character. Notes about this `await`: // Avoid updating the DOM while the user is composing a character. Notes about this `await`:
@ -205,7 +191,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev; rev = newRev;
editor.applyChangesToBase(changeset, author, apool); editor.applyChangesToBase(changeset, author, apool);
}); });
} else if (msg.type === 'ACCEPT_COMMIT') { }
else if (msg.type === 'ACCEPT_COMMIT') {
serverMessageTaskQueue.enqueue(() => { serverMessageTaskQueue.enqueue(() => {
const { newRev } = msg; const { newRev } = msg;
// newRev will equal rev if the changeset has no net effect (identity changeset, removing // newRev will equal rev if the changeset has no net effect (identity changeset, removing
@ -219,7 +206,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev; rev = newRev;
acceptCommit(); acceptCommit();
}); });
} else if (msg.type === 'CLIENT_RECONNECT') { }
else if (msg.type === 'CLIENT_RECONNECT') {
// Server sends a CLIENT_RECONNECT message when there is a client reconnect. // Server sends a CLIENT_RECONNECT message when there is a client reconnect.
// Server also returns all pending revisions along with this CLIENT_RECONNECT message // Server also returns all pending revisions along with this CLIENT_RECONNECT message
serverMessageTaskQueue.enqueue(() => { serverMessageTaskQueue.enqueue(() => {
@ -237,7 +225,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev; rev = newRev;
if (author === pad.getUserId()) { if (author === pad.getUserId()) {
acceptCommit(); acceptCommit();
} else { }
else {
editor.applyChangesToBase(changeset, author, apool); editor.applyChangesToBase(changeset, author, apool);
} }
if (newRev === headRev) { if (newRev === headRev) {
@ -245,18 +234,21 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
setIsPendingRevision(false); setIsPendingRevision(false);
} }
}); });
} else if (msg.type === 'USER_NEWINFO') { }
else if (msg.type === 'USER_NEWINFO') {
const userInfo = msg.userInfo; const userInfo = msg.userInfo;
const id = userInfo.userId; const id = userInfo.userId;
if (userSet[id]) { if (userSet[id]) {
userSet[id] = userInfo; userSet[id] = userInfo;
callbacks.onUpdateUserInfo(userInfo); callbacks.onUpdateUserInfo(userInfo);
} else { }
else {
userSet[id] = userInfo; userSet[id] = userInfo;
callbacks.onUserJoin(userInfo); callbacks.onUserJoin(userInfo);
} }
tellAceActiveAuthorInfo(userInfo); tellAceActiveAuthorInfo(userInfo);
} else if (msg.type === 'USER_LEAVE') { }
else if (msg.type === 'USER_LEAVE') {
const userInfo = msg.userInfo; const userInfo = msg.userInfo;
const id = userInfo.userId; const id = userInfo.userId;
if (userSet[id]) { if (userSet[id]) {
@ -264,11 +256,14 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
fadeAceAuthorInfo(userInfo); fadeAceAuthorInfo(userInfo);
callbacks.onUserLeave(userInfo); callbacks.onUserLeave(userInfo);
} }
} else if (msg.type === 'CLIENT_MESSAGE') { }
else if (msg.type === 'CLIENT_MESSAGE') {
callbacks.onClientMessage(msg.payload); callbacks.onClientMessage(msg.payload);
} else if (msg.type === 'CHAT_MESSAGE') { }
else if (msg.type === 'CHAT_MESSAGE') {
chat.addMessage(msg.message, true, false); chat.addMessage(msg.message, true, false);
} else if (msg.type === 'CHAT_MESSAGES') { }
else if (msg.type === 'CHAT_MESSAGES') {
for (let i = msg.messages.length - 1; i >= 0; i--) { for (let i = msg.messages.length - 1; i >= 0; i--) {
chat.addMessage(msg.messages[i], true, true); chat.addMessage(msg.messages[i], true, true);
} }
@ -277,19 +272,17 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
chat.gotInitalMessages = true; chat.gotInitalMessages = true;
chat.historyPointer = clientVars.chatHead - msg.messages.length; chat.historyPointer = clientVars.chatHead - msg.messages.length;
} }
// messages are loaded, so hide the loading-ball // messages are loaded, so hide the loading-ball
$('#chatloadmessagesball').css('display', 'none'); $('#chatloadmessagesball').css('display', 'none');
// there are less than 100 messages or we reached the top // there are less than 100 messages or we reached the top
if (chat.historyPointer <= 0) { if (chat.historyPointer <= 0) {
$('#chatloadmessagesbutton').css('display', 'none'); $('#chatloadmessagesbutton').css('display', 'none');
} else { }
else {
// there are still more messages, re-show the load-button // there are still more messages, re-show the load-button
$('#chatloadmessagesbutton').css('display', 'block'); $('#chatloadmessagesbutton').css('display', 'block');
} }
} }
// HACKISH: User messages do not have "payload" but "userInfo", so that all // HACKISH: User messages do not have "payload" but "userInfo", so that all
// "handleClientMessage_USER_" hooks would work, populate payload // "handleClientMessage_USER_" hooks would work, populate payload
// FIXME: USER_* messages to have "payload" property instead of "userInfo", // FIXME: USER_* messages to have "payload" property instead of "userInfo",
@ -298,51 +291,45 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
msg.payload = msg.userInfo; msg.payload = msg.userInfo;
} }
// Similar for NEW_CHANGES // Similar for NEW_CHANGES
if (msg.type === 'NEW_CHANGES') msg.payload = msg; if (msg.type === 'NEW_CHANGES')
msg.payload = msg;
hooks.callAll(`handleClientMessage_${msg.type}`, { payload: msg.payload }); hooks.callAll(`handleClientMessage_${msg.type}`, { payload: msg.payload });
}; };
const updateUserInfo = (userInfo) => { const updateUserInfo = (userInfo) => {
userInfo.userId = userId; userInfo.userId = userId;
userSet[userId] = userInfo; userSet[userId] = userInfo;
tellAceActiveAuthorInfo(userInfo); tellAceActiveAuthorInfo(userInfo);
if (!getSocket()) return; if (!getSocket())
sendMessage( return;
{ sendMessage({
type: 'USERINFO_UPDATE', type: 'USERINFO_UPDATE',
userInfo, userInfo,
}); });
}; };
const tellAceActiveAuthorInfo = (userInfo) => { const tellAceActiveAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId); tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
}; };
const tellAceAuthorInfo = (userId, colorId, inactive) => { const tellAceAuthorInfo = (userId, colorId, inactive) => {
if (typeof colorId === 'number') { if (typeof colorId === 'number') {
colorId = clientVars.colorPalette[colorId]; colorId = clientVars.colorPalette[colorId];
} }
const cssColor = colorId; const cssColor = colorId;
if (inactive) { if (inactive) {
editor.setAuthorInfo(userId, { editor.setAuthorInfo(userId, {
bgcolor: cssColor, bgcolor: cssColor,
fade: 0.5, fade: 0.5,
}); });
} else { }
else {
editor.setAuthorInfo(userId, { editor.setAuthorInfo(userId, {
bgcolor: cssColor, bgcolor: cssColor,
}); });
} }
}; };
const fadeAceAuthorInfo = (userInfo) => { const fadeAceAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true); tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
}; };
const getConnectedUsers = () => valuesArray(userSet); const getConnectedUsers = () => valuesArray(userSet);
const tellAceAboutHistoricalAuthors = (hadata) => { const tellAceAboutHistoricalAuthors = (hadata) => {
for (const [author, data] of Object.entries(hadata)) { for (const [author, data] of Object.entries(hadata)) {
if (!userSet[author]) { if (!userSet[author]) {
@ -350,14 +337,12 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
} }
} }
}; };
const setChannelState = (newChannelState, moreInfo) => { const setChannelState = (newChannelState, moreInfo) => {
if (newChannelState !== channelState) { if (newChannelState !== channelState) {
channelState = newChannelState; channelState = newChannelState;
callbacks.onChannelStateChange(channelState, moreInfo); callbacks.onChannelStateChange(channelState, moreInfo);
} }
}; };
const valuesArray = (obj) => { const valuesArray = (obj) => {
const array = []; const array = [];
$.each(obj, (k, v) => { $.each(obj, (k, v) => {
@ -365,11 +350,9 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
}); });
return array; return array;
}; };
// We need to present a working interface even before the socket // We need to present a working interface even before the socket
// is connected for the first time. // is connected for the first time.
let deferredActions = []; let deferredActions = [];
const defer = (func, tag) => function (...args) { const defer = (func, tag) => function (...args) {
const action = () => { const action = () => {
func.call(this, ...args); func.call(this, ...args);
@ -377,34 +360,31 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
action.tag = tag; action.tag = tag;
if (channelState === 'CONNECTING') { if (channelState === 'CONNECTING') {
deferredActions.push(action); deferredActions.push(action);
} else { }
else {
action(); action();
} }
}; };
const doDeferredActions = (tag) => { const doDeferredActions = (tag) => {
const newArray = []; const newArray = [];
for (let i = 0; i < deferredActions.length; i++) { for (let i = 0; i < deferredActions.length; i++) {
const a = deferredActions[i]; const a = deferredActions[i];
if ((!tag) || (tag === a.tag)) { if ((!tag) || (tag === a.tag)) {
a(); a();
} else { }
else {
newArray.push(a); newArray.push(a);
} }
} }
deferredActions = newArray; deferredActions = newArray;
}; };
const sendClientMessage = (msg) => { const sendClientMessage = (msg) => {
sendMessage( sendMessage({
{
type: 'CLIENT_MESSAGE', type: 'CLIENT_MESSAGE',
payload: msg, payload: msg,
}); });
}; };
const getCurrentRevisionNumber = () => rev; const getCurrentRevisionNumber = () => rev;
const getMissedChanges = () => { const getMissedChanges = () => {
const obj = {}; const obj = {};
obj.userInfo = userSet[userId]; obj.userInfo = userSet[userId];
@ -421,24 +401,19 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
} }
return obj; return obj;
}; };
const setStateIdle = () => { const setStateIdle = () => {
committing = false; committing = false;
callbacks.onInternalAction('newlyIdle'); callbacks.onInternalAction('newlyIdle');
schedulePerhapsCallIdleFuncs(); schedulePerhapsCallIdleFuncs();
}; };
const setIsPendingRevision = (value) => { const setIsPendingRevision = (value) => {
isPendingRevision = value; isPendingRevision = value;
}; };
const idleFuncs = []; const idleFuncs = [];
const callWhenNotCommitting = (func) => { const callWhenNotCommitting = (func) => {
idleFuncs.push(func); idleFuncs.push(func);
schedulePerhapsCallIdleFuncs(); schedulePerhapsCallIdleFuncs();
}; };
const schedulePerhapsCallIdleFuncs = () => { const schedulePerhapsCallIdleFuncs = () => {
setTimeout(() => { setTimeout(() => {
if (!committing) { if (!committing) {
@ -449,7 +424,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
} }
}, 0); }, 0);
}; };
const self = { const self = {
setOnUserJoin: (cb) => { setOnUserJoin: (cb) => {
callbacks.onUserJoin = cb; callbacks.onUserJoin = cb;
@ -487,16 +461,12 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
set commitDelay(ms) { commitDelay = ms; }, set commitDelay(ms) { commitDelay = ms; },
get commitDelay() { return commitDelay; }, get commitDelay() { return commitDelay; },
}; };
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData); tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
tellAceActiveAuthorInfo(initialUserInfo); tellAceActiveAuthorInfo(initialUserInfo);
editor.setProperty('userAuthor', userId); editor.setProperty('userAuthor', userId);
editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool); editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
editor.setUserChangeNotificationCallback(handleUserChanges); editor.setUserChangeNotificationCallback(handleUserChanges);
setUpSocket(); setUpSocket();
return self; return self;
}; };
export { getCollabClient };
exports.getCollabClient = getCollabClient;

View file

@ -1,11 +1,9 @@
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js // DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS // THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS
/** /**
@ -23,17 +21,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const colorutils = {}; const colorutils = {};
// Check that a given value is a css hex color value, e.g. // Check that a given value is a css hex color value, e.g.
// "#ffffff" or "#fff" // "#ffffff" or "#fff"
colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor);
// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] // "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0]
colorutils.css2triple = (cssColor) => { colorutils.css2triple = (cssColor) => {
const sixHex = colorutils.css2sixhex(cssColor); const sixHex = colorutils.css2sixhex(cssColor);
const hexToFloat = (hh) => Number(`0x${hh}`) / 255; const hexToFloat = (hh) => Number(`0x${hh}`) / 255;
return [ return [
hexToFloat(sixHex.substr(0, 2)), hexToFloat(sixHex.substr(0, 2)),
@ -41,7 +35,6 @@ colorutils.css2triple = (cssColor) => {
hexToFloat(sixHex.substr(4, 2)), hexToFloat(sixHex.substr(4, 2)),
]; ];
}; };
// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" // "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff"
colorutils.css2sixhex = (cssColor) => { colorutils.css2sixhex = (cssColor) => {
let h = /[0-9a-fA-F]+/.exec(cssColor)[0]; let h = /[0-9a-fA-F]+/.exec(cssColor)[0];
@ -53,7 +46,6 @@ colorutils.css2sixhex = (cssColor) => {
} }
return h; return h;
}; };
// [1.0, 1.0, 1.0] -> "#ffffff" // [1.0, 1.0, 1.0] -> "#ffffff"
colorutils.triple2css = (triple) => { colorutils.triple2css = (triple) => {
const floatToHex = (n) => { const floatToHex = (n) => {
@ -62,8 +54,6 @@ colorutils.triple2css = (triple) => {
}; };
return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`; return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`;
}; };
colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v); colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v);
colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c); colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c);
colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c); colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c);
@ -71,37 +61,31 @@ colorutils.colorMin = (c) => colorutils.min3(c[0], c[1], c[2]);
colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]); colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]);
colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1); colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1);
colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1); colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1);
colorutils.scaleColor = (c, bot, top) => [ colorutils.scaleColor = (c, bot, top) => [
colorutils.scale(c[0], bot, top), colorutils.scale(c[0], bot, top),
colorutils.scale(c[1], bot, top), colorutils.scale(c[1], bot, top),
colorutils.scale(c[2], bot, top), colorutils.scale(c[2], bot, top),
]; ];
colorutils.unscaleColor = (c, bot, top) => [ colorutils.unscaleColor = (c, bot, top) => [
colorutils.unscale(c[0], bot, top), colorutils.unscale(c[0], bot, top),
colorutils.unscale(c[1], bot, top), colorutils.unscale(c[1], bot, top),
colorutils.unscale(c[2], bot, top), colorutils.unscale(c[2], bot, top),
]; ];
// rule of thumb for RGB brightness; 1.0 is white // rule of thumb for RGB brightness; 1.0 is white
colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11; colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11;
colorutils.saturate = (c) => { colorutils.saturate = (c) => {
const min = colorutils.colorMin(c); const min = colorutils.colorMin(c);
const max = colorutils.colorMax(c); const max = colorutils.colorMax(c);
if (max - min <= 0) return [1.0, 1.0, 1.0]; if (max - min <= 0)
return [1.0, 1.0, 1.0];
return colorutils.unscaleColor(c, min, max); return colorutils.unscaleColor(c, min, max);
}; };
colorutils.blend = (c1, c2, t) => [ colorutils.blend = (c1, c2, t) => [
colorutils.scale(t, c1[0], c2[0]), colorutils.scale(t, c1[0], c2[0]),
colorutils.scale(t, c1[1], c2[1]), colorutils.scale(t, c1[1], c2[1]),
colorutils.scale(t, c1[2], c2[2]), colorutils.scale(t, c1[2], c2[2]),
]; ];
colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]]; colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]];
colorutils.complementary = (c) => { colorutils.complementary = (c) => {
const inv = colorutils.invert(c); const inv = colorutils.invert(c);
return [ return [
@ -110,12 +94,9 @@ colorutils.complementary = (c) => {
(inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11), (inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11),
]; ];
}; };
colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => { colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => {
const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff'; const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff';
const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222'; const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222';
return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black; return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black;
}; };
export { colorutils };
exports.colorutils = colorutils;

View file

@ -1,10 +1,13 @@
import AttributeMap from "./AttributeMap.js";
import UNorm from "unorm";
import * as Changeset from "./Changeset.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector // THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); // %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins"); // %APPJET%: import("etherpad.admin.plugins");
@ -23,14 +26,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const _MAX_LIST_LEVEL = 16; const _MAX_LIST_LEVEL = 16;
const AttributeMap = require('./AttributeMap');
const UNorm = require('unorm');
const Changeset = require('./Changeset');
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s); const sanitizeUnicode = (s) => UNorm.nfc(s);
const tagName = (n) => n.tagName && n.tagName.toLowerCase(); const tagName = (n) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin // supportedElems are Supported natively within Etherpad and don't require a plugin
@ -55,7 +51,6 @@ const supportedElems = new Set([
'u', 'u',
'ul', 'ul',
]); ]);
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => { const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
const _blockElems = { const _blockElems = {
div: 1, div: 1,
@ -63,22 +58,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
pre: 1, pre: 1,
li: 1, li: 1,
}; };
hooks.callAll('ccRegisterBlockElements').forEach((element) => { hooks.callAll('ccRegisterBlockElements').forEach((element) => {
_blockElems[element] = 1; _blockElems[element] = 1;
supportedElems.add(element); supportedElems.add(element);
}); });
const isBlockElement = (n) => !!_blockElems[tagName(n) || '']; const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
const textify = (str) => sanitizeUnicode(str.replace(/(\n | \n)/g, ' ')
const textify = (str) => sanitizeUnicode(
str.replace(/(\n | \n)/g, ' ')
.replace(/[\n\r ]/g, ' ') .replace(/[\n\r ]/g, ' ')
.replace(/\xa0/g, ' ') .replace(/\xa0/g, ' ')
.replace(/\t/g, ' ')); .replace(/\t/g, ' '));
const getAssoc = (node, name) => node[`_magicdom_${name}`]; const getAssoc = (node, name) => node[`_magicdom_${name}`];
const lines = (() => { const lines = (() => {
const textArray = []; const textArray = [];
const attribsArray = []; const attribsArray = [];
@ -113,7 +102,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
return self; return self;
})(); })();
const cc = {}; const cc = {};
const _ensureColumnZero = (state) => { const _ensureColumnZero = (state) => {
if (!lines.atColumnZero()) { if (!lines.atColumnZero()) {
cc.startNewLine(state); cc.startNewLine(state);
@ -124,7 +112,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
let selEnd = [-1, -1]; let selEnd = [-1, -1];
const _isEmpty = (node, state) => { const _isEmpty = (node, state) => {
// consider clean blank lines pasted in IE to be empty // consider clean blank lines pasted in IE to be empty
if (node.childNodes.length === 0) return true; if (node.childNodes.length === 0)
return true;
if (node.childNodes.length === 1 && if (node.childNodes.length === 1 &&
getAssoc(node, 'shouldBeEmpty') && getAssoc(node, 'shouldBeEmpty') &&
node.innerHTML === '&nbsp;' && node.innerHTML === '&nbsp;' &&
@ -138,7 +127,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
return false; return false;
}; };
const _pointHere = (charsAfter, state) => { const _pointHere = (charsAfter, state) => {
const ln = lines.length() - 1; const ln = lines.length() - 1;
let chr = lines.textOfLine(ln).length; let chr = lines.textOfLine(ln).length;
@ -148,11 +136,10 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
chr += charsAfter; chr += charsAfter;
return [ln, chr]; return [ln, chr];
}; };
const _reachBlockPoint = (nd, idx, state) => { const _reachBlockPoint = (nd, idx, state) => {
if (nd.nodeType !== nd.TEXT_NODE) _reachPoint(nd, idx, state); if (nd.nodeType !== nd.TEXT_NODE)
_reachPoint(nd, idx, state);
}; };
const _reachPoint = (nd, idx, state) => { const _reachPoint = (nd, idx, state) => {
if (startPoint && nd === startPoint.node && startPoint.index === idx) { if (startPoint && nd === startPoint.node && startPoint.index === idx) {
selStart = _pointHere(0, state); selStart = _pointHere(0, state);
@ -170,7 +157,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cc.incrementAttrib = (state, attribName) => { cc.incrementAttrib = (state, attribName) => {
if (!state.attribs[attribName]) { if (!state.attribs[attribName]) {
state.attribs[attribName] = 1; state.attribs[attribName] = 1;
} else { }
else {
state.attribs[attribName]++; state.attribs[attribName]++;
} }
_recalcAttribString(state); _recalcAttribString(state);
@ -179,9 +167,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.attribs[attribName]--; state.attribs[attribName]--;
_recalcAttribString(state); _recalcAttribString(state);
}; };
const _enterList = (state, listType) => { const _enterList = (state, listType) => {
if (!listType) return; if (!listType)
return;
const oldListType = state.lineAttributes.list; const oldListType = state.lineAttributes.list;
if (listType !== 'none') { if (listType !== 'none') {
state.listNesting = (state.listNesting || 0) + 1; state.listNesting = (state.listNesting || 0) + 1;
@ -190,29 +178,28 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.start = (state.start || 0) + 1; state.start = (state.start || 0) + 1;
} }
} }
if (listType === 'none') { if (listType === 'none') {
delete state.lineAttributes.list; delete state.lineAttributes.list;
} else { }
else {
state.lineAttributes.list = listType; state.lineAttributes.list = listType;
} }
_recalcAttribString(state); _recalcAttribString(state);
return oldListType; return oldListType;
}; };
const _exitList = (state, oldListType) => { const _exitList = (state, oldListType) => {
if (state.lineAttributes.list) { if (state.lineAttributes.list) {
state.listNesting--; state.listNesting--;
} }
if (oldListType && oldListType !== 'none') { if (oldListType && oldListType !== 'none') {
state.lineAttributes.list = oldListType; state.lineAttributes.list = oldListType;
} else { }
else {
delete state.lineAttributes.list; delete state.lineAttributes.list;
delete state.lineAttributes.start; delete state.lineAttributes.start;
} }
_recalcAttribString(state); _recalcAttribString(state);
}; };
const _enterAuthor = (state, author) => { const _enterAuthor = (state, author) => {
const oldAuthor = state.author; const oldAuthor = state.author;
state.authorLevel = (state.authorLevel || 0) + 1; state.authorLevel = (state.authorLevel || 0) + 1;
@ -220,17 +207,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
_recalcAttribString(state); _recalcAttribString(state);
return oldAuthor; return oldAuthor;
}; };
const _exitAuthor = (state, oldAuthor) => { const _exitAuthor = (state, oldAuthor) => {
state.authorLevel--; state.authorLevel--;
state.author = oldAuthor; state.author = oldAuthor;
_recalcAttribString(state); _recalcAttribString(state);
}; };
const _recalcAttribString = (state) => { const _recalcAttribString = (state) => {
const attribs = new AttributeMap(apool); const attribs = new AttributeMap(apool);
for (const [a, count] of Object.entries(state.attribs)) { for (const [a, count] of Object.entries(state.attribs)) {
if (!count) continue; if (!count)
continue;
// The following splitting of the attribute name is a workaround // The following splitting of the attribute name is a workaround
// to enable the content collector to store key-value attributes // to enable the content collector to store key-value attributes
// see https://github.com/ether/etherpad-lite/issues/2567 for more information // see https://github.com/ether/etherpad-lite/issues/2567 for more information
@ -239,15 +225,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// TODO: This approach doesn't support changing existing values: if both 'foo::bar' and // TODO: This approach doesn't support changing existing values: if both 'foo::bar' and
// 'foo::baz' are in state.attribs then the last one encountered while iterating will win. // 'foo::baz' are in state.attribs then the last one encountered while iterating will win.
const ATTRIBUTE_SPLIT_STRING = '::'; const ATTRIBUTE_SPLIT_STRING = '::';
// see if attributeString is splittable // see if attributeString is splittable
const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING); const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING);
if (attributeSplits.length > 1) { if (attributeSplits.length > 1) {
// the attribute name follows the convention key::value // the attribute name follows the convention key::value
// so save it as a key value attribute // so save it as a key value attribute
const [k, v] = attributeSplits; const [k, v] = attributeSplits;
if (v) attribs.set(k, v); if (v)
} else { attribs.set(k, v);
}
else {
// the "normal" case, the attribute is just a switch // the "normal" case, the attribute is just a switch
// so set it true // so set it true
attribs.set(a, 'true'); attribs.set(a, 'true');
@ -257,12 +244,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (apool.putAttrib(['author', state.author], true) >= 0) { if (apool.putAttrib(['author', state.author], true) >= 0) {
// require that author already be in pool // require that author already be in pool
// (don't add authors from other documents, etc.) // (don't add authors from other documents, etc.)
if (state.author) attribs.set('author', state.author); if (state.author)
attribs.set('author', state.author);
} }
} }
state.attribString = attribs.toString(); state.attribString = attribs.toString();
}; };
const _produceLineAttributesMarker = (state) => { const _produceLineAttributesMarker = (state) => {
// TODO: This has to go to AttributeManager. // TODO: This has to go to AttributeManager.
const attribs = new AttributeMap(apool) const attribs = new AttributeMap(apool)
@ -299,11 +286,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
let unsupportedElements = null; let unsupportedElements = null;
if (!state) { if (!state) {
state = { state = {
flags: { /* name -> nesting counter*/ flags: { /* name -> nesting counter*/},
},
localAttribs: null, localAttribs: null,
attribs: { /* name -> nesting counter*/ attribs: { /* name -> nesting counter*/},
},
attribString: '', attribString: '',
// lineAttributes maintain a map from attributes to attribute values set on a line // lineAttributes maintain a map from attributes to attribute values set on a line
lineAttributes: { lineAttributes: {
@ -320,23 +305,22 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.localAttribs = null; state.localAttribs = null;
const isBlock = isBlockElement(node); const isBlock = isBlockElement(node);
if (!isBlock && node.name && (node.name !== 'body')) { if (!isBlock && node.name && (node.name !== 'body')) {
if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name); if (!supportedElems.has(node.name))
state.unsupportedElements.add(node.name);
} }
const isEmpty = _isEmpty(node, state); const isEmpty = _isEmpty(node, state);
if (isBlock) _ensureColumnZero(state); if (isBlock)
_ensureColumnZero(state);
const startLine = lines.length() - 1; const startLine = lines.length() - 1;
_reachBlockPoint(node, 0, state); _reachBlockPoint(node, 0, state);
if (node.nodeType === node.TEXT_NODE) { if (node.nodeType === node.TEXT_NODE) {
const tname = node.parentNode.getAttribute('name'); const tname = node.parentNode.getAttribute('name');
const context = { cc: this, state, tname, node, text: node.nodeValue }; const context = { cc: this, state, tname, node, text: node.nodeValue };
// Hook functions may either return a string (deprecated) or modify context.text. If any hook // Hook functions may either return a string (deprecated) or modify context.text. If any hook
// function modifies context.text then all returned strings are ignored. If no hook functions // function modifies context.text then all returned strings are ignored. If no hook functions
// modify context.text, the first hook function to return a string wins. // modify context.text, the first hook function to return a string wins.
const [hookTxt] = const [hookTxt] = hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text; let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text;
let rest = ''; let rest = '';
let x = 0; // offset into original text let x = 0; // offset into original text
if (txt.length === 0) { if (txt.length === 0) {
@ -354,7 +338,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
consumed = firstLine.length + 1; consumed = firstLine.length + 1;
rest = txt.substring(consumed); rest = txt.substring(consumed);
txt = firstLine; txt = firstLine;
} else { /* will only run this loop body once */ }
else { /* will only run this loop body once */
} }
if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) { if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) {
selStart = _pointHere(startPoint.index - x, state); selStart = _pointHere(startPoint.index - x, state);
@ -385,9 +370,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cc.startNewLine(state); cc.startNewLine(state);
} }
} }
} else if (node.nodeType === node.ELEMENT_NODE) { }
else if (node.nodeType === node.ELEMENT_NODE) {
const tname = tagName(node) || ''; const tname = tagName(node) || '';
if (tname === 'img') { if (tname === 'img') {
hooks.callAll('collectContentImage', { hooks.callAll('collectContentImage', {
cc, cc,
@ -397,11 +382,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cls: null, cls: null,
node, node,
}); });
} else { }
else {
// THIS SEEMS VERY HACKY! -- Please submit a better fix! // THIS SEEMS VERY HACKY! -- Please submit a better fix!
delete state.lineAttributes.img; delete state.lineAttributes.img;
} }
if (tname === 'br') { if (tname === 'br') {
this.breakLine = true; this.breakLine = true;
const tvalue = node.getAttribute('value'); const tvalue = node.getAttribute('value');
@ -416,24 +401,25 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (startNewLine) { if (startNewLine) {
cc.startNewLine(state); cc.startNewLine(state);
} }
} else if (tname === 'script' || tname === 'style') { }
else if (tname === 'script' || tname === 'style') {
// ignore // ignore
} else if (!isEmpty) { }
else if (!isEmpty) {
let styl = node.getAttribute('style'); let styl = node.getAttribute('style');
let cls = node.getAttribute('class'); let cls = node.getAttribute('class');
let isPre = (tname === 'pre'); let isPre = (tname === 'pre');
if ((!isPre) && abrowser && abrowser.safari) { if ((!isPre) && abrowser && abrowser.safari) {
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
} }
if (isPre) cc.incrementFlag(state, 'preMode'); if (isPre)
cc.incrementFlag(state, 'preMode');
let oldListTypeOrNull = null; let oldListTypeOrNull = null;
let oldAuthorOrNull = null; let oldAuthorOrNull = null;
// LibreOffice Writer puts in weird items during import or copy/paste, we should drop them. // LibreOffice Writer puts in weird items during import or copy/paste, we should drop them.
if (cls === 'Numbering_20_Symbols' || cls === 'Bullet_20_Symbols') { if (cls === 'Numbering_20_Symbols' || cls === 'Bullet_20_Symbols') {
styl = null; styl = null;
cls = null; cls = null;
// We have to return here but this could break things in the future, // We have to return here but this could break things in the future,
// for now it shows how to fix the problem // for now it shows how to fix the problem
return; return;
@ -473,32 +459,40 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// check if we find a better hint within the node's children // check if we find a better hint within the node's children
if (!rr && !type) { if (!rr && !type) {
for (const child of node.childNodes) { for (const child of node.childNodes) {
if (tagName(child) !== 'ul') continue; if (tagName(child) !== 'ul')
continue;
type = child.getAttribute('class'); type = child.getAttribute('class');
if (type) break; if (type)
break;
} }
} }
if (rr && rr[1]) { if (rr && rr[1]) {
type = rr[1]; type = rr[1];
} else { }
else {
if (tname === 'ul') { if (tname === 'ul') {
const cls = node.getAttribute('class'); const cls = node.getAttribute('class');
if ((type && type.match('indent')) || (cls && cls.match('indent'))) { if ((type && type.match('indent')) || (cls && cls.match('indent'))) {
type = 'indent'; type = 'indent';
} else { }
else {
type = 'bullet'; type = 'bullet';
} }
} else { }
else {
type = 'number'; type = 'number';
} }
type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
} }
oldListTypeOrNull = (_enterList(state, type) || 'none'); oldListTypeOrNull = (_enterList(state, type) || 'none');
} else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) { }
else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
// This has undesirable behavior in Chrome but is right in other browsers. // This has undesirable behavior in Chrome but is right in other browsers.
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning // See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none'); if (!abrowser.chrome)
} else if (tname === 'li') { oldListTypeOrNull = (_enterList(state, undefined) || 'none');
}
else if (tname === 'li') {
state.lineAttributes.start = state.start || 0; state.lineAttributes.start = state.start || 0;
_recalcAttribString(state); _recalcAttribString(state);
if (state.lineAttributes.list.indexOf('number') !== -1) { if (state.lineAttributes.list.indexOf('number') !== -1) {
@ -533,7 +527,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// But I don't know a way to do that because we're only aware of the previous Line // But I don't know a way to do that because we're only aware of the previous Line
// As the concept of parent's doesn't exist when processing each domline... // As the concept of parent's doesn't exist when processing each domline...
} }
} else { }
else {
// Below needs more testin if it's neccesary as _exitList should take care of this. // Below needs more testin if it's neccesary as _exitList should take care of this.
// delete state.start; // delete state.start;
// delete state.listNesting; // delete state.listNesting;
@ -553,11 +548,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
} }
} }
} }
for (const c of node.childNodes) { for (const c of node.childNodes) {
cc.collectContent(c, state); cc.collectContent(c, state);
} }
if (collectStyles) { if (collectStyles) {
hooks.callAll('collectContentPost', { hooks.callAll('collectContentPost', {
cc, cc,
@ -567,8 +560,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cls, cls,
}); });
} }
if (isPre)
if (isPre) cc.decrementFlag(state, 'preMode'); cc.decrementFlag(state, 'preMode');
if (state.localAttribs) { if (state.localAttribs) {
for (let i = 0; i < state.localAttribs.length; i++) { for (let i = 0; i < state.localAttribs.length; i++) {
cc.decrementAttrib(state, state.localAttribs[i]); cc.decrementAttrib(state, state.localAttribs[i]);
@ -587,14 +580,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (lines.length() - 1 === startLine) { if (lines.length() - 1 === startLine) {
// added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20 // added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20
// this does mean that images etc can't be pasted on lists but imho that's fine // this does mean that images etc can't be pasted on lists but imho that's fine
// If we're doing an export event we need to start a new lines // If we're doing an export event we need to start a new lines
// Export events don't have window available. // Export events don't have window available.
// commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412 // commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412
if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') { if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') {
cc.startNewLine(state); cc.startNewLine(state);
} }
} else { }
else {
_ensureColumnZero(state); _ensureColumnZero(state);
} }
} }
@ -618,23 +611,18 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// each returns [line, char] or [-1,-1] // each returns [line, char] or [-1,-1]
const getSelectionStart = () => selStart; const getSelectionStart = () => selStart;
const getSelectionEnd = () => selEnd; const getSelectionEnd = () => selEnd;
// returns array of strings for lines found, last entry will be "" if // returns array of strings for lines found, last entry will be "" if
// last line is complete (i.e. if a following span should be on a new line). // last line is complete (i.e. if a following span should be on a new line).
// can be called at any point // can be called at any point
cc.getLines = () => lines.textLines(); cc.getLines = () => lines.textLines();
cc.finish = () => { cc.finish = () => {
lines.flush(); lines.flush();
const lineAttribs = lines.attribLines(); const lineAttribs = lines.attribLines();
const lineStrings = cc.getLines(); const lineStrings = cc.getLines();
lineStrings.length--; lineStrings.length--;
lineAttribs.length--; lineAttribs.length--;
const ss = getSelectionStart(); const ss = getSelectionStart();
const se = getSelectionEnd(); const se = getSelectionEnd();
const fixLongLines = () => { const fixLongLines = () => {
// design mode does not deal with with really long lines! // design mode does not deal with with really long lines!
const lineLimit = 2000; // chars const lineLimit = 2000; // chars
@ -660,14 +648,15 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
newStrings.push(oldString); newStrings.push(oldString);
newAttribStrings.push(oldAttribString); newAttribStrings.push(oldAttribString);
} }
const fixLineNumber = (lineChar) => { const fixLineNumber = (lineChar) => {
if (lineChar[0] < 0) return; if (lineChar[0] < 0)
return;
let n = lineChar[0]; let n = lineChar[0];
let c = lineChar[1]; let c = lineChar[1];
if (n > i) { if (n > i) {
n += (newStrings.length - 1); n += (newStrings.length - 1);
} else if (n === i) { }
else if (n === i) {
let a = 0; let a = 0;
while (c > newStrings[a].length) { while (c > newStrings[a].length) {
c -= newStrings[a].length; c -= newStrings[a].length;
@ -692,7 +681,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
}; };
}; };
const wrapData = fixLongLines(); const wrapData = fixLongLines();
return { return {
selStart: ss, selStart: ss,
selEnd: se, selEnd: se,
@ -702,10 +690,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
lineAttribs, lineAttribs,
}; };
}; };
return cc; return cc;
}; };
export { sanitizeUnicode };
exports.sanitizeUnicode = sanitizeUnicode; export { makeContentCollector };
exports.makeContentCollector = makeContentCollector; export { supportedElems };
exports.supportedElems = supportedElems;

View file

@ -1,41 +1,19 @@
'use strict'; 'use strict';
export const makeCSSManager = (browserSheet) => {
/**
* 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
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
exports.makeCSSManager = (browserSheet) => {
const browserRules = () => (browserSheet.cssRules || browserSheet.rules); const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
const browserDeleteRule = (i) => { const browserDeleteRule = (i) => {
if (browserSheet.deleteRule) browserSheet.deleteRule(i); if (browserSheet.deleteRule)
else browserSheet.removeRule(i); browserSheet.deleteRule(i);
else
browserSheet.removeRule(i);
}; };
const browserInsertRule = (i, selector) => { const browserInsertRule = (i, selector) => {
if (browserSheet.insertRule) browserSheet.insertRule(`${selector} {}`, i); if (browserSheet.insertRule)
else browserSheet.addRule(selector, null, i); browserSheet.insertRule(`${selector} {}`, i);
else
browserSheet.addRule(selector, null, i);
}; };
const selectorList = []; const selectorList = [];
const indexOfSelector = (selector) => { const indexOfSelector = (selector) => {
for (let i = 0; i < selectorList.length; i++) { for (let i = 0; i < selectorList.length; i++) {
if (selectorList[i] === selector) { if (selectorList[i] === selector) {
@ -44,7 +22,6 @@ exports.makeCSSManager = (browserSheet) => {
} }
return -1; return -1;
}; };
const selectorStyle = (selector) => { const selectorStyle = (selector) => {
let i = indexOfSelector(selector); let i = indexOfSelector(selector);
if (i < 0) { if (i < 0) {
@ -55,7 +32,6 @@ exports.makeCSSManager = (browserSheet) => {
} }
return browserRules().item(i).style; return browserRules().item(i).style;
}; };
const removeSelectorStyle = (selector) => { const removeSelectorStyle = (selector) => {
const i = indexOfSelector(selector); const i = indexOfSelector(selector);
if (i >= 0) { if (i >= 0) {
@ -63,7 +39,6 @@ exports.makeCSSManager = (browserSheet) => {
selectorList.splice(i, 1); selectorList.splice(i, 1);
} }
}; };
return { return {
selectorStyle, selectorStyle,
removeSelectorStyle, removeSelectorStyle,

View file

@ -1,36 +1,11 @@
import * as Security from "./security.js";
import * as hooks from "./pluginfw/hooks.js";
import * as _ from "./underscore.js";
import { lineAttributeMarker as lineAttributeMarker$0 } from "./linestylefilter.js";
'use strict'; 'use strict';
const lineAttributeMarker = { lineAttributeMarker: lineAttributeMarker$0 }.lineAttributeMarker;
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// requires: top
// requires: plugins
// requires: undefined
const Security = require('./security');
const hooks = require('./pluginfw/hooks');
const _ = require('./underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
const noop = () => { }; const noop = () => { };
const domline = {}; const domline = {};
domline.addToLineClass = (lineClass, cls) => { domline.addToLineClass = (lineClass, cls) => {
// an "empty span" at any point can be used to add classes to // an "empty span" at any point can be used to add classes to
// the line, using line:className. otherwise, we ignore // the line, using line:className. otherwise, we ignore
@ -43,7 +18,6 @@ domline.addToLineClass = (lineClass, cls) => {
}); });
return lineClass; return lineClass;
}; };
// if "document" is falsy we don't create a DOM node, just // if "document" is falsy we don't create a DOM node, just
// an object with innerHTML and className // an object with innerHTML and className
domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
@ -56,37 +30,32 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
finishUpdate: noop, finishUpdate: noop,
lineMarker: 0, lineMarker: 0,
}; };
const document = optDocument; const document = optDocument;
if (document) { if (document) {
result.node = document.createElement('div'); result.node = document.createElement('div');
// JAWS and NVDA screen reader compatibility. Only needed if in a real browser. // JAWS and NVDA screen reader compatibility. Only needed if in a real browser.
result.node.setAttribute('aria-live', 'assertive'); result.node.setAttribute('aria-live', 'assertive');
} else { }
else {
result.node = { result.node = {
innerHTML: '', innerHTML: '',
className: '', className: '',
}; };
} }
let html = []; let html = [];
let preHtml = ''; let preHtml = '';
let postHtml = ''; let postHtml = '';
let curHTML = null; let curHTML = null;
const processSpaces = (s) => domline.processSpaces(s, doesWrap); const processSpaces = (s) => domline.processSpaces(s, doesWrap);
const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); const perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
let lineClass = 'ace-line'; let lineClass = 'ace-line';
result.appendSpan = (txt, cls) => { result.appendSpan = (txt, cls) => {
let processedMarker = false; let processedMarker = false;
// Handle lineAttributeMarker, if present // Handle lineAttributeMarker, if present
if (cls.indexOf(lineAttributeMarker) >= 0) { if (cls.indexOf(lineAttributeMarker) >= 0) {
let listType = /(?:^| )list:(\S+)/.exec(cls); let listType = /(?:^| )list:(\S+)/.exec(cls);
const start = /(?:^| )start:(\S+)/.exec(cls); const start = /(?:^| )start:(\S+)/.exec(cls);
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', { _.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
domline, domline,
cls, cls,
@ -101,7 +70,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
if (listType.indexOf('number') < 0) { if (listType.indexOf('number') < 0) {
preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`; preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
postHtml = `</li></ul>${postHtml}`; postHtml = `</li></ul>${postHtml}`;
} else { }
else {
if (start) { // is it a start of a list with more than one item in? if (start) { // is it a start of a list with more than one item in?
if (Number.parseInt(start[1]) === 1) { // if its the first one at this level? if (Number.parseInt(start[1]) === 1) { // if its the first one at this level?
// Add start class to DIV node // Add start class to DIV node
@ -109,7 +79,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
} }
preHtml += preHtml +=
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`; `<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
} else { }
else {
// Handles pasted contents into existing lists // Handles pasted contents into existing lists
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`; preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
} }
@ -141,15 +112,14 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
} }
if (cls.indexOf('tag') >= 0) { if (cls.indexOf('tag') >= 0) {
cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => { cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => {
if (!simpleTags) simpleTags = []; if (!simpleTags)
simpleTags = [];
simpleTags.push(tag.toLowerCase()); simpleTags.push(tag.toLowerCase());
return space + tag; return space + tag;
}); });
} }
let extraOpenTags = ''; let extraOpenTags = '';
let extraCloseTags = ''; let extraCloseTags = '';
_.map(hooks.callAll('aceCreateDomLine', { _.map(hooks.callAll('aceCreateDomLine', {
domline, domline,
cls, cls,
@ -158,10 +128,10 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
extraOpenTags += modifier.extraOpenTags; extraOpenTags += modifier.extraOpenTags;
extraCloseTags = modifier.extraCloseTags + extraCloseTags; extraCloseTags = modifier.extraCloseTags + extraCloseTags;
}); });
if ((!txt) && cls) { if ((!txt) && cls) {
lineClass = domline.addToLineClass(lineClass, cls); lineClass = domline.addToLineClass(lineClass, cls);
} else if (txt) { }
else if (txt) {
if (href) { if (href) {
const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
// if the url doesn't include a protocol prefix, assume http // if the url doesn't include a protocol prefix, assume http
@ -186,13 +156,7 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
simpleTags.reverse(); simpleTags.reverse();
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`; extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
} }
html.push( html.push('<span class="', Security.escapeHTMLAttribute(cls || ''), '">', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '</span>');
'<span class="', Security.escapeHTMLAttribute(cls || ''),
'">',
extraOpenTags,
perTextNodeProcess(Security.escapeHTML(txt)),
extraCloseTags,
'</span>');
} }
}; };
result.clearSpans = () => { result.clearSpans = () => {
@ -200,13 +164,13 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
lineClass = 'ace-line'; lineClass = 'ace-line';
result.lineMarker = 0; result.lineMarker = 0;
}; };
const writeHTML = () => { const writeHTML = () => {
let newHTML = perHtmlLineProcess(html.join('')); let newHTML = perHtmlLineProcess(html.join(''));
if (!newHTML) { if (!newHTML) {
if ((!document) || (!optBrowser)) { if ((!document) || (!optBrowser)) {
newHTML += '&nbsp;'; newHTML += '&nbsp;';
} else { }
else {
newHTML += '<br/>'; newHTML += '<br/>';
} }
} }
@ -218,8 +182,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
curHTML = newHTML; curHTML = newHTML;
result.node.innerHTML = curHTML; result.node.innerHTML = curHTML;
} }
if (lineClass != null) result.node.className = lineClass; if (lineClass != null)
result.node.className = lineClass;
hooks.callAll('acePostWriteDomLineHTML', { hooks.callAll('acePostWriteDomLineHTML', {
node: result.node, node: result.node,
}); });
@ -228,7 +192,6 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
result.finishUpdate = writeHTML; result.finishUpdate = writeHTML;
return result; return result;
}; };
domline.processSpaces = (s, doesWrap) => { domline.processSpaces = (s, doesWrap) => {
if (s.indexOf('<') < 0 && !doesWrap) { if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut // short-cut
@ -246,10 +209,12 @@ domline.processSpaces = (s, doesWrap) => {
for (let i = parts.length - 1; i >= 0; i--) { for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i]; const p = parts[i];
if (p === ' ') { if (p === ' ') {
if (endOfLine || beforeSpace) parts[i] = '&nbsp;'; if (endOfLine || beforeSpace)
parts[i] = '&nbsp;';
endOfLine = false; endOfLine = false;
beforeSpace = true; beforeSpace = true;
} else if (p.charAt(0) !== '<') { }
else if (p.charAt(0) !== '<') {
endOfLine = false; endOfLine = false;
beforeSpace = false; beforeSpace = false;
} }
@ -260,11 +225,13 @@ domline.processSpaces = (s, doesWrap) => {
if (p === ' ') { if (p === ' ') {
parts[i] = '&nbsp;'; parts[i] = '&nbsp;';
break; break;
} else if (p.charAt(0) !== '<') { }
else if (p.charAt(0) !== '<') {
break; break;
} }
} }
} else { }
else {
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const p = parts[i]; const p = parts[i];
if (p === ' ') { if (p === ' ') {
@ -274,5 +241,4 @@ domline.processSpaces = (s, doesWrap) => {
} }
return parts.join(''); return parts.join('');
}; };
export { domline };
exports.domline = domline;

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
/* eslint-disable-next-line max-len */ /* eslint-disable-next-line max-len */
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/** /**
@ -18,7 +17,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const randomPadName = () => { const randomPadName = () => {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when // the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below // using the PRNG below
@ -39,24 +37,21 @@ const randomPadName = () => {
} }
return randomstring; return randomstring;
}; };
$(() => { $(() => {
$('#go2Name').submit(() => { $('#go2Name').submit(() => {
const padname = $('#padname').val(); const padname = $('#padname').val();
if (padname.length > 0) { if (padname.length > 0) {
window.location = `p/${encodeURIComponent(padname.trim())}`; window.location = `p/${encodeURIComponent(padname.trim())}`;
} else { }
else {
alert('Please enter a name'); alert('Please enter a name');
} }
return false; return false;
}); });
$('#button').click(() => { $('#button').click(() => {
window.location = `p/${randomPadName()}`; window.location = `p/${randomPadName()}`;
}); });
// start the custom js // start the custom js
if (typeof window.customStart === 'function') window.customStart(); if (typeof window.customStart === 'function')
window.customStart();
}); });
// @license-end

View file

@ -1,14 +1,12 @@
'use strict'; 'use strict';
((document) => { ((document) => {
// Set language for l10n // Set language for l10n
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/); let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) language = language[1]; if (language)
language = language[1];
html10n.bind('indexed', () => { html10n.bind('indexed', () => {
html10n.localize([language, navigator.language, navigator.userLanguage, 'en']); html10n.localize([language, navigator.language, navigator.userLanguage, 'en']);
}); });
html10n.bind('localized', () => { html10n.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage(); document.documentElement.lang = html10n.getLanguage();
document.documentElement.dir = html10n.getDirection(); document.documentElement.dir = html10n.getDirection();

View file

@ -1,57 +1,23 @@
import * as Changeset from "./Changeset.js";
import * as attributes from "./attributes.js";
import * as hooks from "./pluginfw/hooks.js";
import AttributeManager from "./AttributeManager.js";
import { padutils as padutils$0 } from "./pad_utils.js";
'use strict'; 'use strict';
/**
* 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
*/
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// requires: easysync2.Changeset
// requires: top
// requires: plugins
// requires: undefined
const Changeset = require('./Changeset');
const attributes = require('./attributes');
const hooks = require('./pluginfw/hooks');
const linestylefilter = {}; const linestylefilter = {};
const AttributeManager = require('./AttributeManager'); const padutils = { padutils: padutils$0 }.padutils;
const padutils = require('./pad_utils').padutils;
linestylefilter.ATTRIB_CLASSES = { linestylefilter.ATTRIB_CLASSES = {
bold: 'tag:b', bold: 'tag:b',
italic: 'tag:i', italic: 'tag:i',
underline: 'tag:u', underline: 'tag:u',
strikethrough: 'tag:s', strikethrough: 'tag:s',
}; };
const lineAttributeMarker = 'lineAttribMarker'; const lineAttributeMarker = 'lineAttribMarker';
exports.lineAttributeMarker = lineAttributeMarker;
linestylefilter.getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { linestylefilter.getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-'; if (c === '.')
return '-';
return `z${c.charCodeAt(0)}z`; return `z${c.charCodeAt(0)}z`;
})}`; })}`;
// lineLength is without newline; aline includes newline, // lineLength is without newline; aline includes newline,
// but may be falsy if lineLength == 0 // but may be falsy if lineLength == 0
linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool) => { linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool) => {
@ -59,56 +25,55 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) { for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) {
Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses); Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses);
} }
if (lineLength === 0)
if (lineLength === 0) return textAndClassFunc; return textAndClassFunc;
const nextAfterAuthorColors = textAndClassFunc; const nextAfterAuthorColors = textAndClassFunc;
const authorColorFunc = (() => { const authorColorFunc = (() => {
const lineEnd = lineLength; const lineEnd = lineLength;
let curIndex = 0; let curIndex = 0;
let extraClasses; let extraClasses;
let leftInAuthor; let leftInAuthor;
const attribsToClasses = (attribs) => { const attribsToClasses = (attribs) => {
let classes = ''; let classes = '';
let isLineAttribMarker = false; let isLineAttribMarker = false;
for (const [key, value] of attributes.attribsFromString(attribs, apool)) { for (const [key, value] of attributes.attribsFromString(attribs, apool)) {
if (!key || !value) continue; if (!key || !value)
continue;
if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) { if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) {
isLineAttribMarker = true; isLineAttribMarker = true;
} }
if (key === 'author') { if (key === 'author') {
classes += ` ${linestylefilter.getAuthorClassName(value)}`; classes += ` ${linestylefilter.getAuthorClassName(value)}`;
} else if (key === 'list') { }
else if (key === 'list') {
classes += ` list:${value}`; classes += ` list:${value}`;
} else if (key === 'start') { }
else if (key === 'start') {
// Needed to introduce the correct Ordered list item start number on import // Needed to introduce the correct Ordered list item start number on import
classes += ` start:${value}`; classes += ` start:${value}`;
} else if (linestylefilter.ATTRIB_CLASSES[key]) { }
else if (linestylefilter.ATTRIB_CLASSES[key]) {
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
} else { }
else {
const results = hooks.callAll('aceAttribsToClasses', { linestylefilter, key, value }); const results = hooks.callAll('aceAttribsToClasses', { linestylefilter, key, value });
classes += ` ${results.join(' ')}`; classes += ` ${results.join(' ')}`;
} }
} }
if (isLineAttribMarker)
if (isLineAttribMarker) classes += ` ${lineAttributeMarker}`; classes += ` ${lineAttributeMarker}`;
return classes.substring(1); return classes.substring(1);
}; };
const attrOps = Changeset.deserializeOps(aline); const attrOps = Changeset.deserializeOps(aline);
let attrOpsNext = attrOps.next(); let attrOpsNext = attrOps.next();
let nextOp, nextOpClasses; let nextOp, nextOpClasses;
const goNextOp = () => { const goNextOp = () => {
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value; nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next(); if (!attrOpsNext.done)
attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
}; };
goNextOp(); goNextOp();
const nextClasses = () => { const nextClasses = () => {
if (curIndex < lineEnd) { if (curIndex < lineEnd) {
extraClasses = nextOpClasses; extraClasses = nextOpClasses;
@ -121,7 +86,6 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
} }
}; };
nextClasses(); nextClasses();
return (txt, cls) => { return (txt, cls) => {
const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', { const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', {
linestylefilter, linestylefilter,
@ -152,7 +116,6 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
})(); })();
return authorColorFunc; return authorColorFunc;
}; };
linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => { linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => {
const at = /@/g; const at = /@/g;
at.lastIndex = 0; at.lastIndex = 0;
@ -164,12 +127,10 @@ linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => {
} }
splitPoints.push(execResult.index); splitPoints.push(execResult.index);
} }
if (!splitPoints)
if (!splitPoints) return textAndClassFunc; return textAndClassFunc;
return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints); return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
}; };
linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc) => { linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc) => {
regExp.lastIndex = 0; regExp.lastIndex = 0;
let regExpMatchs = null; let regExpMatchs = null;
@ -185,9 +146,8 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
regExpMatchs.push([startIndex, regExpMatch]); regExpMatchs.push([startIndex, regExpMatch]);
splitPoints.push(startIndex, startIndex + regExpMatch.length); splitPoints.push(startIndex, startIndex + regExpMatch.length);
} }
if (!regExpMatchs)
if (!regExpMatchs) return textAndClassFunc; return textAndClassFunc;
const regExpMatchForIndex = (idx) => { const regExpMatchForIndex = (idx) => {
for (let k = 0; k < regExpMatchs.length; k++) { for (let k = 0; k < regExpMatchs.length; k++) {
const u = regExpMatchs[k]; const u = regExpMatchs[k];
@ -197,7 +157,6 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
} }
return false; return false;
}; };
const handleRegExpMatchsAfterSplit = (() => { const handleRegExpMatchsAfterSplit = (() => {
let curIndex = 0; let curIndex = 0;
return (txt, cls) => { return (txt, cls) => {
@ -211,29 +170,24 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
curIndex += txtlen; curIndex += txtlen;
}; };
})(); })();
return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints); return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints);
}; };
linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(padutils.urlRegex, 'url'); linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(padutils.urlRegex, 'url');
linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => { linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
let nextPointIndex = 0; let nextPointIndex = 0;
let idx = 0; let idx = 0;
// don't split at 0 // don't split at 0
while (splitPointsOpt && while (splitPointsOpt &&
nextPointIndex < splitPointsOpt.length && nextPointIndex < splitPointsOpt.length &&
splitPointsOpt[nextPointIndex] === 0) { splitPointsOpt[nextPointIndex] === 0) {
nextPointIndex++; nextPointIndex++;
} }
const spanHandler = (txt, cls) => { const spanHandler = (txt, cls) => {
if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) { if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) {
func(txt, cls); func(txt, cls);
idx += txt.length; idx += txt.length;
} else { }
else {
const splitPoints = splitPointsOpt; const splitPoints = splitPointsOpt;
const pointLocInSpan = splitPoints[nextPointIndex] - idx; const pointLocInSpan = splitPoints[nextPointIndex] - idx;
const txtlen = txt.length; const txtlen = txt.length;
@ -243,7 +197,8 @@ linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
if (pointLocInSpan === txtlen) { if (pointLocInSpan === txtlen) {
nextPointIndex++; nextPointIndex++;
} }
} else { }
else {
if (pointLocInSpan > 0) { if (pointLocInSpan > 0) {
func(txt.substring(0, pointLocInSpan), cls); func(txt.substring(0, pointLocInSpan), cls);
idx += pointLocInSpan; idx += pointLocInSpan;
@ -256,10 +211,8 @@ linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
}; };
return spanHandler; return spanHandler;
}; };
linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => { linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => {
let func = linestylefilter.getURLFilter(lineText, textAndClassFunc); let func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
const hookFilters = hooks.callAll('aceGetFilterStack', { const hookFilters = hooks.callAll('aceGetFilterStack', {
linestylefilter, linestylefilter,
browser: abrowser, browser: abrowser,
@ -267,10 +220,8 @@ linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => {
hookFilters.map((hookFilter) => { hookFilters.map((hookFilter) => {
func = hookFilter(lineText, func); func = hookFilter(lineText, func);
}); });
return func; return func;
}; };
// domLineObj is like that returned by domline.createDomLine // domLineObj is like that returned by domline.createDomLine
linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => { linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => {
// remove final newline from text if any // remove final newline from text if any
@ -278,14 +229,12 @@ linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => {
if (text.slice(-1) === '\n') { if (text.slice(-1) === '\n') {
text = text.substring(0, text.length - 1); text = text.substring(0, text.length - 1);
} }
const textAndClassFunc = (tokenText, tokenClass) => { const textAndClassFunc = (tokenText, tokenClass) => {
domLineObj.appendSpan(tokenText, tokenClass); domLineObj.appendSpan(tokenText, tokenClass);
}; };
let func = linestylefilter.getFilterStack(text, textAndClassFunc); let func = linestylefilter.getFilterStack(text, textAndClassFunc);
func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool); func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool);
func(text, ''); func(text, '');
}; };
export { lineAttributeMarker };
exports.linestylefilter = linestylefilter; export { linestylefilter };

View file

@ -1,11 +1,26 @@
import "./vendors/jquery.js";
import "./vendors/farbtastic.js";
import "./vendors/gritter.js";
import * as padUtils from "./pad_utils.js";
import { chat as chat$0 } from "./chat.js";
import { getCollabClient as getCollabClient$0 } from "./collab_client.js";
import { padconnectionstatus as padconnectionstatus$0 } from "./pad_connectionstatus.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
import { padeditbar as padeditbar$0 } from "./pad_editbar.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
import { padimpexp as padimpexp$0 } from "./pad_impexp.js";
import { padmodals as padmodals$0 } from "./pad_modals.js";
import * as padsavedrevs from "./pad_savedrevs.js";
import { paduserlist as paduserlist$0 } from "./pad_userlist.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
import * as socketio from "./socketio.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
* *
@ -21,33 +36,20 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
let socket; let socket;
const Cookies = { Cookies: padUtils }.Cookies;
// These jQuery things should create local references, but for now `require()` const chat = { chat: chat$0 }.chat;
// assigns to the global `$` and augments it with plugins. const getCollabClient = { getCollabClient: getCollabClient$0 }.getCollabClient;
require('./vendors/jquery'); const padconnectionstatus = { padconnectionstatus: padconnectionstatus$0 }.padconnectionstatus;
require('./vendors/farbtastic'); const padcookie = { padcookie: padcookie$0 }.padcookie;
require('./vendors/gritter'); const padeditbar = { padeditbar: padeditbar$0 }.padeditbar;
const padeditor = { padeditor: padeditor$0 }.padeditor;
const Cookies = require('./pad_utils').Cookies; const padimpexp = { padimpexp: padimpexp$0 }.padimpexp;
const chat = require('./chat').chat; const padmodals = { padmodals: padmodals$0 }.padmodals;
const getCollabClient = require('./collab_client').getCollabClient; const paduserlist = { paduserlist: paduserlist$0 }.paduserlist;
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; const padutils = { padutils: padUtils }.padutils;
const padcookie = require('./pad_cookie').padcookie; const colorutils = { colorutils: colorutils$0 }.colorutils;
const padeditbar = require('./pad_editbar').padeditbar; const randomString = { randomString: padUtils }.randomString;
const padeditor = require('./pad_editor').padeditor;
const padimpexp = require('./pad_impexp').padimpexp;
const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs');
const paduserlist = require('./pad_userlist').paduserlist;
const padutils = require('./pad_utils').padutils;
const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString;
const socketio = require('./socketio');
const hooks = require('./pluginfw/hooks');
// This array represents all GET-parameters which can be used to change a setting. // This array represents all GET-parameters which can be used to change a setting.
// name: the parameter-name, eg `?noColors=true` => `noColors` // name: the parameter-name, eg `?noColors=true` => `noColors`
// checkVal: the callback is only executed when // checkVal: the callback is only executed when
@ -122,7 +124,8 @@ const getParameters = [
name: 'alwaysShowChat', name: 'alwaysShowChat',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val) => {
if (!settings.hideChat) chat.stickToScreen(); if (!settings.hideChat)
chat.stickToScreen();
}, },
}, },
{ {
@ -141,18 +144,17 @@ const getParameters = [
}, },
}, },
]; ];
const getParams = () => { const getParams = () => {
// Tries server enforced options first.. // Tries server enforced options first..
for (const setting of getParameters) { for (const setting of getParameters) {
let value = clientVars.padOptions[setting.name]; let value = clientVars.padOptions[setting.name];
if (value == null) continue; if (value == null)
continue;
value = value.toString(); value = value.toString();
if (value === setting.checkVal || setting.checkVal == null) { if (value === setting.checkVal || setting.checkVal == null) {
setting.callback(value); setting.callback(value);
} }
} }
// Then URL applied stuff // Then URL applied stuff
const params = getUrlVars(); const params = getUrlVars();
for (const setting of getParameters) { for (const setting of getParameters) {
@ -162,26 +164,21 @@ const getParams = () => {
} }
} }
}; };
const getUrlVars = () => new URL(window.location.href).searchParams; const getUrlVars = () => new URL(window.location.href).searchParams;
const sendClientReady = (isReconnect) => { const sendClientReady = (isReconnect) => {
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces // unescape neccesary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId); padId = decodeURIComponent(padId);
if (!isReconnect) { if (!isReconnect) {
const titleArray = document.title.split('|'); const titleArray = document.title.split('|');
const title = titleArray[titleArray.length - 1]; const title = titleArray[titleArray.length - 1];
document.title = `${padId.replace(/_+/g, ' ')} | ${title}`; document.title = `${padId.replace(/_+/g, ' ')} | ${title}`;
} }
let token = Cookies.get('token'); let token = Cookies.get('token');
if (token == null || !padutils.isValidAuthorToken(token)) { if (token == null || !padutils.isValidAuthorToken(token)) {
token = padutils.generateAuthorToken(); token = padutils.generateAuthorToken();
Cookies.set('token', token, { expires: 60 }); Cookies.set('token', token, { expires: 60 });
} }
// If known, propagate the display name and color to the server in the CLIENT_READY message. This // If known, propagate the display name and color to the server in the CLIENT_READY message. This
// allows the server to include the values in its reply CLIENT_VARS message (which avoids // allows the server to include the values in its reply CLIENT_VARS message (which avoids
// initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the // initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the
@ -191,7 +188,6 @@ const sendClientReady = (isReconnect) => {
colorId: params.get('userColor'), colorId: params.get('userColor'),
name: params.get('userName'), name: params.get('userName'),
}; };
const msg = { const msg = {
component: 'pad', component: 'pad',
type: 'CLIENT_READY', type: 'CLIENT_READY',
@ -200,22 +196,18 @@ const sendClientReady = (isReconnect) => {
token, token,
userInfo, userInfo,
}; };
// this is a reconnect, lets tell the server our revisionnumber // this is a reconnect, lets tell the server our revisionnumber
if (isReconnect) { if (isReconnect) {
msg.client_rev = pad.collabClient.getCurrentRevisionNumber(); msg.client_rev = pad.collabClient.getCurrentRevisionNumber();
msg.reconnect = true; msg.reconnect = true;
} }
socket.json.send(msg); socket.json.send(msg);
}; };
const handshake = async () => { const handshake = async () => {
let receivedClientVars = false; let receivedClientVars = false;
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces // unescape neccesary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId); padId = decodeURIComponent(padId);
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear // padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
// to the proxy/gateway/whatever that this is a pad connection and should be treated as such // to the proxy/gateway/whatever that this is a pad connection and should be treated as such
socket = pad.socket = socketio.connect(exports.baseURL, '/', { socket = pad.socket = socketio.connect(exports.baseURL, '/', {
@ -225,11 +217,9 @@ const handshake = async () => {
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
}); });
socket.once('connect', () => { socket.once('connect', () => {
sendClientReady(false); sendClientReady(false);
}); });
socket.on('reconnect', () => { socket.on('reconnect', () => {
// pad.collabClient might be null if the hanshake failed (or it never got that far). // pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) { if (pad.collabClient != null) {
@ -237,7 +227,6 @@ const handshake = async () => {
} }
sendClientReady(receivedClientVars); sendClientReady(receivedClientVars);
}); });
const socketReconnecting = () => { const socketReconnecting = () => {
// pad.collabClient might be null if the hanshake failed (or it never got that far). // pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) { if (pad.collabClient != null) {
@ -246,26 +235,24 @@ const handshake = async () => {
pad.collabClient.setChannelState('RECONNECTING'); pad.collabClient.setChannelState('RECONNECTING');
} }
}; };
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason !== 'io server disconnect') return; if (reason !== 'io server disconnect')
return;
socketReconnecting(); socketReconnecting();
socket.connect(); socket.connect();
}); });
socket.on('reconnecting', socketReconnecting); socket.on('reconnecting', socketReconnecting);
socket.on('reconnect_failed', (error) => { socket.on('reconnect_failed', (error) => {
// pad.collabClient might be null if the hanshake failed (or it never got that far). // pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) { if (pad.collabClient != null) {
pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout'); pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout');
} else { }
else {
throw new Error('Reconnect timed out'); throw new Error('Reconnect timed out');
} }
}); });
socket.on('error', (error) => { socket.on('error', (error) => {
// pad.collabClient might be null if the error occurred before the hanshake completed. // pad.collabClient might be null if the error occurred before the hanshake completed.
if (pad.collabClient != null) { if (pad.collabClient != null) {
@ -276,47 +263,45 @@ const handshake = async () => {
// addressed by reconnection logic, so throwing an exception each time there's a socket.io error // addressed by reconnection logic, so throwing an exception each time there's a socket.io error
// just annoys users and fills logs. // just annoys users and fills logs.
}); });
socket.on('message', (obj) => { socket.on('message', (obj) => {
// the access was not granted, give the user a message // the access was not granted, give the user a message
if (obj.accessStatus) { if (obj.accessStatus) {
if (obj.accessStatus === 'deny') { if (obj.accessStatus === 'deny') {
$('#loading').hide(); $('#loading').hide();
$('#permissionDenied').show(); $('#permissionDenied').show();
if (receivedClientVars) { if (receivedClientVars) {
// got kicked // got kicked
$('#editorcontainer').hide(); $('#editorcontainer').hide();
$('#editorloadingbox').show(); $('#editorloadingbox').show();
} }
} }
} else if (!receivedClientVars && obj.type === 'CLIENT_VARS') { }
else if (!receivedClientVars && obj.type === 'CLIENT_VARS') {
receivedClientVars = true; receivedClientVars = true;
window.clientVars = obj.data; window.clientVars = obj.data;
if (window.clientVars.sessionRefreshInterval) { if (window.clientVars.sessionRefreshInterval) {
const ping = const ping = () => $.ajax('../_extendExpressSessionLifetime', { method: 'PUT' }).catch(() => { });
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval); setInterval(ping, window.clientVars.sessionRefreshInterval);
} }
} else if (obj.disconnect) { }
else if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect); padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect(); socket.disconnect();
// block user from making any change to the pad // block user from making any change to the pad
padeditor.disable(); padeditor.disable();
padeditbar.disable(); padeditbar.disable();
padimpexp.disable(); padimpexp.disable();
return; return;
} else { }
else {
pad._messageQ.enqueue(obj); pad._messageQ.enqueue(obj);
} }
}); });
await Promise.all([ await Promise.all([
new Promise((resolve) => { new Promise((resolve) => {
const h = (obj) => { const h = (obj) => {
if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return; if (obj.accessStatus || obj.type !== 'CLIENT_VARS')
return;
socket.off('message', h); socket.off('message', h);
resolve(); resolve();
}; };
@ -329,29 +314,28 @@ const handshake = async () => {
hooks.aCallAll('_socketCreated', { socket }), hooks.aCallAll('_socketCreated', { socket }),
]); ]);
}; };
/** Defers message handling until setCollabClient() is called with a non-null value. */ /** Defers message handling until setCollabClient() is called with a non-null value. */
class MessageQueue { class MessageQueue {
constructor() { constructor() {
this._q = []; this._q = [];
this._cc = null; this._cc = null;
} }
setCollabClient(cc) { setCollabClient(cc) {
this._cc = cc; this._cc = cc;
this.enqueue(); // Flush. this.enqueue(); // Flush.
} }
enqueue(...msgs) { enqueue(...msgs) {
if (this._cc == null) { if (this._cc == null) {
this._q.push(...msgs); this._q.push(...msgs);
} else { }
while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift()); else {
for (const msg of msgs) this._cc.handleMessageFromServer(msg); while (this._q.length > 0)
this._cc.handleMessageFromServer(this._q.shift());
for (const msg of msgs)
this._cc.handleMessageFromServer(msg);
} }
} }
} }
const pad = { const pad = {
// don't access these directly from outside this file, except // don't access these directly from outside this file, except
// for debugging // for debugging
@ -362,7 +346,6 @@ const pad = {
clientTimeOffset: null, clientTimeOffset: null,
padOptions: {}, padOptions: {},
_messageQ: new MessageQueue(), _messageQ: new MessageQueue(),
// these don't require init; clientVars should all go through here // these don't require init; clientVars should all go through here
getPadId: () => clientVars.padId, getPadId: () => clientVars.padId,
getClientIp: () => clientVars.clientIp, getClientIp: () => clientVars.clientIp,
@ -374,15 +357,14 @@ const pad = {
sendClientMessage: (msg) => { sendClientMessage: (msg) => {
pad.collabClient.sendClientMessage(msg); pad.collabClient.sendClientMessage(msg);
}, },
init() { init() {
padutils.setupGlobalExceptionHandler(); padutils.setupGlobalExceptionHandler();
// $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is // $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is
// an async function for some bizarre reason, so the async function is wrapped in a non-async // an async function for some bizarre reason, so the async function is wrapped in a non-async
// function. // function.
$(() => (async () => { $(() => (async () => {
if (window.customStart != null) window.customStart(); if (window.customStart != null)
window.customStart();
$('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220 }); $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220 });
$('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); }); $('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); });
padcookie.init(); padcookie.init();
@ -395,18 +377,15 @@ const pad = {
// initialize the chat // initialize the chat
chat.init(this); chat.init(this);
getParams(); getParams();
padcookie.init(); // initialize the cookies padcookie.init(); // initialize the cookies
pad.initTime = +(new Date()); pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions; pad.padOptions = clientVars.initialOptions;
pad.myUserInfo = { pad.myUserInfo = {
userId: clientVars.userId, userId: clientVars.userId,
name: clientVars.userName, name: clientVars.userName,
ip: pad.getClientIp(), ip: pad.getClientIp(),
colorId: clientVars.userColor, colorId: clientVars.userColor,
}; };
const postAceInit = () => { const postAceInit = () => {
padeditbar.init(); padeditbar.init();
setTimeout(() => { setTimeout(() => {
@ -433,7 +412,6 @@ const pad = {
} }
pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily')); pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily'));
$('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update'); $('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update');
// Prevent sticky chat or chat and users to be checked for mobiles // Prevent sticky chat or chat and users to be checked for mobiles
const checkChatAndUsersVisibility = (x) => { const checkChatAndUsersVisibility = (x) => {
if (x.matches) { // If media query matches if (x.matches) { // If media query matches
@ -444,12 +422,9 @@ const pad = {
const mobileMatch = window.matchMedia('(max-width: 800px)'); const mobileMatch = window.matchMedia('(max-width: 800px)');
mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized
setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load
$('#editorcontainer').addClass('initialized'); $('#editorcontainer').addClass('initialized');
hooks.aCallAll('postAceInit', { ace: padeditor.ace, clientVars, pad }); hooks.aCallAll('postAceInit', { ace: padeditor.ace, clientVars, pad });
}; };
// order of inits is important here: // order of inits is important here:
padimpexp.init(this); padimpexp.init(this);
padsavedrevs.init(this); padsavedrevs.init(this);
@ -457,10 +432,7 @@ const pad = {
paduserlist.init(pad.myUserInfo, this); paduserlist.init(pad.myUserInfo, this);
padconnectionstatus.init(); padconnectionstatus.init();
padmodals.init(this); padmodals.init(this);
pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, { colorPalette: pad.getColorPalette() }, pad);
pad.collabClient = getCollabClient(
padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo,
{colorPalette: pad.getColorPalette()}, pad);
this._messageQ.setCollabClient(this.collabClient); this._messageQ.setCollabClient(this.collabClient);
pad.collabClient.setOnUserJoin(pad.handleUserJoin); pad.collabClient.setOnUserJoin(pad.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
@ -468,17 +440,16 @@ const pad = {
pad.collabClient.setOnClientMessage(pad.handleClientMessage); pad.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange); pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction); pad.collabClient.setOnInternalAction(pad.handleCollabAction);
// load initial chat-messages // load initial chat-messages
if (clientVars.chatHead !== -1) { if (clientVars.chatHead !== -1) {
const chatHead = clientVars.chatHead; const chatHead = clientVars.chatHead;
const start = Math.max(chatHead - 100, 0); const start = Math.max(chatHead - 100, 0);
pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end: chatHead }); pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end: chatHead });
} else { }
else {
// there are no messages // there are no messages
$('#chatloadmessagesbutton').css('display', 'none'); $('#chatloadmessagesbutton').css('display', 'none');
} }
if (window.clientVars.readonly) { if (window.clientVars.readonly) {
chat.hide(); chat.hide();
$('#myusernameedit').attr('disabled', true); $('#myusernameedit').attr('disabled', true);
@ -486,29 +457,26 @@ const pad = {
$('#chaticon').hide(); $('#chaticon').hide();
$('#options-chatandusers').parent().hide(); $('#options-chatandusers').parent().hide();
$('#options-stickychat').parent().hide(); $('#options-stickychat').parent().hide();
} else if (!settings.hideChat) { $('#chaticon').show(); } }
else if (!settings.hideChat) {
$('#chaticon').show();
}
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite'); $('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');
padeditor.ace.callWithAce((ace) => { padeditor.ace.callWithAce((ace) => {
ace.ace_setEditable(!window.clientVars.readonly); ace.ace_setEditable(!window.clientVars.readonly);
}); });
// If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers // If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers
if (settings.LineNumbersDisabled === true) { if (settings.LineNumbersDisabled === true) {
this.changeViewOption('showLineNumbers', false); this.changeViewOption('showLineNumbers', false);
} }
// If the noColors value is set to true then we need to // If the noColors value is set to true then we need to
// hide the background colors on the ace spans // hide the background colors on the ace spans
if (settings.noColors === true) { if (settings.noColors === true) {
this.changeViewOption('noColors', true); this.changeViewOption('noColors', true);
} }
if (settings.rtlIsTrue === true) { if (settings.rtlIsTrue === true) {
this.changeViewOption('rtlIsTrue', true); this.changeViewOption('rtlIsTrue', true);
} }
// If the Monospacefont value is set to true then change it to monospace. // If the Monospacefont value is set to true then change it to monospace.
if (settings.useMonospaceFontGlobal === true) { if (settings.useMonospaceFontGlobal === true) {
this.changeViewOption('padFontFamily', 'RobotoMono'); this.changeViewOption('padFontFamily', 'RobotoMono');
@ -528,7 +496,6 @@ const pad = {
paduserlist.setMyUserInfo(this.myUserInfo); paduserlist.setMyUserInfo(this.myUserInfo);
} }
}, },
dispose: () => { dispose: () => {
padeditor.dispose(); padeditor.dispose();
}, },
@ -544,8 +511,7 @@ const pad = {
const options = {}; const options = {};
options[key] = value; options[key] = value;
pad.handleOptionsChange(options); pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage( pad.collabClient.sendClientMessage({
{
type: 'padoptions', type: 'padoptions',
options, options,
changedBy: pad.myUserInfo.name || 'unnamed', changedBy: pad.myUserInfo.name || 'unnamed',
@ -575,8 +541,7 @@ const pad = {
// caller shouldn't mutate the object // caller shouldn't mutate the object
getPadOptions: () => pad.padOptions, getPadOptions: () => pad.padOptions,
suggestUserName: (userId, name) => { suggestUserName: (userId, name) => {
pad.collabClient.sendClientMessage( pad.collabClient.sendClientMessage({
{
type: 'suggestUserName', type: 'suggestUserName',
unnamedId: userId, unnamedId: userId,
newName: name, newName: name,
@ -597,11 +562,14 @@ const pad = {
pad.notifyChangeName(msg.newName); pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo); paduserlist.setMyUserInfo(pad.myUserInfo);
} }
} else if (msg.type === 'newRevisionList') { }
else if (msg.type === 'newRevisionList') {
padsavedrevs.newRevisionList(msg.revisionList); padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'revisionLabel') { }
else if (msg.type === 'revisionLabel') {
padsavedrevs.newRevisionList(msg.revisionList); padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'padoptions') { }
else if (msg.type === 'padoptions') {
const opts = msg.options; const opts = msg.options;
pad.handleOptionsChange(opts); pad.handleOptionsChange(opts);
} }
@ -614,26 +582,25 @@ const pad = {
padeditbar.enable(); padeditbar.enable();
padimpexp.enable(); padimpexp.enable();
padconnectionstatus.connected(); padconnectionstatus.connected();
} else if (newState === 'RECONNECTING') { }
else if (newState === 'RECONNECTING') {
padeditor.disable(); padeditor.disable();
padeditbar.disable(); padeditbar.disable();
padimpexp.disable(); padimpexp.disable();
padconnectionstatus.reconnecting(); padconnectionstatus.reconnecting();
} else if (newState === 'DISCONNECTED') { }
else if (newState === 'DISCONNECTED') {
pad.diagnosticInfo.disconnectedMessage = message; pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padId = pad.getPadId(); pad.diagnosticInfo.padId = pad.getPadId();
pad.diagnosticInfo.socket = {}; pad.diagnosticInfo.socket = {};
// we filter non objects from the socket object and put them in the diagnosticInfo // we filter non objects from the socket object and put them in the diagnosticInfo
// this ensures we have no cyclic data - this allows us to stringify the data // this ensures we have no cyclic data - this allows us to stringify the data
for (const [i, value] of Object.entries(socket.socket || {})) { for (const [i, value] of Object.entries(socket.socket || {})) {
const type = typeof value; const type = typeof value;
if (type === 'string' || type === 'number') { if (type === 'string' || type === 'number') {
pad.diagnosticInfo.socket[i] = value; pad.diagnosticInfo.socket[i] = value;
} }
} }
pad.asyncSendDiagnosticInfo(); pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog === 'string') { if (typeof window.ajlog === 'string') {
window.ajlog += (`Disconnected: ${message}\n`); window.ajlog += (`Disconnected: ${message}\n`);
@ -641,7 +608,6 @@ const pad = {
padeditor.disable(); padeditor.disable();
padeditbar.disable(); padeditbar.disable();
padimpexp.disable(); padimpexp.disable();
padconnectionstatus.disconnected(message); padconnectionstatus.disconnected(message);
} }
const newFullyConnected = !!padconnectionstatus.isFullyConnected(); const newFullyConnected = !!padconnectionstatus.isFullyConnected();
@ -662,7 +628,8 @@ const pad = {
if (chatVisCookie) { // if the cookie is set for chat always visible if (chatVisCookie) { // if the cookie is set for chat always visible
chat.stickToScreen(true); // stick it to the screen chat.stickToScreen(true); // stick it to the screen
$('#options-stickychat').prop('checked', true); // set the checkbox to on $('#options-stickychat').prop('checked', true); // set the checkbox to on
} else { }
else {
$('#options-stickychat').prop('checked', false); // set the checkbox for off $('#options-stickychat').prop('checked', false); // set the checkbox for off
} }
}, },
@ -671,7 +638,8 @@ const pad = {
if (chatAUVisCookie) { // if the cookie is set for chat always visible if (chatAUVisCookie) { // if the cookie is set for chat always visible
chat.chatAndUsers(true); // stick it to the screen chat.chatAndUsers(true); // stick it to the screen
$('#options-chatandusers').prop('checked', true); // set the checkbox to on $('#options-chatandusers').prop('checked', true); // set the checkbox to on
} else { }
else {
$('#options-chatandusers').prop('checked', false); // set the checkbox for off $('#options-chatandusers').prop('checked', false); // set the checkbox for off
} }
}, },
@ -680,21 +648,22 @@ const pad = {
if (authColCookie) { if (authColCookie) {
pad.changeViewOption('showAuthorColors', true); pad.changeViewOption('showAuthorColors', true);
$('#options-colorscheck').prop('checked', true); $('#options-colorscheck').prop('checked', true);
} else { }
else {
$('#options-colorscheck').prop('checked', false); $('#options-colorscheck').prop('checked', false);
} }
}, },
handleCollabAction: (action) => { handleCollabAction: (action) => {
if (action === 'commitPerformed') { if (action === 'commitPerformed') {
padeditbar.setSyncStatus('syncing'); padeditbar.setSyncStatus('syncing');
} else if (action === 'newlyIdle') { }
else if (action === 'newlyIdle') {
padeditbar.setSyncStatus('done'); padeditbar.setSyncStatus('done');
} }
}, },
asyncSendDiagnosticInfo: () => { asyncSendDiagnosticInfo: () => {
window.setTimeout(() => { window.setTimeout(() => {
$.ajax( $.ajax({
{
type: 'post', type: 'post',
url: 'ep/pad/connection-diagnostic-info', url: 'ep/pad/connection-diagnostic-info',
data: { data: {
@ -723,14 +692,13 @@ const pad = {
window.setTimeout(() => { window.setTimeout(() => {
pad.addHistoricalAuthors(data); pad.addHistoricalAuthors(data);
}, 1000); }, 1000);
} else { }
else {
pad.collabClient.addHistoricalAuthors(data); pad.collabClient.addHistoricalAuthors(data);
} }
}, },
}; };
const init = () => pad.init(); const init = () => pad.init();
const settings = { const settings = {
LineNumbersDisabled: false, LineNumbersDisabled: false,
noColors: false, noColors: false,
@ -739,12 +707,10 @@ const settings = {
globalUserColor: false, globalUserColor: false,
rtlIsTrue: false, rtlIsTrue: false,
}; };
pad.settings = settings; pad.settings = settings;
export const baseURL = '';
exports.baseURL = ''; export { settings };
exports.settings = settings; export { randomString };
exports.randomString = randomString; export { getParams };
exports.getParams = getParams; export { pad };
exports.pad = pad; export { init };
exports.init = init;

View file

@ -1,61 +1,34 @@
'use strict'; 'use strict';
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
createCountDownElementsIfNecessary($modal);
const timer = createTimerForModal($modal, pad);
$modal.find('#cancelreconnect').one('click', () => {
timer.cancel();
disableAutomaticReconnection($modal);
});
enableAutomaticReconnection($modal);
}
};
const createCountDownElementsIfNecessary = ($modal) => { const createCountDownElementsIfNecessary = ($modal) => {
const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0; const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0;
if (elementsDoNotExist) { if (elementsDoNotExist) {
const $defaultMessage = $modal.find('#defaulttext'); const $defaultMessage = $modal.find('#defaulttext');
const $reconnectButton = $modal.find('#forcereconnect'); const $reconnectButton = $modal.find('#forcereconnect');
// create extra DOM elements, if they don't exist // create extra DOM elements, if they don't exist
const $reconnectTimerMessage = const $reconnectTimerMessage = $('<p>')
$('<p>')
.addClass('reconnecttimer') .addClass('reconnecttimer')
.append( .append($('<span>')
$('<span>')
.attr('data-l10n-id', 'pad.modals.reconnecttimer') .attr('data-l10n-id', 'pad.modals.reconnecttimer')
.text('Trying to reconnect in')) .text('Trying to reconnect in'))
.append(' ') .append(' ')
.append( .append($('<span>')
$('<span>')
.addClass('timetoexpire')); .addClass('timetoexpire'));
const $cancelReconnect = const $cancelReconnect = $('<button>')
$('<button>')
.attr('id', 'cancelreconnect') .attr('id', 'cancelreconnect')
.attr('data-l10n-id', 'pad.modals.cancel') .attr('data-l10n-id', 'pad.modals.cancel')
.text('Cancel'); .text('Cancel');
localize($reconnectTimerMessage); localize($reconnectTimerMessage);
localize($cancelReconnect); localize($cancelReconnect);
$reconnectTimerMessage.insertAfter($defaultMessage); $reconnectTimerMessage.insertAfter($defaultMessage);
$cancelReconnect.insertAfter($reconnectButton); $cancelReconnect.insertAfter($reconnectButton);
} }
}; };
const localize = ($element) => { const localize = ($element) => {
html10n.translateElement(html10n.translations, $element.get(0)); html10n.translateElement(html10n.translations, $element.get(0));
}; };
const createTimerForModal = ($modal, pad) => { const createTimerForModal = ($modal, pad) => {
const timeUntilReconnection = const timeUntilReconnection = clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
const timer = new CountDownTimer(timeUntilReconnection); const timer = new CountDownTimer(timeUntilReconnection);
timer.onTick((minutes, seconds) => { timer.onTick((minutes, seconds) => {
updateCountDownTimerMessage($modal, minutes, seconds); updateCountDownTimerMessage($modal, minutes, seconds);
}).onExpire(() => { }).onExpire(() => {
@ -63,14 +36,13 @@ const createTimerForModal = ($modal, pad) => {
if (wasANetworkError) { if (wasANetworkError) {
// cannot simply reconnect, client is having issues to establish connection to server // cannot simply reconnect, client is having issues to establish connection to server
waitUntilClientCanConnectToServerAndThen(() => { forceReconnection($modal); }, pad); waitUntilClientCanConnectToServerAndThen(() => { forceReconnection($modal); }, pad);
} else { }
else {
forceReconnection($modal); forceReconnection($modal);
} }
}).start(); }).start();
return timer; return timer;
}; };
const disableAutomaticReconnection = ($modal) => { const disableAutomaticReconnection = ($modal) => {
toggleAutomaticReconnectionOption($modal, true); toggleAutomaticReconnectionOption($modal, true);
}; };
@ -81,12 +53,10 @@ const toggleAutomaticReconnectionOption = ($modal, disableAutomaticReconnect) =>
$modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect); $modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect);
$modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect); $modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect);
}; };
const waitUntilClientCanConnectToServerAndThen = (callback, pad) => { const waitUntilClientCanConnectToServerAndThen = (callback, pad) => {
whenConnectionIsRestablishedWithServer(callback, pad); whenConnectionIsRestablishedWithServer(callback, pad);
pad.socket.connect(); pad.socket.connect();
}; };
const whenConnectionIsRestablishedWithServer = (callback, pad) => { const whenConnectionIsRestablishedWithServer = (callback, pad) => {
// only add listener for the first try, don't need to add another listener // only add listener for the first try, don't need to add another listener
// on every unsuccessful try // on every unsuccessful try
@ -94,32 +64,25 @@ const whenConnectionIsRestablishedWithServer = (callback, pad) => {
pad.socket.once('connect', callback); pad.socket.once('connect', callback);
} }
}; };
const forceReconnection = ($modal) => { const forceReconnection = ($modal) => {
$modal.find('#forcereconnect').click(); $modal.find('#forcereconnect').click();
}; };
const updateCountDownTimerMessage = ($modal, minutes, seconds) => { const updateCountDownTimerMessage = ($modal, minutes, seconds) => {
minutes = minutes < 10 ? `0${minutes}` : minutes; minutes = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds; seconds = seconds < 10 ? `0${seconds}` : seconds;
$modal.find('.timetoexpire').text(`${minutes}:${seconds}`); $modal.find('.timetoexpire').text(`${minutes}:${seconds}`);
}; };
// store number of tries to reconnect to server, in order to increase time to wait // store number of tries to reconnect to server, in order to increase time to wait
// until next try // until next try
const reconnectionTries = { const reconnectionTries = {
counter: 0, counter: 0,
nextTry() { nextTry() {
// double the time to try to reconnect on every time reconnection fails // double the time to try to reconnect on every time reconnection fails
const nextCounterFactor = 2 ** this.counter; const nextCounterFactor = 2 ** this.counter;
this.counter++; this.counter++;
return nextCounterFactor; return nextCounterFactor;
}, },
}; };
// Timer based on http://stackoverflow.com/a/20618517. // Timer based on http://stackoverflow.com/a/20618517.
// duration: how many **seconds** until the timer ends // duration: how many **seconds** until the timer ends
// granularity (optional): how many **milliseconds** // granularity (optional): how many **milliseconds**
@ -128,11 +91,9 @@ const CountDownTimer = function (duration, granularity) {
this.duration = duration; this.duration = duration;
this.granularity = granularity || 1000; this.granularity = granularity || 1000;
this.running = false; this.running = false;
this.onTickCallbacks = []; this.onTickCallbacks = [];
this.onExpireCallbacks = []; this.onExpireCallbacks = [];
}; };
CountDownTimer.prototype.start = function () { CountDownTimer.prototype.start = function () {
if (this.running) { if (this.running) {
return; return;
@ -143,11 +104,11 @@ CountDownTimer.prototype.start = function () {
let diff; let diff;
const timer = () => { const timer = () => {
diff = that.duration - Math.floor((Date.now() - start) / 1000); diff = that.duration - Math.floor((Date.now() - start) / 1000);
if (diff > 0) { if (diff > 0) {
that.timeoutId = setTimeout(timer, that.granularity); that.timeoutId = setTimeout(timer, that.granularity);
that.tick(diff); that.tick(diff);
} else { }
else {
that.running = false; that.running = false;
that.tick(0); that.tick(0);
that.expire(); that.expire();
@ -155,7 +116,6 @@ CountDownTimer.prototype.start = function () {
}; };
timer(); timer();
}; };
CountDownTimer.prototype.tick = function (diff) { CountDownTimer.prototype.tick = function (diff) {
const obj = CountDownTimer.parse(diff); const obj = CountDownTimer.parse(diff);
this.onTickCallbacks.forEach(function (callback) { this.onTickCallbacks.forEach(function (callback) {
@ -167,28 +127,35 @@ CountDownTimer.prototype.expire = function () {
callback.call(this); callback.call(this);
}, this); }, this);
}; };
CountDownTimer.prototype.onTick = function (callback) { CountDownTimer.prototype.onTick = function (callback) {
if (typeof callback === 'function') { if (typeof callback === 'function') {
this.onTickCallbacks.push(callback); this.onTickCallbacks.push(callback);
} }
return this; return this;
}; };
CountDownTimer.prototype.onExpire = function (callback) { CountDownTimer.prototype.onExpire = function (callback) {
if (typeof callback === 'function') { if (typeof callback === 'function') {
this.onExpireCallbacks.push(callback); this.onExpireCallbacks.push(callback);
} }
return this; return this;
}; };
CountDownTimer.prototype.cancel = function () { CountDownTimer.prototype.cancel = function () {
this.running = false; this.running = false;
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId);
return this; return this;
}; };
CountDownTimer.parse = (seconds) => ({ CountDownTimer.parse = (seconds) => ({
minutes: (seconds / 60) | 0, minutes: (seconds / 60) | 0,
seconds: (seconds % 60) | 0, seconds: (seconds % 60) | 0,
}); });
export const showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
createCountDownElementsIfNecessary($modal);
const timer = createTimerForModal($modal, pad);
$modal.find('#cancelreconnect').one('click', () => {
timer.cancel();
disableAutomaticReconnection($modal);
});
enableAutomaticReconnection($modal);
}
};

View file

@ -1,11 +1,10 @@
import { padmodals as padmodals$0 } from "./pad_modals.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,14 +20,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const padmodals = { padmodals: padmodals$0 }.padmodals;
const padmodals = require('./pad_modals').padmodals;
const padconnectionstatus = (() => { const padconnectionstatus = (() => {
let status = { let status = {
what: 'connecting', what: 'connecting',
}; };
const self = { const self = {
init: () => { init: () => {
$('button#forcereconnect').click(() => { $('button#forcereconnect').click(() => {
@ -46,18 +42,16 @@ const padconnectionstatus = (() => {
status = { status = {
what: 'reconnecting', what: 'reconnecting',
}; };
padmodals.showModal('reconnecting'); padmodals.showModal('reconnecting');
padmodals.showOverlay(); padmodals.showOverlay();
}, },
disconnected: (msg) => { disconnected: (msg) => {
if (status.what === 'disconnected') return; if (status.what === 'disconnected')
return;
status = { status = {
what: 'disconnected', what: 'disconnected',
why: msg, why: msg,
}; };
// These message IDs correspond to localized strings that are presented to the user. If a new // These message IDs correspond to localized strings that are presented to the user. If a new
// message ID is added here then a new div must be added to src/templates/pad.html and the // message ID is added here then a new div must be added to src/templates/pad.html and the
// corresponding l10n IDs must be added to the language files in src/locales. // corresponding l10n IDs must be added to the language files in src/locales.
@ -79,7 +73,6 @@ const padconnectionstatus = (() => {
// Fall back to a generic message. // Fall back to a generic message.
k = 'disconnected'; k = 'disconnected';
} }
padmodals.showModal(k); padmodals.showModal(k);
padmodals.showOverlay(); padmodals.showOverlay();
}, },
@ -88,5 +81,4 @@ const padconnectionstatus = (() => {
}; };
return self; return self;
})(); })();
export { padconnectionstatus };
exports.padconnectionstatus = padconnectionstatus;

View file

@ -1,5 +1,5 @@
import * as padUtils from "./pad_utils.js";
'use strict'; 'use strict';
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -15,14 +15,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const Cookies = { Cookies: padUtils }.Cookies;
const Cookies = require('./pad_utils').Cookies; export const padcookie = new class {
exports.padcookie = new class {
constructor() { constructor() {
this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp'; this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
} }
init() { init() {
const prefs = this.readPrefs_() || {}; const prefs = this.readPrefs_() || {};
delete prefs.userId; delete prefs.userId;
@ -39,31 +36,28 @@ exports.padcookie = new class {
}); });
} }
} }
readPrefs_() { readPrefs_() {
try { try {
const json = Cookies.get(this.cookieName_); const json = Cookies.get(this.cookieName_);
if (json == null) return null; if (json == null)
return null;
return JSON.parse(json); return JSON.parse(json);
} catch (e) { }
catch (e) {
return null; return null;
} }
} }
writePrefs_(prefs) { writePrefs_(prefs) {
Cookies.set(this.cookieName_, JSON.stringify(prefs), { expires: 365 * 100 }); Cookies.set(this.cookieName_, JSON.stringify(prefs), { expires: 365 * 100 });
} }
getPref(prefName) { getPref(prefName) {
return this.readPrefs_()[prefName]; return this.readPrefs_()[prefName];
} }
setPref(prefName, value) { setPref(prefName, value) {
const prefs = this.readPrefs_(); const prefs = this.readPrefs_();
prefs[prefName] = value; prefs[prefName] = value;
this.writePrefs_(prefs); this.writePrefs_(prefs);
} }
clear() { clear() {
this.writePrefs_({}); this.writePrefs_({});
} }

View file

@ -1,68 +1,39 @@
import browser from "./vendors/browser.js";
import * as hooks from "./pluginfw/hooks.js";
import { padutils as padutils$0 } from "./pad_utils.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
import * as padsavedrevs from "./pad_savedrevs.js";
import _ from "underscore";
import "./vendors/nice-select.js";
'use strict'; 'use strict';
const padutils = { padutils: padutils$0 }.padutils;
/** const padeditor = { padeditor: padeditor$0 }.padeditor;
* 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
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
const padeditor = require('./pad_editor').padeditor;
const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore');
require('./vendors/nice-select');
class ToolbarItem { class ToolbarItem {
constructor(element) { constructor(element) {
this.$el = element; this.$el = element;
} }
getCommand() { getCommand() {
return this.$el.attr('data-key'); return this.$el.attr('data-key');
} }
getValue() { getValue() {
if (this.isSelect()) { if (this.isSelect()) {
return this.$el.find('select').val(); return this.$el.find('select').val();
} }
} }
setValue(val) { setValue(val) {
if (this.isSelect()) { if (this.isSelect()) {
return this.$el.find('select').val(val); return this.$el.find('select').val(val);
} }
} }
getType() { getType() {
return this.$el.attr('data-type'); return this.$el.attr('data-type');
} }
isSelect() { isSelect() {
return this.getType() === 'select'; return this.getType() === 'select';
} }
isButton() { isButton() {
return this.getType() === 'button'; return this.getType() === 'button';
} }
bind(callback) { bind(callback) {
if (this.isButton()) { if (this.isButton()) {
this.$el.click((event) => { this.$el.click((event) => {
@ -70,14 +41,14 @@ class ToolbarItem {
callback(this.getCommand(), this); callback(this.getCommand(), this);
event.preventDefault(); event.preventDefault();
}); });
} else if (this.isSelect()) { }
else if (this.isSelect()) {
this.$el.find('select').change(() => { this.$el.find('select').change(() => {
callback(this.getCommand(), this); callback(this.getCommand(), this);
}); });
} }
} }
} }
const syncAnimation = (() => { const syncAnimation = (() => {
const SYNCING = -100; const SYNCING = -100;
const DONE = 100; const DONE = 100;
@ -90,19 +61,22 @@ const syncAnimation = (() => {
const animator = padutils.makeAnimationScheduler(() => { const animator = padutils.makeAnimationScheduler(() => {
if (state === SYNCING || state === DONE) { if (state === SYNCING || state === DONE) {
return false; return false;
} else if (state >= T_GONE) { }
else if (state >= T_GONE) {
state = DONE; state = DONE;
$('#syncstatussyncing').css('display', 'none'); $('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'none'); $('#syncstatusdone').css('display', 'none');
return false; return false;
} else if (state < 0) { }
else if (state < 0) {
state += step; state += step;
if (state >= 0) { if (state >= 0) {
$('#syncstatussyncing').css('display', 'none'); $('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'block').css('opacity', 1); $('#syncstatusdone').css('display', 'block').css('opacity', 1);
} }
return true; return true;
} else { }
else {
state += step; state += step;
if (state >= T_FADE) { if (state >= T_FADE) {
$('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE)); $('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
@ -122,14 +96,12 @@ const syncAnimation = (() => {
}, },
}; };
})(); })();
export const padeditbar = new class {
exports.padeditbar = new class {
constructor() { constructor() {
this._editbarPosition = 0; this._editbarPosition = 0;
this.commands = {}; this.commands = {};
this.dropdowns = []; this.dropdowns = [];
} }
init() { init() {
$('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE $('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE
this.enable(); this.enable();
@ -139,24 +111,19 @@ exports.padeditbar = new class {
this.triggerCommand(command, item); this.triggerCommand(command, item);
}); });
}); });
$('body:not(#editorcontainerbox)').on('keydown', (evt) => { $('body:not(#editorcontainerbox)').on('keydown', (evt) => {
this._bodyKeyEvent(evt); this._bodyKeyEvent(evt);
}); });
$('.show-more-icon-btn').click(() => { $('.show-more-icon-btn').click(() => {
$('.toolbar').toggleClass('full-icons'); $('.toolbar').toggleClass('full-icons');
}); });
this.checkAllIconsAreDisplayedInToolbar(); this.checkAllIconsAreDisplayedInToolbar();
$(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100)); $(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100));
this._registerDefaultCommands(); this._registerDefaultCommands();
hooks.callAll('postToolbarInit', { hooks.callAll('postToolbarInit', {
toolbar: this, toolbar: this,
ace: padeditor.ace, ace: padeditor.ace,
}); });
/* /*
* On safari, the dropdown in the toolbar gets hidden because of toolbar * On safari, the dropdown in the toolbar gets hidden because of toolbar
* overflow:hidden property. This is a bug from Safari: any children with * overflow:hidden property. This is a bug from Safari: any children with
@ -166,7 +133,6 @@ exports.padeditbar = new class {
if (!browser.safari) { if (!browser.safari) {
$('select').niceSelect(); $('select').niceSelect();
} }
// When editor is scrolled, we add a class to style the editbar differently // When editor is scrolled, we add a class to style the editbar differently
$('iframe[name="ace_outer"]').contents().scroll((ev) => { $('iframe[name="ace_outer"]').contents().scroll((ev) => {
$('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2); $('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2);
@ -201,9 +167,9 @@ exports.padeditbar = new class {
if (this.isEnabled() && this.commands[cmd]) { if (this.isEnabled() && this.commands[cmd]) {
this.commands[cmd](cmd, padeditor.ace, item); this.commands[cmd](cmd, padeditor.ace, item);
} }
if (padeditor.ace) padeditor.ace.focus(); if (padeditor.ace)
padeditor.ace.focus();
} }
// cb is deprecated (this function is synchronous so a callback is unnecessary). // cb is deprecated (this function is synchronous so a callback is unnecessary).
toggleDropDown(moduleName, cb = null) { toggleDropDown(moduleName, cb = null) {
let cbErr = null; let cbErr = null;
@ -212,51 +178,54 @@ exports.padeditbar = new class {
if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) { if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) {
return; return;
} }
$('.nice-select').removeClass('open'); $('.nice-select').removeClass('open');
$('.toolbar-popup').removeClass('popup-show'); $('.toolbar-popup').removeClass('popup-show');
// hide all modules and remove highlighting of all buttons // hide all modules and remove highlighting of all buttons
if (moduleName === 'none') { if (moduleName === 'none') {
for (const thisModuleName of this.dropdowns) { for (const thisModuleName of this.dropdowns) {
// skip the userlist // skip the userlist
if (thisModuleName === 'users') continue; if (thisModuleName === 'users')
continue;
const module = $(`#${thisModuleName}`); const module = $(`#${thisModuleName}`);
// skip any "force reconnect" message // skip any "force reconnect" message
const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0; const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0;
if (isAForceReconnectMessage) continue; if (isAForceReconnectMessage)
continue;
if (module.hasClass('popup-show')) { if (module.hasClass('popup-show')) {
$(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); $(`li[data-key=${thisModuleName}] > a`).removeClass('selected');
module.removeClass('popup-show'); module.removeClass('popup-show');
} }
} }
} else { }
else {
// hide all modules that are not selected and remove highlighting // hide all modules that are not selected and remove highlighting
// respectively add highlighting to the corresponding button // respectively add highlighting to the corresponding button
for (const thisModuleName of this.dropdowns) { for (const thisModuleName of this.dropdowns) {
const module = $(`#${thisModuleName}`); const module = $(`#${thisModuleName}`);
if (module.hasClass('popup-show')) { if (module.hasClass('popup-show')) {
$(`li[data-key=${thisModuleName}] > a`).removeClass('selected'); $(`li[data-key=${thisModuleName}] > a`).removeClass('selected');
module.removeClass('popup-show'); module.removeClass('popup-show');
} else if (thisModuleName === moduleName) { }
else if (thisModuleName === moduleName) {
$(`li[data-key=${thisModuleName}] > a`).addClass('selected'); $(`li[data-key=${thisModuleName}] > a`).addClass('selected');
module.addClass('popup-show'); module.addClass('popup-show');
} }
} }
} }
} catch (err) { }
catch (err) {
cbErr = err || new Error(err); cbErr = err || new Error(err);
} finally { }
if (cb) Promise.resolve().then(() => cb(cbErr)); finally {
if (cb)
Promise.resolve().then(() => cb(cbErr));
} }
} }
setSyncStatus(status) { setSyncStatus(status) {
if (status === 'syncing') { if (status === 'syncing') {
syncAnimation.syncing(); syncAnimation.syncing();
} else if (status === 'done') { }
else if (status === 'done') {
syncAnimation.done(); syncAnimation.done();
} }
} }
@ -264,7 +233,6 @@ exports.padeditbar = new class {
const padUrl = window.location.href.split('?')[0]; const padUrl = window.location.href.split('?')[0];
const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false'; const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false';
const props = 'width="100%" height="600" frameborder="0"'; const props = 'width="100%" height="600" frameborder="0"';
if ($('#readonlyinput').is(':checked')) { if ($('#readonlyinput').is(':checked')) {
const urlParts = padUrl.split('/'); const urlParts = padUrl.split('/');
urlParts.pop(); urlParts.pop();
@ -272,7 +240,8 @@ exports.padeditbar = new class {
$('#embedinput') $('#embedinput')
.val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`); .val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`);
$('#linkinput').val(readonlyLink); $('#linkinput').val(readonlyLink);
} else { }
else {
$('#embedinput') $('#embedinput')
.val(`<iframe name="embed_readwrite" src="${padUrl}${params}" ${props}></iframe>`); .val(`<iframe name="embed_readwrite" src="${padUrl}${params}" ${props}></iframe>`);
$('#linkinput').val(padUrl); $('#linkinput').val(padUrl);
@ -283,7 +252,6 @@ exports.padeditbar = new class {
$('.toolbar').removeClass('cropped'); $('.toolbar').removeClass('cropped');
$('body').removeClass('mobile-layout'); $('body').removeClass('mobile-layout');
const menuLeft = $('.toolbar .menu_left')[0]; const menuLeft = $('.toolbar .menu_left')[0];
// this is approximate, we cannot measure it because on mobile // this is approximate, we cannot measure it because on mobile
// Layout it takes the full width on the bottom of the page // Layout it takes the full width on the bottom of the page
const menuRightWidth = 280; const menuRightWidth = 280;
@ -295,7 +263,6 @@ exports.padeditbar = new class {
$('.toolbar').addClass('cropped'); $('.toolbar').addClass('cropped');
} }
} }
_bodyKeyEvent(evt) { _bodyKeyEvent(evt) {
// If the event is Alt F9 or Escape & we're already in the editbar menu // If the event is Alt F9 or Escape & we're already in the editbar menu
// Send the users focus back to the pad // Send the users focus back to the pad
@ -311,15 +278,16 @@ exports.padeditbar = new class {
if (typeof pad === 'undefined') { if (typeof pad === 'undefined') {
// Timeslider probably.. // Timeslider probably..
$('#editorcontainerbox').focus(); // Focus back onto the pad $('#editorcontainerbox').focus(); // Focus back onto the pad
} else { }
else {
padeditor.ace.focus(); // Sends focus back to pad padeditor.ace.focus(); // Sends focus back to pad
// The above focus doesn't always work in FF, you have to hit enter afterwards // The above focus doesn't always work in FF, you have to hit enter afterwards
evt.preventDefault(); evt.preventDefault();
} }
} else { }
else {
// Focus on the editbar :) // Focus on the editbar :)
const firstEditbarElement = parent.parent.$('#editbar button').first(); const firstEditbarElement = parent.parent.$('#editbar button').first();
$(evt.currentTarget).blur(); $(evt.currentTarget).blur();
firstEditbarElement.focus(); firstEditbarElement.focus();
evt.preventDefault(); evt.preventDefault();
@ -328,47 +296,44 @@ exports.padeditbar = new class {
// Are we in the toolbar?? // Are we in the toolbar??
if ($(':focus').parents('.toolbar').length === 1) { if ($(':focus').parents('.toolbar').length === 1) {
// On arrow keys go to next/previous button item in editbar // On arrow keys go to next/previous button item in editbar
if (evt.keyCode !== 39 && evt.keyCode !== 37) return; if (evt.keyCode !== 39 && evt.keyCode !== 37)
return;
// Get all the focusable items in the editbar // Get all the focusable items in the editbar
const focusItems = $('#editbar').find('button, select'); const focusItems = $('#editbar').find('button, select');
// On left arrow move to next button in editbar // On left arrow move to next button in editbar
if (evt.keyCode === 37) { if (evt.keyCode === 37) {
// If a dropdown is visible or we're in an input don't move to the next button // If a dropdown is visible or we're in an input don't move to the next button
if ($('.popup').is(':visible') || evt.target.localName === 'input') return; if ($('.popup').is(':visible') || evt.target.localName === 'input')
return;
this._editbarPosition--; this._editbarPosition--;
// Allow focus to shift back to end of row and start of row // Allow focus to shift back to end of row and start of row
if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1; if (this._editbarPosition === -1)
this._editbarPosition = focusItems.length - 1;
$(focusItems[this._editbarPosition]).focus(); $(focusItems[this._editbarPosition]).focus();
} }
// On right arrow move to next button in editbar // On right arrow move to next button in editbar
if (evt.keyCode === 39) { if (evt.keyCode === 39) {
// If a dropdown is visible or we're in an input don't move to the next button // If a dropdown is visible or we're in an input don't move to the next button
if ($('.popup').is(':visible') || evt.target.localName === 'input') return; if ($('.popup').is(':visible') || evt.target.localName === 'input')
return;
this._editbarPosition++; this._editbarPosition++;
// Allow focus to shift back to end of row and start of row // Allow focus to shift back to end of row and start of row
if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0; if (this._editbarPosition >= focusItems.length)
this._editbarPosition = 0;
$(focusItems[this._editbarPosition]).focus(); $(focusItems[this._editbarPosition]).focus();
} }
} }
} }
_registerDefaultCommands() { _registerDefaultCommands() {
this.registerDropdownCommand('showusers', 'users'); this.registerDropdownCommand('showusers', 'users');
this.registerDropdownCommand('settings'); this.registerDropdownCommand('settings');
this.registerDropdownCommand('connectivity'); this.registerDropdownCommand('connectivity');
this.registerDropdownCommand('import_export'); this.registerDropdownCommand('import_export');
this.registerDropdownCommand('embed'); this.registerDropdownCommand('embed');
this.registerCommand('settings', () => { this.registerCommand('settings', () => {
this.toggleDropDown('settings'); this.toggleDropDown('settings');
$('#options-stickychat').focus(); $('#options-stickychat').focus();
}); });
this.registerCommand('import_export', () => { this.registerCommand('import_export', () => {
this.toggleDropDown('import_export'); this.toggleDropDown('import_export');
// If Import file input exists then focus on it.. // If Import file input exists then focus on it..
@ -376,30 +341,26 @@ exports.padeditbar = new class {
setTimeout(() => { setTimeout(() => {
$('#importfileinput').focus(); $('#importfileinput').focus();
}, 100); }, 100);
} else { }
else {
$('.exportlink').first().focus(); $('.exportlink').first().focus();
} }
}); });
this.registerCommand('showusers', () => { this.registerCommand('showusers', () => {
this.toggleDropDown('users'); this.toggleDropDown('users');
$('#myusernameedit').focus(); $('#myusernameedit').focus();
}); });
this.registerCommand('embed', () => { this.registerCommand('embed', () => {
this.setEmbedLinks(); this.setEmbedLinks();
this.toggleDropDown('embed'); this.toggleDropDown('embed');
$('#linkinput').focus().select(); $('#linkinput').focus().select();
}); });
this.registerCommand('savedRevision', () => { this.registerCommand('savedRevision', () => {
padsavedrevs.saveNow(); padsavedrevs.saveNow();
}); });
this.registerCommand('showTimeSlider', () => { this.registerCommand('showTimeSlider', () => {
document.location = `${document.location.pathname}/timeslider`; document.location = `${document.location.pathname}/timeslider`;
}); });
const aceAttributeCommand = (cmd, ace) => { const aceAttributeCommand = (cmd, ace) => {
ace.ace_toggleAttributeOnSelection(cmd); ace.ace_toggleAttributeOnSelection(cmd);
}; };
@ -407,33 +368,26 @@ exports.padeditbar = new class {
this.registerAceCommand('italic', aceAttributeCommand); this.registerAceCommand('italic', aceAttributeCommand);
this.registerAceCommand('underline', aceAttributeCommand); this.registerAceCommand('underline', aceAttributeCommand);
this.registerAceCommand('strikethrough', aceAttributeCommand); this.registerAceCommand('strikethrough', aceAttributeCommand);
this.registerAceCommand('undo', (cmd, ace) => { this.registerAceCommand('undo', (cmd, ace) => {
ace.ace_doUndoRedo(cmd); ace.ace_doUndoRedo(cmd);
}); });
this.registerAceCommand('redo', (cmd, ace) => { this.registerAceCommand('redo', (cmd, ace) => {
ace.ace_doUndoRedo(cmd); ace.ace_doUndoRedo(cmd);
}); });
this.registerAceCommand('insertunorderedlist', (cmd, ace) => { this.registerAceCommand('insertunorderedlist', (cmd, ace) => {
ace.ace_doInsertUnorderedList(); ace.ace_doInsertUnorderedList();
}); });
this.registerAceCommand('insertorderedlist', (cmd, ace) => { this.registerAceCommand('insertorderedlist', (cmd, ace) => {
ace.ace_doInsertOrderedList(); ace.ace_doInsertOrderedList();
}); });
this.registerAceCommand('indent', (cmd, ace) => { this.registerAceCommand('indent', (cmd, ace) => {
if (!ace.ace_doIndentOutdent(false)) { if (!ace.ace_doIndentOutdent(false)) {
ace.ace_doInsertUnorderedList(); ace.ace_doInsertUnorderedList();
} }
}); });
this.registerAceCommand('outdent', (cmd, ace) => { this.registerAceCommand('outdent', (cmd, ace) => {
ace.ace_doIndentOutdent(true); ace.ace_doIndentOutdent(true);
}); });
this.registerAceCommand('clearauthorship', (cmd, ace) => { this.registerAceCommand('clearauthorship', (cmd, ace) => {
// If we have the whole document selected IE control A has been hit // If we have the whole document selected IE control A has been hit
const rep = ace.ace_getRep(); const rep = ace.ace_getRep();
@ -454,7 +408,6 @@ exports.padeditbar = new class {
* This does make wonder if it's worth having a checkbox to avoid being * This does make wonder if it's worth having a checkbox to avoid being
* prompted again but that's probably overkill for this contribution. * prompted again but that's probably overkill for this contribution.
*/ */
// if we don't have any text selected, we have a caret or we have already said to prompt // if we don't have any text selected, we have a caret or we have already said to prompt
if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) { if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) {
if (window.confirm(html10n.get('pad.editbar.clearcolors'))) { if (window.confirm(html10n.get('pad.editbar.clearcolors'))) {
@ -462,17 +415,17 @@ exports.padeditbar = new class {
['author', ''], ['author', ''],
]); ]);
} }
} else { }
else {
ace.ace_setAttributeOnSelection('author', ''); ace.ace_setAttributeOnSelection('author', '');
} }
}); });
this.registerCommand('timeslider_returnToPad', (cmd) => { this.registerCommand('timeslider_returnToPad', (cmd) => {
if (document.referrer.length > 0 && if (document.referrer.length > 0 &&
document.referrer.substring(document.referrer.lastIndexOf('/') - 1, document.referrer.substring(document.referrer.lastIndexOf('/') - 1, document.referrer.lastIndexOf('/')) === 'p') {
document.referrer.lastIndexOf('/')) === 'p') {
document.location = document.referrer; document.location = document.referrer;
} else { }
else {
document.location = document.location.href document.location = document.location.href
.substring(0, document.location.href.lastIndexOf('/')); .substring(0, document.location.href.lastIndexOf('/'));
} }

View file

@ -1,10 +1,11 @@
import * as padUtils from "./pad_utils.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -20,16 +21,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const Cookies = { Cookies: padUtils }.Cookies;
const Cookies = require('./pad_utils').Cookies; const padcookie = { padcookie: padcookie$0 }.padcookie;
const padcookie = require('./pad_cookie').padcookie; const padutils = { padutils: padUtils }.padutils;
const padutils = require('./pad_utils').padutils;
const padeditor = (() => { const padeditor = (() => {
let Ace2Editor = undefined; let Ace2Editor = undefined;
let pad = undefined; let pad = undefined;
let settings = undefined; let settings = undefined;
const self = { const self = {
ace: null, ace: null,
// this is accessed directly from other files // this is accessed directly from other files
@ -59,13 +57,11 @@ const padeditor = (() => {
padutils.bindCheckboxChange($('#options-linenoscheck'), () => { padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck'))); pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck')));
}); });
// Author colors // Author colors
padutils.bindCheckboxChange($('#options-colorscheck'), () => { padutils.bindCheckboxChange($('#options-colorscheck'), () => {
padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck')); padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck'));
pad.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck')); pad.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck'));
}); });
// Right to left // Right to left
padutils.bindCheckboxChange($('#options-rtlcheck'), () => { padutils.bindCheckboxChange($('#options-rtlcheck'), () => {
pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck'))); pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck')));
@ -74,17 +70,14 @@ const padeditor = (() => {
pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection())); pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection())); padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection()));
}); });
// font family change // font family change
$('#viewfontmenu').change(() => { $('#viewfontmenu').change(() => {
pad.changeViewOption('padFontFamily', $('#viewfontmenu').val()); pad.changeViewOption('padFontFamily', $('#viewfontmenu').val());
}); });
// Language // Language
html10n.bind('localized', () => { html10n.bind('localized', () => {
$('#languagemenu').val(html10n.getLanguage()); $('#languagemenu').val(html10n.getLanguage());
// translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist // translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist
// this does not interfere with html10n's normal value-setting because // this does not interfere with html10n's normal value-setting because
// html10n just ingores <input>s // html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten // also, a value which has been set by the user will be not overwritten
@ -108,32 +101,28 @@ const padeditor = (() => {
setViewOptions: (newOptions) => { setViewOptions: (newOptions) => {
const getOption = (key, defaultValue) => { const getOption = (key, defaultValue) => {
const value = String(newOptions[key]); const value = String(newOptions[key]);
if (value === 'true') return true; if (value === 'true')
if (value === 'false') return false; return true;
if (value === 'false')
return false;
return defaultValue; return defaultValue;
}; };
let v; let v;
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection())); v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
self.ace.setProperty('rtlIsTrue', v); self.ace.setProperty('rtlIsTrue', v);
padutils.setCheckbox($('#options-rtlcheck'), v); padutils.setCheckbox($('#options-rtlcheck'), v);
v = getOption('showLineNumbers', true); v = getOption('showLineNumbers', true);
self.ace.setProperty('showslinenumbers', v); self.ace.setProperty('showslinenumbers', v);
padutils.setCheckbox($('#options-linenoscheck'), v); padutils.setCheckbox($('#options-linenoscheck'), v);
v = getOption('showAuthorColors', true); v = getOption('showAuthorColors', true);
self.ace.setProperty('showsauthorcolors', v); self.ace.setProperty('showsauthorcolors', v);
$('#chattext').toggleClass('authorColors', v); $('#chattext').toggleClass('authorColors', v);
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v); $('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
padutils.setCheckbox($('#options-colorscheck'), v); padutils.setCheckbox($('#options-colorscheck'), v);
// Override from parameters if true // Override from parameters if true
if (settings.noColors !== false) { if (settings.noColors !== false) {
self.ace.setProperty('showsauthorcolors', !settings.noColors); self.ace.setProperty('showsauthorcolors', !settings.noColors);
} }
self.ace.setProperty('textface', newOptions.padFontFamily || ''); self.ace.setProperty('textface', newOptions.padFontFamily || '');
}, },
dispose: () => { dispose: () => {
@ -159,10 +148,7 @@ const padeditor = (() => {
}; };
return self; return self;
})(); })();
export const focusOnLine = (ace) => {
exports.padeditor = padeditor;
exports.focusOnLine = (ace) => {
// If a number is in the URI IE #L124 go to that line number // If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1); const lineNumber = window.location.hash.substr(1);
if (lineNumber) { if (lineNumber) {
@ -208,3 +194,4 @@ exports.focusOnLine = (ace) => {
} }
// End of setSelection / set Y position of editor // End of setSelection / set Y position of editor
}; };
export { padeditor };

View file

@ -1,11 +1,9 @@
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,10 +19,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const padimpexp = (() => { const padimpexp = (() => {
let pad; let pad;
// /// import // /// import
const addImportFrames = () => { const addImportFrames = () => {
$('#import .importframe').remove(); $('#import .importframe').remove();
@ -34,18 +30,17 @@ const padimpexp = (() => {
.addClass('importframe'); .addClass('importframe');
$('#import').append(iframe); $('#import').append(iframe);
}; };
const fileInputUpdated = () => { const fileInputUpdated = () => {
$('#importsubmitinput').addClass('throbbold'); $('#importsubmitinput').addClass('throbbold');
$('#importformfilediv').addClass('importformenabled'); $('#importformfilediv').addClass('importformenabled');
$('#importsubmitinput').removeAttr('disabled'); $('#importsubmitinput').removeAttr('disabled');
$('#importmessagefail').fadeOut('fast'); $('#importmessagefail').fadeOut('fast');
}; };
const fileInputSubmit = function (e) { const fileInputSubmit = function (e) {
e.preventDefault(); e.preventDefault();
$('#importmessagefail').fadeOut('fast'); $('#importmessagefail').fadeOut('fast');
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return; if (!window.confirm(html10n.get('pad.impexp.confirmimport')))
return;
$('#importsubmitinput').attr({ disabled: true }).val(html10n.get('pad.impexp.importing')); $('#importsubmitinput').attr({ disabled: true }).val(html10n.get('pad.impexp.importing'));
window.setTimeout(() => $('#importfileinput').attr({ disabled: true }), 0); window.setTimeout(() => $('#importfileinput').attr({ disabled: true }), 0);
$('#importarrow').stop(true, true).hide(); $('#importarrow').stop(true, true).hide();
@ -60,14 +55,17 @@ const padimpexp = (() => {
dataType: 'json', dataType: 'json',
timeout: 25000, timeout: 25000,
}).catch((err) => { }).catch((err) => {
if (err.responseJSON) return err.responseJSON; if (err.responseJSON)
return err.responseJSON;
return { code: 2, message: 'Unknown import error' }; return { code: 2, message: 'Unknown import error' };
}); });
if (code !== 0) { if (code !== 0) {
importErrorMessage(message); importErrorMessage(message);
} else { }
else {
$('#import_export').removeClass('popup-show'); $('#import_export').removeClass('popup-show');
if (directDatabaseAccess) window.location.reload(); if (directDatabaseAccess)
window.location.reload();
} }
$('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton')); $('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton'));
window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0); window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0);
@ -75,7 +73,6 @@ const padimpexp = (() => {
addImportFrames(); addImportFrames();
})(); })();
}; };
const importErrorMessage = (status) => { const importErrorMessage = (status) => {
const known = [ const known = [
'convertFailed', 'convertFailed',
@ -85,7 +82,6 @@ const padimpexp = (() => {
'permission', 'permission',
]; ];
const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`); const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`);
const showError = (fade) => { const showError = (fade) => {
const popup = $('#importmessagefail').empty() const popup = $('#importmessagefail').empty()
.append($('<strong>') .append($('<strong>')
@ -94,73 +90,67 @@ const padimpexp = (() => {
.append(document.createTextNode(msg)); .append(document.createTextNode(msg));
popup[(fade ? 'fadeIn' : 'show')](); popup[(fade ? 'fadeIn' : 'show')]();
}; };
if ($('#importexport .importmessage').is(':visible')) { if ($('#importexport .importmessage').is(':visible')) {
$('#importmessagesuccess').fadeOut('fast'); $('#importmessagesuccess').fadeOut('fast');
$('#importmessagefail').fadeOut('fast', () => showError(true)); $('#importmessagefail').fadeOut('fast', () => showError(true));
} else { }
else {
showError(); showError();
} }
}; };
// /// export // /// export
function cantExport() { function cantExport() {
let type = $(this); let type = $(this);
if (type.hasClass('exporthrefpdf')) { if (type.hasClass('exporthrefpdf')) {
type = 'PDF'; type = 'PDF';
} else if (type.hasClass('exporthrefdoc')) { }
else if (type.hasClass('exporthrefdoc')) {
type = 'Microsoft Word'; type = 'Microsoft Word';
} else if (type.hasClass('exporthrefodt')) { }
else if (type.hasClass('exporthrefodt')) {
type = 'OpenDocument'; type = 'OpenDocument';
} else { }
else {
type = 'this file'; type = 'this file';
} }
alert(html10n.get('pad.impexp.exportdisabled', { type })); alert(html10n.get('pad.impexp.exportdisabled', { type }));
return false; return false;
} }
// /// // ///
const self = { const self = {
init: (_pad) => { init: (_pad) => {
pad = _pad; pad = _pad;
// get /p/padname // get /p/padname
// if /p/ isn't available due to a rewrite we use the clientVars padId // if /p/ isn't available due to a rewrite we use the clientVars padId
const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId; const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId;
// i10l buttom import // i10l buttom import
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton')); $('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
html10n.bind('localized', () => { html10n.bind('localized', () => {
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton')); $('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
}); });
// build the export links // build the export links
$('#exporthtmla').attr('href', `${padRootPath}/export/html`); $('#exporthtmla').attr('href', `${padRootPath}/export/html`);
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`); $('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
$('#exportplaina').attr('href', `${padRootPath}/export/txt`); $('#exportplaina').attr('href', `${padRootPath}/export/txt`);
// hide stuff thats not avaible if abiword/soffice is disabled // hide stuff thats not avaible if abiword/soffice is disabled
if (clientVars.exportAvailable === 'no') { if (clientVars.exportAvailable === 'no') {
$('#exportworda').remove(); $('#exportworda').remove();
$('#exportpdfa').remove(); $('#exportpdfa').remove();
$('#exportopena').remove(); $('#exportopena').remove();
$('#importmessageabiword').show(); $('#importmessageabiword').show();
} else if (clientVars.exportAvailable === 'withoutPDF') { }
else if (clientVars.exportAvailable === 'withoutPDF') {
$('#exportpdfa').remove(); $('#exportpdfa').remove();
$('#exportworda').attr('href', `${padRootPath}/export/doc`); $('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`); $('#exportopena').attr('href', `${padRootPath}/export/odt`);
$('#importexport').css({ height: '142px' }); $('#importexport').css({ height: '142px' });
$('#importexportline').css({ height: '142px' }); $('#importexportline').css({ height: '142px' });
} else { }
else {
$('#exportworda').attr('href', `${padRootPath}/export/doc`); $('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`); $('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`); $('#exportopena').attr('href', `${padRootPath}/export/odt`);
} }
addImportFrames(); addImportFrames();
$('#importfileinput').change(fileInputUpdated); $('#importfileinput').change(fileInputUpdated);
$('#importform').unbind('submit').submit(fileInputSubmit); $('#importform').unbind('submit').submit(fileInputSubmit);
@ -179,5 +169,4 @@ const padimpexp = (() => {
}; };
return self; return self;
})(); })();
export { padimpexp };
exports.padimpexp = padimpexp;

View file

@ -1,11 +1,11 @@
import { padeditbar as padeditbar$0 } from "./pad_editbar.js";
import * as automaticReconnect from "./pad_automatic_reconnect.js";
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,10 +21,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const padeditbar = { padeditbar: padeditbar$0 }.padeditbar;
const padeditbar = require('./pad_editbar').padeditbar;
const automaticReconnect = require('./pad_automatic_reconnect');
const padmodals = (() => { const padmodals = (() => {
let pad = undefined; let pad = undefined;
const self = { const self = {
@ -35,10 +32,8 @@ const padmodals = (() => {
padeditbar.toggleDropDown('none'); padeditbar.toggleDropDown('none');
$('#connectivity .visible').removeClass('visible'); $('#connectivity .visible').removeClass('visible');
$(`#connectivity .${messageId}`).addClass('visible'); $(`#connectivity .${messageId}`).addClass('visible');
const $modal = $(`#connectivity .${messageId}`); const $modal = $(`#connectivity .${messageId}`);
automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad); automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad);
padeditbar.toggleDropDown('connectivity'); padeditbar.toggleDropDown('connectivity');
}, },
showOverlay: () => { showOverlay: () => {
@ -51,5 +46,4 @@ const padmodals = (() => {
}; };
return self; return self;
})(); })();
export { padmodals };
exports.padmodals = padmodals;

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
/** /**
* Copyright 2012 Peter 'Pita' Martischka * Copyright 2012 Peter 'Pita' Martischka
* *
@ -15,10 +14,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
let pad; let pad;
export const saveNow = () => {
exports.saveNow = () => {
pad.collabClient.sendMessage({ type: 'SAVE_REVISION' }); pad.collabClient.sendMessage({ type: 'SAVE_REVISION' });
$.gritter.add({ $.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
@ -32,7 +29,6 @@ exports.saveNow = () => {
class_name: 'saved-revision', class_name: 'saved-revision',
}); });
}; };
export const init = (_pad) => {
exports.init = (_pad) => {
pad = _pad; pad = _pad;
}; };

View file

@ -1,5 +1,6 @@
import { padutils as padutils$0 } from "./pad_utils.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict'; 'use strict';
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -15,21 +16,15 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const padutils = { padutils: padutils$0 }.padutils;
const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks');
let myUserInfo = {}; let myUserInfo = {};
let colorPickerOpen = false; let colorPickerOpen = false;
let colorPickerSetup = false; let colorPickerSetup = false;
const paduserlist = (() => { const paduserlist = (() => {
const rowManager = (() => { const rowManager = (() => {
// The row manager handles rendering rows of the user list and animating // The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height // their insertion, removal, and reordering. It manipulates TD height
// and TD opacity. // and TD opacity.
const nextRowId = () => `usertr${nextRowId.counter++}`; const nextRowId = () => `usertr${nextRowId.counter++}`;
nextRowId.counter = 1; nextRowId.counter = 1;
// objects are shared; fields are "domId","data","animationStep" // objects are shared; fields are "domId","data","animationStep"
@ -38,7 +33,6 @@ const paduserlist = (() => {
const rowsPresent = []; // in order const rowsPresent = []; // in order
const ANIMATION_START = -12; // just starting to fade in const ANIMATION_START = -12; // just starting to fade in
const ANIMATION_END = 12; // just finishing fading out const ANIMATION_END = 12; // just finishing fading out
const animateStep = () => { const animateStep = () => {
// animation must be symmetrical // animation must be symmetrical
for (let i = rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal for (let i = rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal
@ -49,13 +43,16 @@ const paduserlist = (() => {
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity); const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -OPACITY_STEPS) { if (step <= -OPACITY_STEPS) {
node.find('td').height(animHeight); node.find('td').height(animHeight);
} else if (step === -OPACITY_STEPS + 1) { }
else if (step === -OPACITY_STEPS + 1) {
node.empty().append(createUserRowTds(animHeight, row.data)) node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1 / OPACITY_STEPS); .find('td').css('opacity', baseOpacity * 1 / OPACITY_STEPS);
} else if (step < 0) { }
else if (step < 0) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS) node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS)
.height(animHeight); .height(animHeight);
} else if (step === 0) { }
else if (step === 0) {
// set HTML in case modified during animation // set HTML in case modified during animation
node.empty().append(createUserRowTds(animHeight, row.data)) node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1).height(animHeight); .find('td').css('opacity', baseOpacity * 1).height(animHeight);
@ -71,48 +68,45 @@ const paduserlist = (() => {
if (step < OPACITY_STEPS) { if (step < OPACITY_STEPS) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS) node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS)
.height(animHeight); .height(animHeight);
} else if (step === OPACITY_STEPS) { }
else if (step === OPACITY_STEPS) {
node.empty().append(createEmptyRowTds(animHeight)); node.empty().append(createEmptyRowTds(animHeight));
} else if (step <= ANIMATION_END) { }
else if (step <= ANIMATION_END) {
node.find('td').height(animHeight); node.find('td').height(animHeight);
} else { }
else {
rowsFadingOut.splice(i, 1); // remove from set rowsFadingOut.splice(i, 1); // remove from set
node.remove(); node.remove();
} }
} }
handleOtherUserInputs(); handleOtherUserInputs();
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
}; };
const getAnimationHeight = (step, power) => { const getAnimationHeight = (step, power) => {
let a = Math.abs(step / 12); let a = Math.abs(step / 12);
if (power === 2) a **= 2; if (power === 2)
else if (power === 3) a **= 3; a **= 2;
else if (power === 4) a **= 4; else if (power === 3)
else if (power >= 5) a **= 5; a **= 3;
else if (power === 4)
a **= 4;
else if (power >= 5)
a **= 5;
return Math.round(26 * (1 - a)); return Math.round(26 * (1 - a));
}; };
const OPACITY_STEPS = 6; const OPACITY_STEPS = 6;
const ANIMATION_STEP_TIME = 20; const ANIMATION_STEP_TIME = 20;
const LOWER_FRAMERATE_FACTOR = 2; const LOWER_FRAMERATE_FACTOR = 2;
const {scheduleAnimation} = const { scheduleAnimation } = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR);
padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR);
const NUMCOLS = 4; const NUMCOLS = 4;
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite // we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
// IE's poor handling when manipulating the DOM directly. // IE's poor handling when manipulating the DOM directly.
const createEmptyRowTds = (height) => $('<td>') const createEmptyRowTds = (height) => $('<td>')
.attr('colspan', NUMCOLS) .attr('colspan', NUMCOLS)
.css('border', 0) .css('border', 0)
.css('height', `${height}px`); .css('height', `${height}px`);
const isNameEditable = (data) => (!data.name) && (data.status !== 'Disconnected'); const isNameEditable = (data) => (!data.name) && (data.status !== 'Disconnected');
const replaceUserRowContents = (tr, height, data) => { const replaceUserRowContents = (tr, height, data) => {
const tds = createUserRowTds(height, data); const tds = createUserRowTds(height, data);
if (isNameEditable(data) && tr.find('td.usertdname input:enabled').length > 0) { if (isNameEditable(data) && tr.find('td.usertdname input:enabled').length > 0) {
@ -121,29 +115,32 @@ const paduserlist = (() => {
const oldTd = $(tr.find('td').get(i)); const oldTd = $(tr.find('td').get(i));
if (!oldTd.hasClass('usertdname')) { if (!oldTd.hasClass('usertdname')) {
oldTd.replaceWith(td); oldTd.replaceWith(td);
} else { }
else {
// Prevent leak. I'm not 100% confident that this is necessary, but it shouldn't hurt. // Prevent leak. I'm not 100% confident that this is necessary, but it shouldn't hurt.
$(td).remove(); $(td).remove();
} }
}); });
} else { }
else {
tr.empty().append(tds); tr.empty().append(tds);
} }
return tr; return tr;
}; };
const createUserRowTds = (height, data) => { const createUserRowTds = (height, data) => {
let name; let name;
if (data.name) { if (data.name) {
name = document.createTextNode(data.name); name = document.createTextNode(data.name);
} else { }
else {
name = $('<input>') name = $('<input>')
.attr('data-l10n-id', 'pad.userlist.unnamed') .attr('data-l10n-id', 'pad.userlist.unnamed')
.attr('type', 'text') .attr('type', 'text')
.addClass('editempty') .addClass('editempty')
.addClass('newinput') .addClass('newinput')
.attr('value', html10n.get('pad.userlist.unnamed')); .attr('value', html10n.get('pad.userlist.unnamed'));
if (isNameEditable(data)) name.attr('disabled', 'disabled'); if (isNameEditable(data))
name.attr('disabled', 'disabled');
} }
return $() return $()
.add($('<td>') .add($('<td>')
@ -162,22 +159,19 @@ const paduserlist = (() => {
.addClass('activity') .addClass('activity')
.text(data.activity)); .text(data.activity));
}; };
const createRow = (id, contents, authorId) => $('<tr>') const createRow = (id, contents, authorId) => $('<tr>')
.attr('data-authorId', authorId) .attr('data-authorId', authorId)
.attr('id', id) .attr('id', id)
.append(contents); .append(contents);
const rowNode = (row) => $(`#${row.domId}`); const rowNode = (row) => $(`#${row.domId}`);
const handleRowData = (row) => { const handleRowData = (row) => {
if (row.data && row.data.status === 'Disconnected') { if (row.data && row.data.status === 'Disconnected') {
row.opacity = 0.5; row.opacity = 0.5;
} else { }
else {
delete row.opacity; delete row.opacity;
} }
}; };
const handleOtherUserInputs = () => { const handleOtherUserInputs = () => {
// handle 'INPUT' elements for naming other unnamed users // handle 'INPUT' elements for naming other unnamed users
$('#otheruserstable input.newinput').each(function () { $('#otheruserstable input.newinput').each(function () {
@ -192,14 +186,10 @@ const paduserlist = (() => {
} }
}).removeClass('newinput'); }).removeClass('newinput');
}; };
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc. // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
const insertRow = (position, data, animationPower) => { const insertRow = (position, data, animationPower) => {
position = Math.max(0, Math.min(rowsPresent.length, position)); position = Math.max(0, Math.min(rowsPresent.length, position));
animationPower = (animationPower === undefined ? 4 : animationPower); animationPower = (animationPower === undefined ? 4 : animationPower);
const domId = nextRowId(); const domId = nextRowId();
const row = { const row = {
data, data,
@ -208,33 +198,30 @@ const paduserlist = (() => {
animationPower, animationPower,
}; };
const authorId = data.id; const authorId = data.id;
handleRowData(row); handleRowData(row);
rowsPresent.splice(position, 0, row); rowsPresent.splice(position, 0, row);
let tr; let tr;
if (animationPower === 0) { if (animationPower === 0) {
tr = createRow(domId, createUserRowTds(getAnimationHeight(0), data), authorId); tr = createRow(domId, createUserRowTds(getAnimationHeight(0), data), authorId);
row.animationStep = 0; row.animationStep = 0;
} else { }
else {
rowsFadingIn.push(row); rowsFadingIn.push(row);
tr = createRow(domId, createEmptyRowTds(getAnimationHeight(ANIMATION_START)), authorId); tr = createRow(domId, createEmptyRowTds(getAnimationHeight(ANIMATION_START)), authorId);
} }
$('table#otheruserstable').show(); $('table#otheruserstable').show();
if (position === 0) { if (position === 0) {
$('table#otheruserstable').prepend(tr); $('table#otheruserstable').prepend(tr);
} else { }
else {
rowNode(rowsPresent[position - 1]).after(tr); rowNode(rowsPresent[position - 1]).after(tr);
} }
if (animationPower !== 0) { if (animationPower !== 0) {
scheduleAnimation(); scheduleAnimation();
} }
handleOtherUserInputs(); handleOtherUserInputs();
return row; return row;
}; };
const updateRow = (position, data) => { const updateRow = (position, data) => {
const row = rowsPresent[position]; const row = rowsPresent[position];
if (row) { if (row) {
@ -250,7 +237,6 @@ const paduserlist = (() => {
} }
} }
}; };
const removeRow = (position, animationPower) => { const removeRow = (position, animationPower) => {
animationPower = (animationPower === undefined ? 4 : animationPower); animationPower = (animationPower === undefined ? 4 : animationPower);
const row = rowsPresent[position]; const row = rowsPresent[position];
@ -258,7 +244,8 @@ const paduserlist = (() => {
rowsPresent.splice(position, 1); // remove rowsPresent.splice(position, 1); // remove
if (animationPower === 0) { if (animationPower === 0) {
rowNode(row).remove(); rowNode(row).remove();
} else { }
else {
row.animationStep = -row.animationStep; // use symmetry row.animationStep = -row.animationStep; // use symmetry
row.animationPower = animationPower; row.animationPower = animationPower;
rowsFadingOut.push(row); rowsFadingOut.push(row);
@ -269,10 +256,7 @@ const paduserlist = (() => {
$('table#otheruserstable').hide(); $('table#otheruserstable').hide();
} }
}; };
// newPosition is position after the row has been removed // newPosition is position after the row has been removed
const moveRow = (oldPosition, newPosition, animationPower) => { const moveRow = (oldPosition, newPosition, animationPower) => {
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
const row = rowsPresent[oldPosition]; const row = rowsPresent[oldPosition];
@ -282,7 +266,6 @@ const paduserlist = (() => {
insertRow(newPosition, rowData, animationPower); insertRow(newPosition, rowData, animationPower);
} }
}; };
const self = { const self = {
insertRow, insertRow,
removeRow, removeRow,
@ -293,26 +276,26 @@ const paduserlist = (() => {
})(); // //////// rowManager })(); // //////// rowManager
const otherUsersInfo = []; const otherUsersInfo = [];
const otherUsersData = []; const otherUsersData = [];
const rowManagerMakeNameEditor = (jnode, userId) => { const rowManagerMakeNameEditor = (jnode, userId) => {
setUpEditable(jnode, () => { setUpEditable(jnode, () => {
const existingIndex = findExistingIndex(userId); const existingIndex = findExistingIndex(userId);
if (existingIndex >= 0) { if (existingIndex >= 0) {
return otherUsersInfo[existingIndex].name || ''; return otherUsersInfo[existingIndex].name || '';
} else { }
else {
return ''; return '';
} }
}, (newName) => { }, (newName) => {
if (!newName) { if (!newName) {
jnode.addClass('editempty'); jnode.addClass('editempty');
jnode.val(html10n.get('pad.userlist.unnamed')); jnode.val(html10n.get('pad.userlist.unnamed'));
} else { }
else {
jnode.attr('disabled', 'disabled'); jnode.attr('disabled', 'disabled');
pad.suggestUserName(userId, newName); pad.suggestUserName(userId, newName);
} }
}); });
}; };
const findExistingIndex = (userId) => { const findExistingIndex = (userId) => {
let existingIndex = -1; let existingIndex = -1;
for (let i = 0; i < otherUsersInfo.length; i++) { for (let i = 0; i < otherUsersInfo.length; i++) {
@ -323,7 +306,6 @@ const paduserlist = (() => {
} }
return existingIndex; return existingIndex;
}; };
const setUpEditable = (jqueryNode, valueGetter, valueSetter) => { const setUpEditable = (jqueryNode, valueGetter, valueSetter) => {
jqueryNode.bind('focus', (evt) => { jqueryNode.bind('focus', (evt) => {
const oldValue = valueGetter(); const oldValue = valueGetter();
@ -343,20 +325,15 @@ const paduserlist = (() => {
}); });
jqueryNode.removeAttr('disabled').addClass('editable'); jqueryNode.removeAttr('disabled').addClass('editable');
}; };
let pad = undefined; let pad = undefined;
const self = { const self = {
init: (myInitialUserInfo, _pad) => { init: (myInitialUserInfo, _pad) => {
pad = _pad; pad = _pad;
self.setMyUserInfo(myInitialUserInfo); self.setMyUserInfo(myInitialUserInfo);
if ($('#online_count').length === 0) { if ($('#online_count').length === 0) {
$('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>'); $('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>');
} }
$('#otheruserstable tr').remove(); $('#otheruserstable tr').remove();
$('#myusernameedit').addClass('myusernameedithoverable'); $('#myusernameedit').addClass('myusernameedithoverable');
setUpEditable($('#myusernameedit'), () => myUserInfo.name || '', (newValue) => { setUpEditable($('#myusernameedit'), () => myUserInfo.name || '', (newValue) => {
myUserInfo.name = newValue; myUserInfo.name = newValue;
@ -367,7 +344,6 @@ const paduserlist = (() => {
self.renderMyUserInfo(); self.renderMyUserInfo();
}, 0); }, 0);
}); });
// color picker // color picker
$('#myswatchbox').click(showColorPicker); $('#myswatchbox').click(showColorPicker);
$('#mycolorpicker .pickerswatchouter').click(function () { $('#mycolorpicker .pickerswatchouter').click(function () {
@ -394,17 +370,15 @@ const paduserlist = (() => {
users: () => { users: () => {
// Returns an object of users who have been on this pad // Returns an object of users who have been on this pad
const userList = self.usersOnline(); const userList = self.usersOnline();
// Now we add historical authors // Now we add historical authors
const historical = clientVars.collab_client_vars.historicalAuthorData; const historical = clientVars.collab_client_vars.historicalAuthorData;
for (const [key, { userId }] of Object.entries(historical)) { for (const [key, { userId }] of Object.entries(historical)) {
// Check we don't already have this author in our array // Check we don't already have this author in our array
let exists = false; let exists = false;
userList.forEach((user) => { userList.forEach((user) => {
if (user.userId === userId) exists = true; if (user.userId === userId)
exists = true;
}); });
if (exists === false) { if (exists === false) {
userList.push(historical[key]); userList.push(historical[key]);
} }
@ -416,10 +390,7 @@ const paduserlist = (() => {
if (typeof info.colorId === 'number') { if (typeof info.colorId === 'number') {
info.colorId = clientVars.colorPalette[info.colorId]; info.colorId = clientVars.colorPalette[info.colorId];
} }
myUserInfo = $.extend({}, info);
myUserInfo = $.extend(
{}, info);
self.renderMyUserInfo(); self.renderMyUserInfo();
}, },
userJoinOrUpdate: (info) => { userJoinOrUpdate: (info) => {
@ -427,11 +398,9 @@ const paduserlist = (() => {
// not sure how this would happen // not sure how this would happen
return; return;
} }
hooks.callAll('userJoinOrUpdate', { hooks.callAll('userJoinOrUpdate', {
userInfo: info, userInfo: info,
}); });
const userData = {}; const userData = {};
userData.color = typeof info.colorId === 'number' userData.color = typeof info.colorId === 'number'
? clientVars.colorPalette[info.colorId] : info.colorId; ? clientVars.colorPalette[info.colorId] : info.colorId;
@ -439,9 +408,7 @@ const paduserlist = (() => {
userData.status = ''; userData.status = '';
userData.activity = ''; userData.activity = '';
userData.id = info.userId; userData.id = info.userId;
const existingIndex = findExistingIndex(info.userId); const existingIndex = findExistingIndex(info.userId);
let numUsersBesides = otherUsersInfo.length; let numUsersBesides = otherUsersInfo.length;
if (existingIndex >= 0) { if (existingIndex >= 0) {
numUsersBesides--; numUsersBesides--;
@ -458,14 +425,14 @@ const paduserlist = (() => {
const idThis = info.userId; const idThis = info.userId;
return (nameN > nameThis) || (nameN === nameThis && idN > idThis); return (nameN > nameThis) || (nameN === nameThis && idN > idThis);
}); });
if (existingIndex >= 0) { if (existingIndex >= 0) {
// update // update
if (existingIndex === newIndex) { if (existingIndex === newIndex) {
otherUsersInfo[existingIndex] = info; otherUsersInfo[existingIndex] = info;
otherUsersData[existingIndex] = userData; otherUsersData[existingIndex] = userData;
rowManager.updateRow(existingIndex, userData); rowManager.updateRow(existingIndex, userData);
} else { }
else {
otherUsersInfo.splice(existingIndex, 1); otherUsersInfo.splice(existingIndex, 1);
otherUsersData.splice(existingIndex, 1); otherUsersData.splice(existingIndex, 1);
otherUsersInfo.splice(newIndex, 0, info); otherUsersInfo.splice(newIndex, 0, info);
@ -473,12 +440,12 @@ const paduserlist = (() => {
rowManager.updateRow(existingIndex, userData); rowManager.updateRow(existingIndex, userData);
rowManager.moveRow(existingIndex, newIndex); rowManager.moveRow(existingIndex, newIndex);
} }
} else { }
else {
otherUsersInfo.splice(newIndex, 0, info); otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData); otherUsersData.splice(newIndex, 0, userData);
rowManager.insertRow(newIndex, userData); rowManager.insertRow(newIndex, userData);
} }
self.updateNumberOfOnlineUsers(); self.updateNumberOfOnlineUsers();
}, },
updateNumberOfOnlineUsers: () => { updateNumberOfOnlineUsers: () => {
@ -488,9 +455,7 @@ const paduserlist = (() => {
online++; online++;
} }
} }
$('#online_count').text(online); $('#online_count').text(online);
return online; return online;
}, },
userLeave: (info) => { userLeave: (info) => {
@ -523,30 +488,28 @@ const paduserlist = (() => {
}, 8000); // how long to wait }, 8000); // how long to wait
userData.leaveTimer = thisLeaveTimer; userData.leaveTimer = thisLeaveTimer;
} }
self.updateNumberOfOnlineUsers(); self.updateNumberOfOnlineUsers();
}, },
renderMyUserInfo: () => { renderMyUserInfo: () => {
if (myUserInfo.name) { if (myUserInfo.name) {
$('#myusernameedit').removeClass('editempty').val(myUserInfo.name); $('#myusernameedit').removeClass('editempty').val(myUserInfo.name);
} else { }
else {
$('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername')); $('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername'));
} }
if (colorPickerOpen) { if (colorPickerOpen) {
$('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable'); $('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable');
} else { }
else {
$('#myswatchbox').addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable'); $('#myswatchbox').addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable');
} }
$('#myswatch').css({ 'background-color': myUserInfo.colorId }); $('#myswatch').css({ 'background-color': myUserInfo.colorId });
$('li[data-key=showusers] > a').css({ 'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}` }); $('li[data-key=showusers] > a').css({ 'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}` });
}, },
}; };
return self; return self;
})(); })();
const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode); const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode);
const closeColorPicker = (accept) => { const closeColorPicker = (accept) => {
if (accept) { if (accept) {
let newColor = $('#mycolorpickerpreview').css('background-color'); let newColor = $('#mycolorpickerpreview').css('background-color');
@ -556,55 +519,46 @@ const closeColorPicker = (accept) => {
delete (parts[0]); delete (parts[0]);
for (let i = 1; i <= 3; ++i) { for (let i = 1; i <= 3; ++i) {
parts[i] = parseInt(parts[i]).toString(16); parts[i] = parseInt(parts[i]).toString(16);
if (parts[i].length === 1) parts[i] = `0${parts[i]}`; if (parts[i].length === 1)
parts[i] = `0${parts[i]}`;
} }
newColor = `#${parts.join('')}`; // "0070ff" newColor = `#${parts.join('')}`; // "0070ff"
} }
myUserInfo.colorId = newColor; myUserInfo.colorId = newColor;
pad.notifyChangeColor(newColor); pad.notifyChangeColor(newColor);
paduserlist.renderMyUserInfo(); paduserlist.renderMyUserInfo();
} else { }
else {
// pad.notifyChangeColor(previousColorId); // pad.notifyChangeColor(previousColorId);
// paduserlist.renderMyUserInfo(); // paduserlist.renderMyUserInfo();
} }
colorPickerOpen = false; colorPickerOpen = false;
$('#mycolorpicker').removeClass('popup-show'); $('#mycolorpicker').removeClass('popup-show');
}; };
const showColorPicker = () => { const showColorPicker = () => {
$.farbtastic('#colorpicker').setColor(myUserInfo.colorId); $.farbtastic('#colorpicker').setColor(myUserInfo.colorId);
if (!colorPickerOpen) { if (!colorPickerOpen) {
const palette = pad.getColorPalette(); const palette = pad.getColorPalette();
if (!colorPickerSetup) { if (!colorPickerSetup) {
const colorsList = $('#colorpickerswatches'); const colorsList = $('#colorpickerswatches');
for (let i = 0; i < palette.length; i++) { for (let i = 0; i < palette.length; i++) {
const li = $('<li>', { const li = $('<li>', {
style: `background: ${palette[i]};`, style: `background: ${palette[i]};`,
}); });
li.appendTo(colorsList); li.appendTo(colorsList);
li.bind('click', (event) => { li.bind('click', (event) => {
$('#colorpickerswatches li').removeClass('picked'); $('#colorpickerswatches li').removeClass('picked');
$(event.target).addClass('picked'); $(event.target).addClass('picked');
const newColorId = getColorPickerSwatchIndex($('#colorpickerswatches .picked')); const newColorId = getColorPickerSwatchIndex($('#colorpickerswatches .picked'));
pad.notifyChangeColor(newColorId); pad.notifyChangeColor(newColorId);
}); });
} }
colorPickerSetup = true; colorPickerSetup = true;
} }
$('#mycolorpicker').addClass('popup-show'); $('#mycolorpicker').addClass('popup-show');
colorPickerOpen = true; colorPickerOpen = true;
$('#colorpickerswatches li').removeClass('picked'); $('#colorpickerswatches li').removeClass('picked');
$($('#colorpickerswatches li')[myUserInfo.colorId]).addClass('picked'); // seems weird $($('#colorpickerswatches li')[myUserInfo.colorId]).addClass('picked'); // seems weird
} }
}; };
export { paduserlist };
exports.paduserlist = paduserlist;

View file

@ -1,32 +1,6 @@
import * as pluginUtils from "./shared.js";
import * as defs from "./plugin_defs.js";
'use strict'; 'use strict';
const pluginUtils = require('./shared');
const defs = require('./plugin_defs');
exports.baseURL = '';
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
exports.update = (cb) => {
// It appears that this response (see #620) may interrupt the current thread
// of execution on Firefox. This schedules the response in the run-loop,
// which appears to fix the issue.
const callback = () => setTimeout(cb, 0);
jQuery.getJSON(
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
).done((data) => {
defs.plugins = data.plugins;
defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
defs.loaded = true;
callback();
}).fail((err) => {
console.error(`Failed to load plugin-definitions: ${err}`);
callback();
});
};
const adoptPluginsFromAncestorsOf = (frame) => { const adoptPluginsFromAncestorsOf = (frame) => {
// Bind plugins with parent; // Bind plugins with parent;
let parentRequire = null; let parentRequire = null;
@ -37,22 +11,42 @@ const adoptPluginsFromAncestorsOf = (frame) => {
break; break;
} }
} }
} catch (error) { }
catch (error) {
// Silence (this can only be a XDomain issue). // Silence (this can only be a XDomain issue).
console.error(error); console.error(error);
} }
if (!parentRequire)
if (!parentRequire) throw new Error('Parent plugins could not be found.'); throw new Error('Parent plugins could not be found.');
const ancestorPluginDefs = parentRequire('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); const ancestorPluginDefs = parentRequire('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
defs.hooks = ancestorPluginDefs.hooks; defs.hooks = ancestorPluginDefs.hooks;
defs.loaded = ancestorPluginDefs.loaded; defs.loaded = ancestorPluginDefs.loaded;
defs.parts = ancestorPluginDefs.parts; defs.parts = ancestorPluginDefs.parts;
defs.plugins = ancestorPluginDefs.plugins; defs.plugins = ancestorPluginDefs.plugins;
const ancestorPlugins = parentRequire('ep_etherpad-lite/static/js/pluginfw/client_plugins'); const ancestorPlugins = parentRequire('ep_etherpad-lite/static/js/pluginfw/client_plugins');
exports.baseURL = ancestorPlugins.baseURL; ancestorPlugins.baseURL;
exports.ensure = ancestorPlugins.ensure; ancestorPlugins.ensure;
exports.update = ancestorPlugins.update; ancestorPlugins.update;
}; };
export const baseURL = '';
exports.adoptPluginsFromAncestorsOf = adoptPluginsFromAncestorsOf; export const ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
export const update = (cb) => {
// It appears that this response (see #620) may interrupt the current thread
// of execution on Firefox. This schedules the response in the run-loop,
// which appears to fix the issue.
const callback = () => setTimeout(cb, 0);
jQuery.getJSON(`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`).done((data) => {
defs.plugins = data.plugins;
defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
defs.loaded = true;
callback();
}).fail((err) => {
console.error(`Failed to load plugin-definitions: ${err}`);
callback();
});
};
export { adoptPluginsFromAncestorsOf as baseURL };
export { adoptPluginsFromAncestorsOf as ensure };
export { adoptPluginsFromAncestorsOf as update };
export { adoptPluginsFromAncestorsOf };

View file

@ -2,4 +2,4 @@
// Provides a require'able version of jQuery without leaking $ and jQuery; // Provides a require'able version of jQuery without leaking $ and jQuery;
window.$ = require('./vendors/jquery'); window.$ = require('./vendors/jquery');
const jq = window.$.noConflict(true); const jq = window.$.noConflict(true);
exports.jQuery = exports.$ = jq; export { jq as $ };

View file

@ -1,36 +1,23 @@
import * as caretPosition from "./caretPosition.js";
'use strict'; 'use strict';
/*
This file handles scroll on edition or when user presses arrow keys.
In this file we have two representations of line (browser and rep line).
Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
Browser Line = each vertical line. A <div> can be break into more than one
browser line.
*/
const caretPosition = require('./caretPosition');
function Scroll(outerWin) { function Scroll(outerWin) {
// scroll settings // scroll settings
this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport; this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
// DOM reference // DOM reference
this.outerWin = outerWin; this.outerWin = outerWin;
this.doc = this.outerWin.document; this.doc = this.outerWin.document;
this.rootDocument = parent.parent.document; this.rootDocument = parent.parent.document;
} }
Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary = Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
function (rep, isScrollableEvent, innerHeight) { function (rep, isScrollableEvent, innerHeight) {
// are we placing the caret on the line at the bottom of viewport? // are we placing the caret on the line at the bottom of viewport?
// And if so, do we need to scroll the editor, as defined on the settings.json? // And if so, do we need to scroll the editor, as defined on the settings.json?
const shouldScrollWhenCaretIsAtBottomOfViewport = const shouldScrollWhenCaretIsAtBottomOfViewport = this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
if (shouldScrollWhenCaretIsAtBottomOfViewport) { if (shouldScrollWhenCaretIsAtBottomOfViewport) {
// avoid scrolling when selection includes multiple lines -- // avoid scrolling when selection includes multiple lines --
// user can potentially be selecting more lines // user can potentially be selecting more lines
// than it fits on viewport // than it fits on viewport
const multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0]; const multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
// avoid scrolling when pad loads // avoid scrolling when pad loads
if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) { if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
// when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0 // when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
@ -39,21 +26,19 @@ Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
} }
} }
}; };
Scroll.prototype.scrollWhenPressArrowKeys = function (arrowUp, rep, innerHeight) { Scroll.prototype.scrollWhenPressArrowKeys = function (arrowUp, rep, innerHeight) {
// if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous // if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
// rep line on the top of the viewport // rep line on the top of the viewport
if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) { if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) {
const pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight); const pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
// by default, the browser scrolls to the middle of the viewport. To avoid the twist made // by default, the browser scrolls to the middle of the viewport. To avoid the twist made
// when we apply a second scroll, we made it immediately (without animation) // when we apply a second scroll, we made it immediately (without animation)
this._scrollYPageWithoutAnimation(-pixelsToScroll); this._scrollYPageWithoutAnimation(-pixelsToScroll);
} else { }
else {
this.scrollNodeVerticallyIntoView(rep, innerHeight); this.scrollNodeVerticallyIntoView(rep, innerHeight);
} }
}; };
// Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking // Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking
// if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are // if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are
// other lines after caretLine(), and all of them are out of viewport. // other lines after caretLine(), and all of them are out of viewport.
@ -65,10 +50,8 @@ Scroll.prototype._isCaretAtTheBottomOfViewport = function (rep) {
const caretLine = rep.selStart[0]; const caretLine = rep.selStart[0];
const lineAfterCaretLine = caretLine + 1; const lineAfterCaretLine = caretLine + 1;
const firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep); const firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
const caretLineIsPartiallyVisibleOnViewport = const caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
this._isLinePartiallyVisibleOnViewport(caretLine, rep); const lineAfterCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
const lineAfterCaretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) { if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
// check if the caret is in the bottom of the viewport // check if the caret is in the bottom of the viewport
const caretLinePosition = caretPosition.getPosition(); const caretLinePosition = caretPosition.getPosition();
@ -79,7 +62,6 @@ Scroll.prototype._isCaretAtTheBottomOfViewport = function (rep) {
} }
return false; return false;
}; };
Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep) { Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep) {
const lineNode = rep.lines.atIndex(lineNumber); const lineNode = rep.lines.atIndex(lineNumber);
const linePosition = this._getLineEntryTopBottom(lineNode); const linePosition = this._getLineEntryTopBottom(lineNode);
@ -88,48 +70,40 @@ Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep)
const viewport = this._getViewPortTopBottom(); const viewport = this._getViewPortTopBottom();
const viewportBottom = viewport.bottom; const viewportBottom = viewport.bottom;
const viewportTop = viewport.top; const viewportTop = viewport.top;
const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom; const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom; const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
const topOfLineIsBelowViewportTop = lineTop >= viewportTop; const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom; const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom; const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop; const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) || return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
(topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) || (topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
(bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop); (bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
}; };
Scroll.prototype._getViewPortTopBottom = function () { Scroll.prototype._getViewPortTopBottom = function () {
const theTop = this.getScrollY(); const theTop = this.getScrollY();
const doc = this.doc; const doc = this.doc;
const height = doc.documentElement.clientHeight; // includes padding const height = doc.documentElement.clientHeight; // includes padding
// we have to get the exactly height of the viewport. // we have to get the exactly height of the viewport.
// So it has to subtract all the values which changes // So it has to subtract all the values which changes
// the viewport height (E.g. padding, position top) // the viewport height (E.g. padding, position top)
const viewportExtraSpacesAndPosition = const viewportExtraSpacesAndPosition = this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
return { return {
top: theTop, top: theTop,
bottom: (theTop + height - viewportExtraSpacesAndPosition), bottom: (theTop + height - viewportExtraSpacesAndPosition),
}; };
}; };
Scroll.prototype._getEditorPositionTop = function () { Scroll.prototype._getEditorPositionTop = function () {
const editor = parent.document.getElementsByTagName('iframe'); const editor = parent.document.getElementsByTagName('iframe');
const editorPositionTop = editor[0].offsetTop; const editorPositionTop = editor[0].offsetTop;
return editorPositionTop; return editorPositionTop;
}; };
// ep_page_view adds padding-top, which makes the viewport smaller // ep_page_view adds padding-top, which makes the viewport smaller
Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function () { Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function () {
const aceOuter = this.rootDocument.getElementsByName('ace_outer'); const aceOuter = this.rootDocument.getElementsByName('ace_outer');
const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top')); const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
return aceOuterPaddingTop; return aceOuterPaddingTop;
}; };
Scroll.prototype._getScrollXY = function () { Scroll.prototype._getScrollXY = function () {
const win = this.outerWin; const win = this.outerWin;
const odoc = this.doc; const odoc = this.doc;
@ -147,36 +121,27 @@ Scroll.prototype._getScrollXY = function () {
}; };
} }
}; };
Scroll.prototype.getScrollX = function () { Scroll.prototype.getScrollX = function () {
return this._getScrollXY().x; return this._getScrollXY().x;
}; };
Scroll.prototype.getScrollY = function () { Scroll.prototype.getScrollY = function () {
return this._getScrollXY().y; return this._getScrollXY().y;
}; };
Scroll.prototype.setScrollX = function (x) { Scroll.prototype.setScrollX = function (x) {
this.outerWin.scrollTo(x, this.getScrollY()); this.outerWin.scrollTo(x, this.getScrollY());
}; };
Scroll.prototype.setScrollY = function (y) { Scroll.prototype.setScrollY = function (y) {
this.outerWin.scrollTo(this.getScrollX(), y); this.outerWin.scrollTo(this.getScrollX(), y);
}; };
Scroll.prototype.setScrollXY = function (x, y) { Scroll.prototype.setScrollXY = function (x, y) {
this.outerWin.scrollTo(x, y); this.outerWin.scrollTo(x, y);
}; };
Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) { Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
const caretLine = rep.selStart[0]; const caretLine = rep.selStart[0];
const linePrevCaretLine = caretLine - 1; const linePrevCaretLine = caretLine - 1;
const firstLineVisibleBeforeCaretLine = const firstLineVisibleBeforeCaretLine = caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep); const caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const caretLineIsPartiallyVisibleOnViewport = const lineBeforeCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) { if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
const caretLinePosition = caretPosition.getPosition(); // get the position of the browser line const caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
const viewportPosition = this._getViewPortTopBottom(); const viewportPosition = this._getViewPortTopBottom();
@ -184,8 +149,7 @@ Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
const viewportBottom = viewportPosition.bottom; const viewportBottom = viewportPosition.bottom;
const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop; const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom; const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
const caretLineIsInsideOfViewport = const caretLineIsInsideOfViewport = caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
if (caretLineIsInsideOfViewport) { if (caretLineIsInsideOfViewport) {
const prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep); const prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
const previousLineIsAboveViewportTop = prevLineTop < viewportTop; const previousLineIsAboveViewportTop = prevLineTop < viewportTop;
@ -194,7 +158,6 @@ Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
} }
return false; return false;
}; };
// By default, when user makes an edition in a line out of viewport, this line goes // By default, when user makes an edition in a line out of viewport, this line goes
// to the edge of viewport. This function gets the extra pixels necessary to get the // to the edge of viewport. This function gets the extra pixels necessary to get the
// caret line in a position X relative to Y% viewport. // caret line in a position X relative to Y% viewport.
@ -207,7 +170,6 @@ Scroll.prototype._getPixelsRelativeToPercentageOfViewport =
} }
return pixels; return pixels;
}; };
// we use different percentages when change selection. It depends on if it is // we use different percentages when change selection. It depends on if it is
// either above the top or below the bottom of the page // either above the top or below the bottom of the page
Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) { Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) {
@ -217,7 +179,6 @@ Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) {
} }
return percentageToScroll; return percentageToScroll;
}; };
Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeight) { Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeight) {
let pixels = 0; let pixels = 0;
const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp; const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
@ -226,35 +187,28 @@ Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeigh
} }
return pixels; return pixels;
}; };
Scroll.prototype._scrollYPage = function (pixelsToScroll) { Scroll.prototype._scrollYPage = function (pixelsToScroll) {
const durationOfAnimationToShowFocusline = this.scrollSettings.duration; const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
if (durationOfAnimationToShowFocusline) { if (durationOfAnimationToShowFocusline) {
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline); this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
} else { }
else {
this._scrollYPageWithoutAnimation(pixelsToScroll); this._scrollYPageWithoutAnimation(pixelsToScroll);
} }
}; };
Scroll.prototype._scrollYPageWithoutAnimation = function (pixelsToScroll) { Scroll.prototype._scrollYPageWithoutAnimation = function (pixelsToScroll) {
this.outerWin.scrollBy(0, pixelsToScroll); this.outerWin.scrollBy(0, pixelsToScroll);
}; };
Scroll.prototype._scrollYPageWithAnimation = Scroll.prototype._scrollYPageWithAnimation =
function (pixelsToScroll, durationOfAnimationToShowFocusline) { function (pixelsToScroll, durationOfAnimationToShowFocusline) {
const outerDocBody = this.doc.getElementById('outerdocbody'); const outerDocBody = this.doc.getElementById('outerdocbody');
// it works on later versions of Chrome // it works on later versions of Chrome
const $outerDocBody = $(outerDocBody); const $outerDocBody = $(outerDocBody);
this._triggerScrollWithAnimation( this._triggerScrollWithAnimation($outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
$outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
// it works on Firefox and earlier versions of Chrome // it works on Firefox and earlier versions of Chrome
const $outerDocBodyParent = $outerDocBody.parent(); const $outerDocBodyParent = $outerDocBody.parent();
this._triggerScrollWithAnimation( this._triggerScrollWithAnimation($outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
$outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
}; };
// using a custom queue and clearing it, we avoid creating a queue of scroll animations. // using a custom queue and clearing it, we avoid creating a queue of scroll animations.
// So if this function is called twice quickly, only the last one runs. // So if this function is called twice quickly, only the last one runs.
Scroll.prototype._triggerScrollWithAnimation = Scroll.prototype._triggerScrollWithAnimation =
@ -268,14 +222,12 @@ Scroll.prototype._triggerScrollWithAnimation =
queue: 'scrollanimation', queue: 'scrollanimation',
}).dequeue('scrollanimation'); }).dequeue('scrollanimation');
}; };
// scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance // scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance
// needed to be completely in view. If the value is greater than 0 and less than or equal to 1, // needed to be completely in view. If the value is greater than 0 and less than or equal to 1,
// besides of scrolling the minimum needed to be visible, it scrolls additionally // besides of scrolling the minimum needed to be visible, it scrolls additionally
// (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels // (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels
Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) { Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
const viewport = this._getViewPortTopBottom(); const viewport = this._getViewPortTopBottom();
// when the selection changes outside of the viewport the browser automatically scrolls the line // when the selection changes outside of the viewport the browser automatically scrolls the line
// to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now // to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
// So, when the line scrolled gets outside of the viewport we let the browser handle it. // So, when the line scrolled gets outside of the viewport we let the browser handle it.
@ -286,10 +238,10 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
const caretIsAboveOfViewport = distanceOfTopOfViewport < 0; const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0; const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
if (caretIsAboveOfViewport) { if (caretIsAboveOfViewport) {
const pixelsToScroll = const pixelsToScroll = distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
this._scrollYPage(pixelsToScroll); this._scrollYPage(pixelsToScroll);
} else if (caretIsBelowOfViewport) { }
else if (caretIsBelowOfViewport) {
// setTimeout is required here as line might not be fully rendered onto the pad // setTimeout is required here as line might not be fully rendered onto the pad
setTimeout(() => { setTimeout(() => {
const outer = window.parent; const outer = window.parent;
@ -302,17 +254,14 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
} }
} }
}; };
Scroll.prototype._partOfRepLineIsOutOfViewport = function (viewportPosition, rep) { Scroll.prototype._partOfRepLineIsOutOfViewport = function (viewportPosition, rep) {
const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]); const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
const line = rep.lines.atIndex(focusLine); const line = rep.lines.atIndex(focusLine);
const linePosition = this._getLineEntryTopBottom(line); const linePosition = this._getLineEntryTopBottom(line);
const lineIsAboveOfViewport = linePosition.top < viewportPosition.top; const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom; const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
return lineIsBelowOfViewport || lineIsAboveOfViewport; return lineIsBelowOfViewport || lineIsAboveOfViewport;
}; };
Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) { Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
const dom = entry.lineNode; const dom = entry.lineNode;
const top = dom.offsetTop; const top = dom.offsetTop;
@ -322,12 +271,10 @@ Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
obj.bottom = (top + height); obj.bottom = (top + height);
return obj; return obj;
}; };
Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function (arrowUp, rep) { Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function (arrowUp, rep) {
const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp; const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep); return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
}; };
Scroll.prototype.getVisibleLineRange = function (rep) { Scroll.prototype.getVisibleLineRange = function (rep) {
const viewport = this._getViewPortTopBottom(); const viewport = this._getViewPortTopBottom();
// console.log("viewport top/bottom: %o", viewport); // console.log("viewport top/bottom: %o", viewport);
@ -338,14 +285,13 @@ Scroll.prototype.getVisibleLineRange = function (rep) {
// the viewport. That is the first line that is below the viewport bottom. // the viewport. That is the first line that is below the viewport bottom.
// So the line that is in the bottom of the viewport is the very previous one. // So the line that is in the bottom of the viewport is the very previous one.
let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom); let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom);
if (end < start) end = start; // unlikely if (end < start)
end = start; // unlikely
// top.console.log(start+","+(end -1)); // top.console.log(start+","+(end -1));
return [start, end - 1]; return [start, end - 1];
}; };
Scroll.prototype.getVisibleCharRange = function (rep) { Scroll.prototype.getVisibleCharRange = function (rep) {
const lineRange = this.getVisibleLineRange(rep); const lineRange = this.getVisibleLineRange(rep);
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])]; return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
}; };
export const init = (outerWin) => new Scroll(outerWin);
exports.init = (outerWin) => new Scroll(outerWin);

View file

@ -1,19 +1,2 @@
'use strict'; 'use strict';
export * from "security";
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
module.exports = require('security');

View file

@ -1,12 +1,9 @@
'use strict'; 'use strict';
// Specific hash to display the skin variants builder popup // Specific hash to display the skin variants builder popup
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') { if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$('#skin-variants').addClass('popup-show'); $('#skin-variants').addClass('popup-show');
const containers = ['editor', 'background', 'toolbar']; const containers = ['editor', 'background', 'toolbar'];
const colors = ['super-light', 'light', 'dark', 'super-dark']; const colors = ['super-light', 'light', 'dark', 'super-dark'];
// add corresponding classes when config change // add corresponding classes when config change
const updateSkinVariantsClasses = () => { const updateSkinVariantsClasses = () => {
const domsToUpdate = [ const domsToUpdate = [
@ -19,20 +16,16 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
domsToUpdate.forEach((el) => { el.removeClass(`${color}-${container}`); }); domsToUpdate.forEach((el) => { el.removeClass(`${color}-${container}`); });
}); });
}); });
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); }); domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
const newClasses = []; const newClasses = [];
$('select.skin-variant-color').each(function () { $('select.skin-variant-color').each(function () {
newClasses.push(`${$(this).val()}-${$(this).data('container')}`); newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
}); });
if ($('#skin-variant-full-width').is(':checked')) newClasses.push('full-width-editor'); if ($('#skin-variant-full-width').is(':checked'))
newClasses.push('full-width-editor');
domsToUpdate.forEach((el) => { el.addClass(newClasses.join(' ')); }); domsToUpdate.forEach((el) => { el.addClass(newClasses.join(' ')); });
$('#skin-variants-result').val(`"skinVariants": "${newClasses.join(' ')}",`); $('#skin-variants-result').val(`"skinVariants": "${newClasses.join(' ')}",`);
}; };
// run on init // run on init
const updateCheckboxFromSkinClasses = () => { const updateCheckboxFromSkinClasses = () => {
$('html').attr('class').split(' ').forEach((classItem) => { $('html').attr('class').split(' ').forEach((classItem) => {
@ -42,14 +35,11 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$(`.skin-variant-color[data-container="${container}"`).val(color); $(`.skin-variant-color[data-container="${container}"`).val(color);
} }
}); });
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor')); $('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
}; };
$('.skin-variant').change(() => { $('.skin-variant').change(() => {
updateSkinVariantsClasses(); updateSkinVariantsClasses();
}); });
updateCheckboxFromSkinClasses(); updateCheckboxFromSkinClasses();
updateSkinVariantsClasses(); updateSkinVariantsClasses();
} }

View file

@ -1,11 +1,9 @@
'use strict'; 'use strict';
/** /**
* This code is mostly from the old Etherpad. Please help us to comment this code. * 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. * This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -21,9 +19,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const _entryWidth = (e) => (e && e.width) || 0; const _entryWidth = (e) => (e && e.width) || 0;
class Node { class Node {
constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) { constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) {
this.key = entry != null ? entry.key : null; this.key = entry != null ? entry.key : null;
@ -34,7 +30,6 @@ class Node {
this.downSkips = Array(levels).fill(downSkips); this.downSkips = Array(levels).fill(downSkips);
this.downSkipWidths = Array(levels).fill(downSkipWidths); this.downSkipWidths = Array(levels).fill(downSkipWidths);
} }
propagateWidthChange() { propagateWidthChange() {
const oldWidth = this.downSkipWidths[0]; const oldWidth = this.downSkipWidths[0];
const newWidth = _entryWidth(this.entry); const newWidth = _entryWidth(this.entry);
@ -51,7 +46,6 @@ class Node {
return widthChange; return widthChange;
} }
} }
// A "point" object at index x allows modifications immediately after the first x elements of the // A "point" object at index x allows modifications immediately after the first x elements of the
// skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point // skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point
// is still valid and points to the same index in the skiplist. Other operations with other points // is still valid and points to the same index in the skiplist. Other operations with other points
@ -89,24 +83,21 @@ class Point {
this.nodes = nodes; this.nodes = nodes;
this.widthSkips = widthSkips; this.widthSkips = widthSkips;
} }
toString() { toString() {
return `Point(${this.loc})`; return `Point(${this.loc})`;
} }
insert(entry) { insert(entry) {
if (entry.key == null) throw new Error('entry.key must not be null'); if (entry.key == null)
throw new Error('entry.key must not be null');
if (this._skipList.containsKey(entry.key)) { if (this._skipList.containsKey(entry.key)) {
throw new Error(`an entry with key ${entry.key} already exists`); throw new Error(`an entry with key ${entry.key} already exists`);
} }
const newNode = new Node(entry); const newNode = new Node(entry);
const pNodes = this.nodes; const pNodes = this.nodes;
const pIdxs = this.idxs; const pIdxs = this.idxs;
const pLoc = this.loc; const pLoc = this.loc;
const widthLoc = this.widthSkips[0] + this.nodes[0].downSkipWidths[0]; const widthLoc = this.widthSkips[0] + this.nodes[0].downSkipWidths[0];
const newWidth = _entryWidth(entry); const newWidth = _entryWidth(entry);
// The new node will have at least level 1 // The new node will have at least level 1
// With a proability of 0.01^(n-1) the nodes level will be >= n // With a proability of 0.01^(n-1) the nodes level will be >= n
while (newNode.levels === 0 || Math.random() < 0.01) { while (newNode.levels === 0 || Math.random() < 0.01) {
@ -149,7 +140,6 @@ class Point {
this._skipList._keyToNodeMap.set(newNode.key, newNode); this._skipList._keyToNodeMap.set(newNode.key, newNode);
this._skipList._totalWidth += newWidth; this._skipList._totalWidth += newWidth;
} }
delete() { delete() {
const elem = this.nodes[0].downPtrs[0]; const elem = this.nodes[0].downPtrs[0];
const elemWidth = _entryWidth(elem.entry); const elemWidth = _entryWidth(elem.entry);
@ -163,7 +153,8 @@ class Point {
up.downSkips[i] = totalSkip; up.downSkips[i] = totalSkip;
const totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth; const totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth;
up.downSkipWidths[i] = totalWidthSkip; up.downSkipWidths[i] = totalWidthSkip;
} else { }
else {
const up = this.nodes[i]; const up = this.nodes[i];
up.downSkips[i]--; up.downSkips[i]--;
up.downSkipWidths[i] -= elemWidth; up.downSkipWidths[i] -= elemWidth;
@ -172,12 +163,10 @@ class Point {
this._skipList._keyToNodeMap.delete(elem.key); this._skipList._keyToNodeMap.delete(elem.key);
this._skipList._totalWidth -= elemWidth; this._skipList._totalWidth -= elemWidth;
} }
getNode() { getNode() {
return this.nodes[0].downPtrs[0]; return this.nodes[0].downPtrs[0];
} }
} }
/** /**
* The skip-list contains "entries", JavaScript objects that each must have a unique "key" * The skip-list contains "entries", JavaScript objects that each must have a unique "key"
* property that is a string. * property that is a string.
@ -192,7 +181,6 @@ class SkipList {
this._start.downPtrs[0] = this._end; this._start.downPtrs[0] = this._end;
this._end.upPtrs[0] = this._start; this._end.upPtrs[0] = this._start;
} }
_getNodeAtOffset(targetOffset) { _getNodeAtOffset(targetOffset) {
let i = 0; let i = 0;
let n = this._start; let n = this._start;
@ -204,25 +192,26 @@ class SkipList {
} }
lvl--; lvl--;
} }
if (n === this._start) return (this._start.downPtrs[0] || null); if (n === this._start)
return (this._start.downPtrs[0] || null);
if (n === this._end) { if (n === this._end) {
return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null;
} }
return n; return n;
} }
_getNodeIndex(node, byWidth) { _getNodeIndex(node, byWidth) {
let dist = (byWidth ? 0 : -1); let dist = (byWidth ? 0 : -1);
let n = node; let n = node;
while (n !== this._start) { while (n !== this._start) {
const lvl = n.levels - 1; const lvl = n.levels - 1;
n = n.upPtrs[lvl]; n = n.upPtrs[lvl];
if (byWidth) dist += n.downSkipWidths[lvl]; if (byWidth)
else dist += n.downSkips[lvl]; dist += n.downSkipWidths[lvl];
else
dist += n.downSkips[lvl];
} }
return dist; return dist;
} }
// Returns index of first entry such that entryFunc(entry) is truthy, // Returns index of first entry such that entryFunc(entry) is truthy,
// or length() if no such entry. Assumes all falsy entries come before // or length() if no such entry. Assumes all falsy entries come before
// all truthy entries. // all truthy entries.
@ -230,13 +219,14 @@ class SkipList {
let low = this._start; let low = this._start;
let lvl = this._start.levels - 1; let lvl = this._start.levels - 1;
let lowIndex = -1; let lowIndex = -1;
const f = (node) => { const f = (node) => {
if (node === this._start) return false; if (node === this._start)
else if (node === this._end) return true; return false;
else return entryFunc(node.entry); else if (node === this._end)
return true;
else
return entryFunc(node.entry);
}; };
while (lvl >= 0) { while (lvl >= 0) {
let nextLow = low.downPtrs[lvl]; let nextLow = low.downPtrs[lvl];
while (!f(nextLow)) { while (!f(nextLow)) {
@ -248,50 +238,56 @@ class SkipList {
} }
return lowIndex + 1; return lowIndex + 1;
} }
length() { return this._keyToNodeMap.size; } length() { return this._keyToNodeMap.size; }
atIndex(i) { atIndex(i) {
if (i < 0) console.warn(`atIndex(${i})`); if (i < 0)
if (i >= this._keyToNodeMap.size) console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`); console.warn(`atIndex(${i})`);
if (i >= this._keyToNodeMap.size)
console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`);
return (new Point(this, i)).getNode().entry; return (new Point(this, i)).getNode().entry;
} }
// differs from Array.splice() in that new elements are in an array, not varargs // differs from Array.splice() in that new elements are in an array, not varargs
splice(start, deleteCount, newEntryArray) { splice(start, deleteCount, newEntryArray) {
if (start < 0) console.warn(`splice(${start}, ...)`); if (start < 0)
console.warn(`splice(${start}, ...)`);
if (start + deleteCount > this._keyToNodeMap.size) { if (start + deleteCount > this._keyToNodeMap.size) {
console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._keyToNodeMap.size}`); console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._keyToNodeMap.size}`);
console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._keyToNodeMap.size); console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._keyToNodeMap.size);
console.trace(); console.trace();
} }
if (!newEntryArray)
if (!newEntryArray) newEntryArray = []; newEntryArray = [];
const pt = new Point(this, start); const pt = new Point(this, start);
for (let i = 0; i < deleteCount; i++) pt.delete(); for (let i = 0; i < deleteCount; i++)
pt.delete();
for (let i = (newEntryArray.length - 1); i >= 0; i--) { for (let i = (newEntryArray.length - 1); i >= 0; i--) {
const entry = newEntryArray[i]; const entry = newEntryArray[i];
pt.insert(entry); pt.insert(entry);
} }
} }
next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; } next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; }
prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; } prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; }
push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); } push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); }
slice(start, end) { slice(start, end) {
// act like Array.slice() // act like Array.slice()
if (start === undefined) start = 0; if (start === undefined)
else if (start < 0) start += this._keyToNodeMap.size; start = 0;
if (end === undefined) end = this._keyToNodeMap.size; else if (start < 0)
else if (end < 0) end += this._keyToNodeMap.size; start += this._keyToNodeMap.size;
if (end === undefined)
if (start < 0) start = 0; end = this._keyToNodeMap.size;
if (start > this._keyToNodeMap.size) start = this._keyToNodeMap.size; else if (end < 0)
if (end < 0) end = 0; end += this._keyToNodeMap.size;
if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size; if (start < 0)
start = 0;
if (end <= start) return []; if (start > this._keyToNodeMap.size)
start = this._keyToNodeMap.size;
if (end < 0)
end = 0;
if (end > this._keyToNodeMap.size)
end = this._keyToNodeMap.size;
if (end <= start)
return [];
let n = this.atIndex(start); let n = this.atIndex(start);
const array = [n]; const array = [n];
for (let i = 1; i < (end - start); i++) { for (let i = 1; i < (end - start); i++) {
@ -300,7 +296,6 @@ class SkipList {
} }
return array; return array;
} }
atKey(key) { return this._keyToNodeMap.get(key).entry; } atKey(key) { return this._keyToNodeMap.get(key).entry; }
indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); } indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); }
indexOfEntry(entry) { return this.indexOfKey(entry.key); } indexOfEntry(entry) { return this.indexOfKey(entry.key); }
@ -316,15 +311,18 @@ class SkipList {
} }
totalWidth() { return this._totalWidth; } totalWidth() { return this._totalWidth; }
offsetOfIndex(i) { offsetOfIndex(i) {
if (i < 0) return 0; if (i < 0)
if (i >= this._keyToNodeMap.size) return this._totalWidth; return 0;
if (i >= this._keyToNodeMap.size)
return this._totalWidth;
return this.offsetOfEntry(this.atIndex(i)); return this.offsetOfEntry(this.atIndex(i));
} }
indexOfOffset(offset) { indexOfOffset(offset) {
if (offset <= 0) return 0; if (offset <= 0)
if (offset >= this._totalWidth) return this._keyToNodeMap.size; return 0;
if (offset >= this._totalWidth)
return this._keyToNodeMap.size;
return this.indexOfEntry(this.atOffset(offset)); return this.indexOfEntry(this.atOffset(offset));
} }
} }
export default SkipList;
module.exports = SkipList;

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
/** /**
* Creates a socket.io connection. * Creates a socket.io connection.
* @param etherpadBaseUrl - Etherpad URL. If relative, it is assumed to be relative to * @param etherpadBaseUrl - Etherpad URL. If relative, it is assumed to be relative to
@ -21,9 +20,9 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
const namespaceUrl = new URL(namespace, new URL('/', baseUrl)); const namespaceUrl = new URL(namespace, new URL('/', baseUrl));
return io(namespaceUrl.href, Object.assign({ path: socketioUrl.pathname }, options)); return io(namespaceUrl.href, Object.assign({ path: socketioUrl.pathname }, options));
}; };
if (typeof exports === 'object') { if (typeof exports === 'object') {
exports.connect = connect; }
} else { else {
window.socketio = { connect }; window.socketio = { connect };
} }
export { connect };

View file

@ -1,102 +1,69 @@
import "./vendors/jquery.js";
import * as padUtils from "./pad_utils.js";
import * as hooks from "./pluginfw/hooks.js";
import * as socketio from "./socketio.js";
import { loadBroadcastRevisionsJS } from "./broadcast_revisions.js";
import { padimpexp } from "./pad_impexp.js";
'use strict'; 'use strict';
const Cookies = { Cookies: padUtils }.Cookies;
/** const randomString = { randomString: padUtils }.randomString;
* This code is mostly from the old Etherpad. Please help us to comment this code. const padutils = { padutils: padUtils }.padutils;
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('./vendors/jquery');
const Cookies = require('./pad_utils').Cookies;
const randomString = require('./pad_utils').randomString;
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
const socketio = require('./socketio');
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider; let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
const init = () => { const init = () => {
padutils.setupGlobalExceptionHandler(); padutils.setupGlobalExceptionHandler();
$(document).ready(() => { $(document).ready(() => {
// start the custom js // start the custom js
if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef if (typeof customStart === 'function')
customStart(); // eslint-disable-line no-undef
// get the padId out of the url // get the padId out of the url
const urlParts = document.location.pathname.split('/'); const urlParts = document.location.pathname.split('/');
padId = decodeURIComponent(urlParts[urlParts.length - 2]); padId = decodeURIComponent(urlParts[urlParts.length - 2]);
// set the title // set the title
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`; document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
// ensure we have a token // ensure we have a token
token = Cookies.get('token'); token = Cookies.get('token');
if (token == null) { if (token == null) {
token = `t.${randomString()}`; token = `t.${randomString()}`;
Cookies.set('token', token, { expires: 60 }); Cookies.set('token', token, { expires: 60 });
} }
socket = socketio.connect(exports.baseURL, '/', { query: { padId } }); socket = socketio.connect(exports.baseURL, '/', { query: { padId } });
// send the ready message once we're connected // send the ready message once we're connected
socket.on('connect', () => { socket.on('connect', () => {
sendSocketMsg('CLIENT_READY', {}); sendSocketMsg('CLIENT_READY', {});
}); });
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
BroadcastSlider.showReconnectUI(); BroadcastSlider.showReconnectUI();
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason === 'io server disconnect') socket.connect(); if (reason === 'io server disconnect')
socket.connect();
}); });
// route the incoming messages // route the incoming messages
socket.on('message', (message) => { socket.on('message', (message) => {
if (message.type === 'CLIENT_VARS') { if (message.type === 'CLIENT_VARS') {
handleClientVars(message); handleClientVars(message);
} else if (message.accessStatus) { }
else if (message.accessStatus) {
$('body').html('<h2>You have no permission to access this pad</h2>'); $('body').html('<h2>You have no permission to access this pad</h2>');
} else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') { }
else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') {
changesetLoader.handleMessageFromServer(message); changesetLoader.handleMessageFromServer(message);
} }
}); });
// get all the export links // get all the export links
exportLinks = $('#export > .exportlink'); exportLinks = $('#export > .exportlink');
$('button#forcereconnect').click(() => { $('button#forcereconnect').click(() => {
window.location.reload(); window.location.reload();
}); });
socket; // make the socket available
exports.socket = socket; // make the socket available BroadcastSlider; // Make the slider available
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
hooks.aCallAll('postTimesliderInit'); hooks.aCallAll('postTimesliderInit');
}); });
}; };
// sends a message over the socket // sends a message over the socket
const sendSocketMsg = (type, data) => { const sendSocketMsg = (type, data) => {
socket.json.send({ socket.json.send({
component: 'pad', // FIXME: Remove this stupidity! component: 'pad',
type, type,
data, data,
padId, padId,
@ -104,33 +71,24 @@ const sendSocketMsg = (type, data) => {
sessionID: Cookies.get('sessionID'), sessionID: Cookies.get('sessionID'),
}); });
}; };
const fireWhenAllScriptsAreLoaded = []; const fireWhenAllScriptsAreLoaded = [];
const handleClientVars = (message) => { const handleClientVars = (message) => {
// save the client Vars // save the client Vars
window.clientVars = message.data; window.clientVars = message.data;
if (window.clientVars.sessionRefreshInterval) { if (window.clientVars.sessionRefreshInterval) {
const ping = const ping = () => $.ajax('../../_extendExpressSessionLifetime', { method: 'PUT' }).catch(() => { });
() => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
setInterval(ping, window.clientVars.sessionRefreshInterval); setInterval(ping, window.clientVars.sessionRefreshInterval);
} }
// load all script that doesn't work without the clientVars // load all script that doesn't work without the clientVars
BroadcastSlider = require('./broadcast_slider') BroadcastSlider = require('./broadcast_slider')
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded); .loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
({ loadBroadcastRevisionsJS }.loadBroadcastRevisionsJS());
require('./broadcast_revisions').loadBroadcastRevisionsJS();
changesetLoader = require('./broadcast') changesetLoader = require('./broadcast')
.loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider); .loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
// initialize export ui // initialize export ui
require('./pad_impexp').padimpexp.init(); ({ padimpexp }.padimpexp.init());
// Create a base URI used for timeslider exports // Create a base URI used for timeslider exports
const baseURI = document.location.pathname; const baseURI = document.location.pathname;
// change export urls when the slider moves // change export urls when the slider moves
BroadcastSlider.onSlider((revno) => { BroadcastSlider.onSlider((revno) => {
// exportLinks is a jQuery Array, so .each is allowed. // exportLinks is a jQuery Array, so .each is allowed.
@ -146,23 +104,21 @@ const handleClientVars = (message) => {
} }
}); });
}); });
// fire all start functions of these scripts, formerly fired with window.load // fire all start functions of these scripts, formerly fired with window.load
for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) { for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) {
fireWhenAllScriptsAreLoaded[i](); fireWhenAllScriptsAreLoaded[i]();
} }
$('#ui-slider-handle').css('left', $('#ui-slider-bar').width() - 2); $('#ui-slider-handle').css('left', $('#ui-slider-bar').width() - 2);
// Translate some strings where we only want to set the title not the actual values // Translate some strings where we only want to set the title not the actual values
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause')); $('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
$('#leftstep').attr('title', html10n.get('timeslider.backRevision')); $('#leftstep').attr('title', html10n.get('timeslider.backRevision'));
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision')); $('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
// font family change // font family change
$('#viewfontmenu').change(function () { $('#viewfontmenu').change(function () {
$('#innerdocbody').css('font-family', $(this).val() || ''); $('#innerdocbody').css('font-family', $(this).val() || '');
}); });
}; };
export const baseURL = '';
exports.baseURL = ''; export { init as socket };
exports.init = init; export { init as BroadcastSlider };
export { init };

View file

@ -1,3 +1,2 @@
'use strict'; 'use strict';
export { default } from "underscore";
module.exports = require('underscore');

View file

@ -1,30 +1,6 @@
import * as Changeset from "./Changeset.js";
import * as _ from "./underscore.js";
'use strict'; 'use strict';
/**
* 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
*/
/**
* Copyright 2009 Google Inc.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
const Changeset = require('./Changeset');
const _ = require('./underscore');
const undoModule = (() => { const undoModule = (() => {
const stack = (() => { const stack = (() => {
const stackElements = []; const stackElements = [];
@ -34,43 +10,36 @@ const undoModule = (() => {
// 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> } // 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> }
// invariant: no two consecutive EXTERNAL_CHANGEs // invariant: no two consecutive EXTERNAL_CHANGEs
let numUndoableEvents = 0; let numUndoableEvents = 0;
const UNDOABLE_EVENT = 'undoableEvent'; const UNDOABLE_EVENT = 'undoableEvent';
const EXTERNAL_CHANGE = 'externalChange'; const EXTERNAL_CHANGE = 'externalChange';
const clearStack = () => { const clearStack = () => {
stackElements.length = 0; stackElements.length = 0;
stackElements.push( stackElements.push({
{
elementType: UNDOABLE_EVENT, elementType: UNDOABLE_EVENT,
eventType: 'bottom', eventType: 'bottom',
}); });
numUndoableEvents = 1; numUndoableEvents = 1;
}; };
clearStack(); clearStack();
const pushEvent = (event) => { const pushEvent = (event) => {
const e = _.extend( const e = _.extend({}, event);
{}, event);
e.elementType = UNDOABLE_EVENT; e.elementType = UNDOABLE_EVENT;
stackElements.push(e); stackElements.push(e);
numUndoableEvents++; numUndoableEvents++;
}; };
const pushExternalChange = (cs) => { const pushExternalChange = (cs) => {
const idx = stackElements.length - 1; const idx = stackElements.length - 1;
if (stackElements[idx].elementType === EXTERNAL_CHANGE) { if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
stackElements[idx].changeset = stackElements[idx].changeset =
Changeset.compose(stackElements[idx].changeset, cs, getAPool()); Changeset.compose(stackElements[idx].changeset, cs, getAPool());
} else { }
stackElements.push( else {
{ stackElements.push({
elementType: EXTERNAL_CHANGE, elementType: EXTERNAL_CHANGE,
changeset: cs, changeset: cs,
}); });
} }
}; };
const _exposeEvent = (nthFromTop) => { const _exposeEvent = (nthFromTop) => {
// precond: 0 <= nthFromTop < numUndoableEvents // precond: 0 <= nthFromTop < numUndoableEvents
const targetIndex = stackElements.length - 1 - nthFromTop; const targetIndex = stackElements.length - 1 - nthFromTop;
@ -101,27 +70,24 @@ const undoModule = (() => {
stackElements.splice(idx - 2, 1); stackElements.splice(idx - 2, 1);
idx--; idx--;
} }
} else { }
else {
idx--; idx--;
} }
} }
}; };
const getNthFromTop = (n) => { const getNthFromTop = (n) => {
// precond: 0 <= n < numEvents() // precond: 0 <= n < numEvents()
_exposeEvent(n); _exposeEvent(n);
return stackElements[stackElements.length - 1 - n]; return stackElements[stackElements.length - 1 - n];
}; };
const numEvents = () => numUndoableEvents; const numEvents = () => numUndoableEvents;
const popEvent = () => { const popEvent = () => {
// precond: numEvents() > 0 // precond: numEvents() > 0
_exposeEvent(0); _exposeEvent(0);
numUndoableEvents--; numUndoableEvents--;
return stackElements.pop(); return stackElements.pop();
}; };
return { return {
numEvents, numEvents,
popEvent, popEvent,
@ -131,15 +97,12 @@ const undoModule = (() => {
getNthFromTop, getNthFromTop,
}; };
})(); })();
// invariant: stack always has at least one undoable event // invariant: stack always has at least one undoable event
let undoPtr = 0; // zero-index from top of stack, 0 == top let undoPtr = 0; // zero-index from top of stack, 0 == top
const clearHistory = () => { const clearHistory = () => {
stack.clearStack(); stack.clearStack();
undoPtr = 0; undoPtr = 0;
}; };
const _charOccurrences = (str, c) => { const _charOccurrences = (str, c) => {
let i = 0; let i = 0;
let count = 0; let count = 0;
@ -152,13 +115,12 @@ const undoModule = (() => {
} }
return count; return count;
}; };
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode); const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode);
const _mergeChangesets = (cs1, cs2) => { const _mergeChangesets = (cs1, cs2) => {
if (!cs1) return cs2; if (!cs1)
if (!cs2) return cs1; return cs2;
if (!cs2)
return cs1;
// Rough heuristic for whether changesets should be considered one action: // Rough heuristic for whether changesets should be considered one action:
// each does exactly one insertion, no dels, and the composition does also; or // each does exactly one insertion, no dels, and the composition does also; or
// each does exactly one deletion, no ins, and the composition does also. // each does exactly one deletion, no ins, and the composition does also.
@ -176,7 +138,8 @@ const undoModule = (() => {
if (plusCount3 === 1 && minusCount3 === 0) { if (plusCount3 === 1 && minusCount3 === 0) {
return merge; return merge;
} }
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) { }
else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
const merge = Changeset.compose(cs1, cs2, getAPool()); const merge = Changeset.compose(cs1, cs2, getAPool());
const plusCount3 = _opcodeOccurrences(merge, '+'); const plusCount3 = _opcodeOccurrences(merge, '+');
const minusCount3 = _opcodeOccurrences(merge, '-'); const minusCount3 = _opcodeOccurrences(merge, '-');
@ -186,10 +149,8 @@ const undoModule = (() => {
} }
return null; return null;
}; };
const reportEvent = (event) => { const reportEvent = (event) => {
const topEvent = stack.getNthFromTop(0); const topEvent = stack.getNthFromTop(0);
const applySelectionToTop = () => { const applySelectionToTop = () => {
if ((typeof event.selStart) === 'number') { if ((typeof event.selStart) === 'number') {
topEvent.selStart = event.selStart; topEvent.selStart = event.selStart;
@ -197,10 +158,10 @@ const undoModule = (() => {
topEvent.selFocusAtStart = event.selFocusAtStart; topEvent.selFocusAtStart = event.selFocusAtStart;
} }
}; };
if ((!event.backset) || Changeset.isIdentity(event.backset)) { if ((!event.backset) || Changeset.isIdentity(event.backset)) {
applySelectionToTop(); applySelectionToTop();
} else { }
else {
let merged = false; let merged = false;
if (topEvent.eventType === event.eventType) { if (topEvent.eventType === event.eventType) {
const merge = _mergeChangesets(event.backset, topEvent.backset); const merge = _mergeChangesets(event.backset, topEvent.backset);
@ -224,17 +185,16 @@ const undoModule = (() => {
undoPtr = 0; undoPtr = 0;
} }
}; };
const reportExternalChange = (changeset) => { const reportExternalChange = (changeset) => {
if (changeset && !Changeset.isIdentity(changeset)) { if (changeset && !Changeset.isIdentity(changeset)) {
stack.pushExternalChange(changeset); stack.pushExternalChange(changeset);
} }
}; };
const _getSelectionInfo = (event) => { const _getSelectionInfo = (event) => {
if ((typeof event.selStart) !== 'number') { if ((typeof event.selStart) !== 'number') {
return null; return null;
} else { }
else {
return { return {
selStart: event.selStart, selStart: event.selStart,
selEnd: event.selEnd, selEnd: event.selEnd,
@ -242,13 +202,11 @@ const undoModule = (() => {
}; };
} }
}; };
// For "undo" and "redo", the change event must be returned // For "undo" and "redo", the change event must be returned
// by eventFunc and NOT reported through the normal mechanism. // by eventFunc and NOT reported through the normal mechanism.
// "eventFunc" should take a changeset and an optional selection info object, // "eventFunc" should take a changeset and an optional selection info object,
// or can be called with no arguments to mean that no undo is possible. // or can be called with no arguments to mean that no undo is possible.
// "eventFunc" will be called exactly once. // "eventFunc" will be called exactly once.
const performUndo = (eventFunc) => { const performUndo = (eventFunc) => {
if (undoPtr < stack.numEvents() - 1) { if (undoPtr < stack.numEvents() - 1) {
const backsetEvent = stack.getNthFromTop(undoPtr); const backsetEvent = stack.getNthFromTop(undoPtr);
@ -256,9 +214,11 @@ const undoModule = (() => {
const undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent)); const undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.pushEvent(undoEvent); stack.pushEvent(undoEvent);
undoPtr += 2; undoPtr += 2;
} else { eventFunc(); } }
else {
eventFunc();
}
}; };
const performRedo = (eventFunc) => { const performRedo = (eventFunc) => {
if (undoPtr >= 2) { if (undoPtr >= 2) {
const backsetEvent = stack.getNthFromTop(0); const backsetEvent = stack.getNthFromTop(0);
@ -266,11 +226,12 @@ const undoModule = (() => {
eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent)); eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.popEvent(); stack.popEvent();
undoPtr -= 2; undoPtr -= 2;
} else { eventFunc(); } }
else {
eventFunc();
}
}; };
const getAPool = () => undoModule.apool; const getAPool = () => undoModule.apool;
return { return {
clearHistory, clearHistory,
reportEvent, reportEvent,
@ -281,5 +242,4 @@ const undoModule = (() => {
apool: null, apool: null,
}; // apool is filled in by caller }; // apool is filled in by caller
})(); })();
export { undoModule };
exports.undoModule = undoModule;

View file

@ -1,198 +1,179 @@
// WARNING: This file may have been modified from original. // WARNING: This file may have been modified from original.
// TODO: Check requirement of this file, this afaik was to cover weird edge cases // TODO: Check requirement of this file, this afaik was to cover weird edge cases
// that have probably been fixed in browsers. // that have probably been fixed in browsers.
/*! /*!
* Bowser - a browser detector * Bowser - a browser detector
* https://github.com/ded/bowser * https://github.com/ded/bowser
* MIT License | (c) Dustin Diaz 2015 * MIT License | (c) Dustin Diaz 2015
*/ */
!function (name, definition) { !function (name, definition) {
if (typeof module != 'undefined' && module.exports) module.exports = definition() if (typeof module != 'undefined' && module.exports)
else if (typeof define == 'function' && define.amd) define(definition) ;
else this[name] = definition() else if (typeof define == 'function' && define.amd)
define(definition);
else
this[name] = definition();
}('bowser', function () { }('bowser', function () {
/** /**
* See useragents.js for examples of navigator.userAgent * See useragents.js for examples of navigator.userAgent
*/ */
var t = true;
var t = true
function detect(ua) { function detect(ua) {
function getFirstMatch(regex) { function getFirstMatch(regex) {
var match = ua.match(regex); var match = ua.match(regex);
return (match && match.length > 1 && match[1]) || ''; return (match && match.length > 1 && match[1]) || '';
} }
function getSecondMatch(regex) { function getSecondMatch(regex) {
var match = ua.match(regex); var match = ua.match(regex);
return (match && match.length > 1 && match[2]) || ''; return (match && match.length > 1 && match[2]) || '';
} }
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase(), likeAndroid = /like android/i.test(ua), android = !likeAndroid && /android/i.test(ua), chromeos = /CrOS/.test(ua), silk = /silk/i.test(ua), sailfish = /sailfish/i.test(ua), tizen = /tizen/i.test(ua), webos = /(web|hpw)os/i.test(ua), windowsphone = /windows phone/i.test(ua), windows = !windowsphone && /windows/i.test(ua), mac = !iosdevice && !silk && /macintosh/i.test(ua), linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua), edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i), versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i), tablet = /tablet/i.test(ua), mobile = !tablet && /[^-]mobi/i.test(ua), result;
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
, likeAndroid = /like android/i.test(ua)
, android = !likeAndroid && /android/i.test(ua)
, chromeos = /CrOS/.test(ua)
, silk = /silk/i.test(ua)
, sailfish = /sailfish/i.test(ua)
, tizen = /tizen/i.test(ua)
, webos = /(web|hpw)os/i.test(ua)
, windowsphone = /windows phone/i.test(ua)
, windows = !windowsphone && /windows/i.test(ua)
, mac = !iosdevice && !silk && /macintosh/i.test(ua)
, linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua)
, edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
, versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
, tablet = /tablet/i.test(ua)
, mobile = !tablet && /[^-]mobi/i.test(ua)
, result
if (/opera|opr/i.test(ua)) { if (/opera|opr/i.test(ua)) {
result = { result = {
name: 'Opera' name: 'Opera',
, opera: t opera: t,
, version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i) version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
} };
} }
else if (/yabrowser/i.test(ua)) { else if (/yabrowser/i.test(ua)) {
result = { result = {
name: 'Yandex Browser' name: 'Yandex Browser',
, yandexbrowser: t yandexbrowser: t,
, version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i) version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
} };
} }
else if (windowsphone) { else if (windowsphone) {
result = { result = {
name: 'Windows Phone' name: 'Windows Phone',
, windowsphone: t windowsphone: t
} };
if (edgeVersion) { if (edgeVersion) {
result.msedge = t result.msedge = t;
result.version = edgeVersion result.version = edgeVersion;
} }
else { else {
result.msie = t result.msie = t;
result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i) result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i);
} }
} }
else if (/msie|trident/i.test(ua)) { else if (/msie|trident/i.test(ua)) {
result = { result = {
name: 'Internet Explorer' name: 'Internet Explorer',
, msie: t msie: t,
, version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i) version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
};
} }
} else if (chromeos) { else if (chromeos) {
result = { result = {
name: 'Chrome' name: 'Chrome',
, chromeos: t chromeos: t,
, chromeBook: t chromeBook: t,
, chrome: t chrome: t,
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i) version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
};
} }
} else if (/chrome.+? edge/i.test(ua)) { else if (/chrome.+? edge/i.test(ua)) {
result = { result = {
name: 'Microsoft Edge' name: 'Microsoft Edge',
, msedge: t msedge: t,
, version: edgeVersion version: edgeVersion
} };
} }
else if (/chrome|crios|crmo/i.test(ua)) { else if (/chrome|crios|crmo/i.test(ua)) {
result = { result = {
name: 'Chrome' name: 'Chrome',
, chrome: t chrome: t,
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i) version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
} };
} }
else if (iosdevice) { else if (iosdevice) {
result = { result = {
name: iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod' name: iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
} };
// WTF: version is not part of user agent in web apps // WTF: version is not part of user agent in web apps
if (versionIdentifier) { if (versionIdentifier) {
result.version = versionIdentifier result.version = versionIdentifier;
} }
} }
else if (sailfish) { else if (sailfish) {
result = { result = {
name: 'Sailfish' name: 'Sailfish',
, sailfish: t sailfish: t,
, version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i) version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
} };
} }
else if (/seamonkey\//i.test(ua)) { else if (/seamonkey\//i.test(ua)) {
result = { result = {
name: 'SeaMonkey' name: 'SeaMonkey',
, seamonkey: t seamonkey: t,
, version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i) version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
} };
} }
else if (/firefox|iceweasel/i.test(ua)) { else if (/firefox|iceweasel/i.test(ua)) {
result = { result = {
name: 'Firefox' name: 'Firefox',
, firefox: t firefox: t,
, version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i) version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
} };
if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) { if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
result.firefoxos = t result.firefoxos = t;
} }
} }
else if (silk) { else if (silk) {
result = { result = {
name: 'Amazon Silk' name: 'Amazon Silk',
, silk: t silk: t,
, version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i) version: getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
} };
} }
else if (android) { else if (android) {
result = { result = {
name: 'Android' name: 'Android',
, version: versionIdentifier version: versionIdentifier
} };
} }
else if (/phantom/i.test(ua)) { else if (/phantom/i.test(ua)) {
result = { result = {
name: 'PhantomJS' name: 'PhantomJS',
, phantom: t phantom: t,
, version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i) version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
} };
} }
else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) { else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
result = { result = {
name: 'BlackBerry' name: 'BlackBerry',
, blackberry: t blackberry: t,
, version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i) version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
} };
} }
else if (webos) { else if (webos) {
result = { result = {
name: 'WebOS' name: 'WebOS',
, webos: t webos: t,
, version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i) version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
}; };
/touchpad\//i.test(ua) && (result.touchpad = t) /touchpad\//i.test(ua) && (result.touchpad = t);
} }
else if (/bada/i.test(ua)) { else if (/bada/i.test(ua)) {
result = { result = {
name: 'Bada' name: 'Bada',
, bada: t bada: t,
, version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i) version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
}; };
} }
else if (tizen) { else if (tizen) {
result = { result = {
name: 'Tizen' name: 'Tizen',
, tizen: t tizen: t,
, version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
}; };
} }
else if (/safari/i.test(ua)) { else if (/safari/i.test(ua)) {
result = { result = {
name: 'Safari' name: 'Safari',
, safari: t safari: t,
, version: versionIdentifier version: versionIdentifier
} };
} }
else { else {
result = { result = {
@ -200,64 +181,71 @@
version: getSecondMatch(/^(.*)\/(.*) /) version: getSecondMatch(/^(.*)\/(.*) /)
}; };
} }
// set webkit or gecko flag for browsers based on these engines // set webkit or gecko flag for browsers based on these engines
if (!result.msedge && /(apple)?webkit/i.test(ua)) { if (!result.msedge && /(apple)?webkit/i.test(ua)) {
result.name = result.name || "Webkit" result.name = result.name || "Webkit";
result.webkit = t result.webkit = t;
if (!result.version && versionIdentifier) { if (!result.version && versionIdentifier) {
result.version = versionIdentifier result.version = versionIdentifier;
} }
} else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko"
result.gecko = t
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
} }
else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko";
result.gecko = t;
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i);
}
// set OS flags for platforms that have multiple browsers // set OS flags for platforms that have multiple browsers
if (!result.msedge && (android || result.silk)) { if (!result.msedge && (android || result.silk)) {
result.android = t result.android = t;
} else if (iosdevice) { }
result[iosdevice] = t else if (iosdevice) {
result.ios = t result[iosdevice] = t;
} else if (windows) { result.ios = t;
result.windows = t }
} else if (mac) { else if (windows) {
result.mac = t result.windows = t;
} else if (linux) { }
result.linux = t else if (mac) {
result.mac = t;
}
else if (linux) {
result.linux = t;
} }
// OS version extraction // OS version extraction
var osVersion = ''; var osVersion = '';
if (result.windowsphone) { if (result.windowsphone) {
osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i); osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
} else if (iosdevice) { }
else if (iosdevice) {
osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i); osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
osVersion = osVersion.replace(/[_\s]/g, '.'); osVersion = osVersion.replace(/[_\s]/g, '.');
} else if (android) { }
else if (android) {
osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i); osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
} else if (result.webos) { }
else if (result.webos) {
osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i); osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
} else if (result.blackberry) { }
else if (result.blackberry) {
osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i); osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
} else if (result.bada) { }
else if (result.bada) {
osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i); osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
} else if (result.tizen) { }
else if (result.tizen) {
osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i); osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
} }
if (osVersion) { if (osVersion) {
result.osversion = osVersion; result.osversion = osVersion;
} }
// device type extraction // device type extraction
var osMajorVersion = osVersion.split('.')[0]; var osMajorVersion = osVersion.split('.')[0];
if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) { if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
result.tablet = t result.tablet = t;
} else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) { }
result.mobile = t else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
result.mobile = t;
} }
// Graded Browser Support // Graded Browser Support
// http://developer.yahoo.com/yui/articles/gbs // http://developer.yahoo.com/yui/articles/gbs
if (result.msedge || if (result.msedge ||
@ -268,8 +256,7 @@
(result.safari && result.version >= 6) || (result.safari && result.version >= 6) ||
(result.opera && result.version >= 10.0) || (result.opera && result.version >= 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] >= 6) || (result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
(result.blackberry && result.version >= 10.1) (result.blackberry && result.version >= 10.1)) {
) {
result.a = t; result.a = t;
} }
else if ((result.msie && result.version < 10) || else if ((result.msie && result.version < 10) ||
@ -277,16 +264,14 @@
(result.firefox && result.version < 20.0) || (result.firefox && result.version < 20.0) ||
(result.safari && result.version < 6) || (result.safari && result.version < 6) ||
(result.opera && result.version < 10.0) || (result.opera && result.version < 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] < 6) (result.ios && result.osversion && result.osversion.split(".")[0] < 6)) {
) { result.c = t;
result.c = t
} else result.x = t
return result
} }
else
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '') result.x = t;
return result;
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '');
bowser.test = function (browserList) { bowser.test = function (browserList) {
for (var i = 0; i < browserList.length; ++i) { for (var i = 0; i < browserList.length; ++i) {
var browserItem = browserList[i]; var browserItem = browserList[i];
@ -297,14 +282,13 @@
} }
} }
return false; return false;
} };
/* /*
* Set our detect method to the main bowser object so we can * Set our detect method to the main bowser object so we can
* reuse it to test other user agents. * reuse it to test other user agents.
* This is needed to implement future tests. * This is needed to implement future tests.
*/ */
bowser._detect = detect; bowser._detect = detect;
return bowser;
return bowser
}); });
export default definition();

View file

@ -1,6 +1,5 @@
// WARNING: This file has been modified from original. // WARNING: This file has been modified from original.
// TODO: Replace with https://github.com/Simonwep/pickr // TODO: Replace with https://github.com/Simonwep/pickr
// Farbtastic 2.0 alpha // Farbtastic 2.0 alpha
// Original can be found at: // Original can be found at:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js // https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js
@ -8,25 +7,19 @@
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt // https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06 // edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
(function ($) { (function ($) {
var __debug = false; var __debug = false;
var __factor = 1; var __factor = 1;
$.fn.farbtastic = function (options) { $.fn.farbtastic = function (options) {
$.farbtastic(this, options); $.farbtastic(this, options);
return this; return this;
}; };
$.farbtastic = function (container, options) { $.farbtastic = function (container, options) {
var container = $(container)[0]; var container = $(container)[0];
return container.farbtastic || (container.farbtastic = new $._farbtastic(container, options)); return container.farbtastic || (container.farbtastic = new $._farbtastic(container, options));
} };
$._farbtastic = function (container, options) { $._farbtastic = function (container, options) {
var fb = this; var fb = this;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/** /**
* Link to the given element(s) or callback. * Link to the given element(s) or callback.
*/ */
@ -35,10 +28,8 @@ $._farbtastic = function (container, options) {
if (typeof fb.callback == 'object') { if (typeof fb.callback == 'object') {
$(fb.callback).unbind('keyup', fb.updateValue); $(fb.callback).unbind('keyup', fb.updateValue);
} }
// Reset color // Reset color
fb.color = null; fb.color = null;
// Bind callback or elements // Bind callback or elements
if (typeof callback == 'function') { if (typeof callback == 'function') {
fb.callback = callback; fb.callback = callback;
@ -51,13 +42,12 @@ $._farbtastic = function (container, options) {
} }
} }
return this; return this;
} };
fb.updateValue = function (event) { fb.updateValue = function (event) {
if (this.value && this.value != fb.color) { if (this.value && this.value != fb.color) {
fb.setColor(this.value); fb.setColor(this.value);
} }
} };
/** /**
* Change color with HTML syntax #123456 * Change color with HTML syntax #123456
*/ */
@ -70,52 +60,42 @@ $._farbtastic = function (container, options) {
fb.updateDisplay(); fb.updateDisplay();
} }
return this; return this;
} };
/** /**
* Change color with HSL triplet [0..1, 0..1, 0..1] * Change color with HSL triplet [0..1, 0..1, 0..1]
*/ */
fb.setHSL = function (hsl) { fb.setHSL = function (hsl) {
fb.hsl = hsl; fb.hsl = hsl;
var convertedHSL = [hsl[0]];
var convertedHSL = [hsl[0]]
convertedHSL[1] = hsl[1] * __factor + ((1 - __factor) / 2); convertedHSL[1] = hsl[1] * __factor + ((1 - __factor) / 2);
convertedHSL[2] = hsl[2] * __factor + ((1 - __factor) / 2); convertedHSL[2] = hsl[2] * __factor + ((1 - __factor) / 2);
fb.rgb = fb.HSLToRGB(convertedHSL); fb.rgb = fb.HSLToRGB(convertedHSL);
fb.color = fb.pack(fb.rgb); fb.color = fb.pack(fb.rgb);
fb.updateDisplay(); fb.updateDisplay();
return this; return this;
} };
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/** /**
* Initialize the color picker widget. * Initialize the color picker widget.
*/ */
fb.initWidget = function () { fb.initWidget = function () {
// Insert markup and size accordingly. // Insert markup and size accordingly.
var dim = { var dim = {
width: options.width, width: options.width,
height: options.width height: options.width
}; };
$(container) $(container)
.html( .html('<div class="farbtastic" style="position: relative">' +
'<div class="farbtastic" style="position: relative">' +
'<div class="farbtastic-solid"></div>' + '<div class="farbtastic-solid"></div>' +
'<canvas class="farbtastic-mask"></canvas>' + '<canvas class="farbtastic-mask"></canvas>' +
'<canvas class="farbtastic-overlay"></canvas>' + '<canvas class="farbtastic-overlay"></canvas>' +
'</div>' '</div>')
)
.find('*').attr(dim).css(dim).end() .find('*').attr(dim).css(dim).end()
.find('div>*').css('position', 'absolute'); .find('div>*').css('position', 'absolute');
// IE Fix: Recreate canvas elements with doc.createElement and excanvas. // IE Fix: Recreate canvas elements with doc.createElement and excanvas.
browser.msie && $('canvas', container).each(function () { browser.msie && $('canvas', container).each(function () {
// Fetch info. // Fetch info.
var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') }, var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') }, e = document.createElement('canvas');
e = document.createElement('canvas');
// Replace element. // Replace element.
$(this).before($(e).attr(attr)).remove(); $(this).before($(e).attr(attr)).remove();
// Init with explorerCanvas. // Init with explorerCanvas.
@ -124,7 +104,6 @@ $._farbtastic = function (container, options) {
$(e).attr(dim).css(dim).css('position', 'absolute') $(e).attr(dim).css(dim).css('position', 'absolute')
.find('*').attr(dim).css(dim); .find('*').attr(dim).css(dim);
}); });
// Determine layout // Determine layout
fb.radius = (options.width - options.wheelWidth) / 2 - 1; fb.radius = (options.width - options.wheelWidth) / 2 - 1;
fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1; fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1;
@ -136,7 +115,6 @@ $._farbtastic = function (container, options) {
left: fb.mid - fb.square, left: fb.mid - fb.square,
top: fb.mid - fb.square top: fb.mid - fb.square
}); });
// Set up drawing context. // Set up drawing context.
fb.cnvMask = $('.farbtastic-mask', container); fb.cnvMask = $('.farbtastic-mask', container);
fb.ctxMask = fb.cnvMask[0].getContext('2d'); fb.ctxMask = fb.cnvMask[0].getContext('2d');
@ -144,12 +122,10 @@ $._farbtastic = function (container, options) {
fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d'); fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d');
fb.ctxMask.translate(fb.mid, fb.mid); fb.ctxMask.translate(fb.mid, fb.mid);
fb.ctxOverlay.translate(fb.mid, fb.mid); fb.ctxOverlay.translate(fb.mid, fb.mid);
// Draw widget base layers. // Draw widget base layers.
fb.drawCircle(); fb.drawCircle();
fb.drawMask(); fb.drawMask();
} };
/** /**
* Draw the color wheel. * Draw the color wheel.
*/ */
@ -157,19 +133,14 @@ $._farbtastic = function (container, options) {
var tm = +(new Date()); var tm = +(new Date());
// Draw a hue circle with a bunch of gradient-stroked beziers. // Draw a hue circle with a bunch of gradient-stroked beziers.
// Have to use beziers, as gradient-stroked arcs don't work. // Have to use beziers, as gradient-stroked arcs don't work.
var n = 24, var n = 24, r = fb.radius, w = options.wheelWidth, nudge = 8 / r / n * Math.PI, // Fudge factor for seams.
r = fb.radius, m = fb.ctxMask, angle1 = 0, color1, d1;
w = options.wheelWidth,
nudge = 8 / r / n * Math.PI, // Fudge factor for seams.
m = fb.ctxMask,
angle1 = 0, color1, d1;
m.save(); m.save();
m.lineWidth = w / r; m.lineWidth = w / r;
m.scale(r, r); m.scale(r, r);
// Each segment goes from angle1 to angle2. // Each segment goes from angle1 to angle2.
for (var i = 0; i <= n; ++i) { for (var i = 0; i <= n; ++i) {
var d2 = i / n, var d2 = i / n, angle2 = d2 * Math.PI * 2,
angle2 = d2 * Math.PI * 2,
// Endpoints // Endpoints
x1 = Math.sin(angle1), y1 = -Math.cos(angle1); x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
x2 = Math.sin(angle2), y2 = -Math.cos(angle2), x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
@ -213,18 +184,18 @@ $._farbtastic = function (container, options) {
} }
} }
// Prevent seams where curves join. // Prevent seams where curves join.
angle1 = angle2 - nudge; color1 = color2; d1 = d2; angle1 = angle2 - nudge;
color1 = color2;
d1 = d2;
} }
m.restore(); m.restore();
__debug && $('body').append('<div>drawCircle ' + (+(new Date()) - tm) + 'ms'); __debug && $('body').append('<div>drawCircle ' + (+(new Date()) - tm) + 'ms');
}; };
/** /**
* Draw the saturation/luminance mask. * Draw the saturation/luminance mask.
*/ */
fb.drawMask = function () { fb.drawMask = function () {
var tm = +(new Date()); var tm = +(new Date());
// Iterate over sat/lum space and calculate appropriate mask pixel values. // Iterate over sat/lum space and calculate appropriate mask pixel values.
var size = fb.square * 2, sq = fb.square; var size = fb.square * 2, sq = fb.square;
function calculateMask(sizex, sizey, outputPixel) { function calculateMask(sizex, sizey, outputPixel) {
@ -236,15 +207,12 @@ $._farbtastic = function (container, options) {
// From sat/lum to alpha and color (grayscale) // From sat/lum to alpha and color (grayscale)
var a = 1 - 2 * Math.min(l * s, (1 - l) * s); var a = 1 - 2 * Math.min(l * s, (1 - l) * s);
var c = (a > 0) ? ((2 * l - 1 + a) * .5 / a) : 0; var c = (a > 0) ? ((2 * l - 1 + a) * .5 / a) : 0;
a = a * __factor + (1 - __factor) / 2; a = a * __factor + (1 - __factor) / 2;
c = c * __factor + (1 - __factor) / 2; c = c * __factor + (1 - __factor) / 2;
outputPixel(x, y, c, a); outputPixel(x, y, c, a);
} }
} }
} }
// Method #1: direct pixel access (new Canvas). // Method #1: direct pixel access (new Canvas).
if (fb.ctxMask.getImageData) { if (fb.ctxMask.getImageData) {
// Create half-resolution buffer. // Create half-resolution buffer.
@ -253,13 +221,11 @@ $._farbtastic = function (container, options) {
buffer.width = buffer.height = sz + 1; buffer.width = buffer.height = sz + 1;
var ctx = buffer.getContext('2d'); var ctx = buffer.getContext('2d');
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1); var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
var i = 0; var i = 0;
calculateMask(sz, sz, function (x, y, c, a) { calculateMask(sz, sz, function (x, y, c, a) {
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255; frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
frame.data[i++] = a * 255; frame.data[i++] = a * 255;
}); });
ctx.putImageData(frame, 0, 0); ctx.putImageData(frame, 0, 0);
fb.ctxMask.drawImage(buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2); fb.ctxMask.drawImage(buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2);
} }
@ -287,12 +253,7 @@ $._farbtastic = function (container, options) {
a = Math.round(a * 255); a = Math.round(a * 255);
// We can only start outputting gradients once we have two rows of pixels. // We can only start outputting gradients once we have two rows of pixels.
if (y > 0) { if (y > 0) {
var c_last = cache_last[x][0], var c_last = cache_last[x][0], a_last = cache_last[x][1], color1 = fb.packDX(c_last, a_last), color2 = fb.packDX(c, a), y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq), y2 = Math.round(fb.mid + (y * .333 - 1) * sq);
a_last = cache_last[x][1],
color1 = fb.packDX(c_last, a_last),
color2 = fb.packDX(c, a),
y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq),
y2 = Math.round(fb.mid + (y * .333 - 1) * sq);
$('<div>').css({ $('<div>').css({
position: 'absolute', position: 'absolute',
filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr=" + color1 + ", EndColorStr=" + color2 + ", GradientType=0)", filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr=" + color1 + ", EndColorStr=" + color2 + ", GradientType=0)",
@ -307,26 +268,20 @@ $._farbtastic = function (container, options) {
}); });
} }
__debug && $('body').append('<div>drawMask ' + (+(new Date()) - tm) + 'ms'); __debug && $('body').append('<div>drawMask ' + (+(new Date()) - tm) + 'ms');
} };
/** /**
* Draw the selection markers. * Draw the selection markers.
*/ */
fb.drawMarkers = function () { fb.drawMarkers = function () {
// Determine marker dimensions // Determine marker dimensions
var sz = options.width; var sz = options.width;
var angle = fb.hsl[0] * 6.28, var angle = fb.hsl[0] * 6.28, x1 = Math.sin(angle) * fb.radius, y1 = -Math.cos(angle) * fb.radius, x2 = 2 * fb.square * (.5 - fb.hsl[1]), y2 = 2 * fb.square * (.5 - fb.hsl[2]);
x1 = Math.sin(angle) * fb.radius,
y1 = -Math.cos(angle) * fb.radius,
x2 = 2 * fb.square * (.5 - fb.hsl[1]),
y2 = 2 * fb.square * (.5 - fb.hsl[2]);
var circles = [ var circles = [
{ x: x1, y: y1, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 }, { x: x1, y: y1, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 },
{ x: x1, y: y1, r: fb.markerSize, c: '#fff', lw: 2 }, { x: x1, y: y1, r: fb.markerSize, c: '#fff', lw: 2 },
{ x: x2, y: y2, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 }, { x: x2, y: y2, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 },
{ x: x2, y: y2, r: fb.markerSize, c: '#fff', lw: 2 }, { x: x2, y: y2, r: fb.markerSize, c: '#fff', lw: 2 },
]; ];
// Update the overlay canvas. // Update the overlay canvas.
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz); fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
for (i in circles) { for (i in circles) {
@ -337,21 +292,17 @@ $._farbtastic = function (container, options) {
fb.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true); fb.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true);
fb.ctxOverlay.stroke(); fb.ctxOverlay.stroke();
} }
} };
/** /**
* Update the markers and styles * Update the markers and styles
*/ */
fb.updateDisplay = function () { fb.updateDisplay = function () {
// Determine whether labels/markers should invert. // Determine whether labels/markers should invert.
fb.invert = (fb.rgb[0] * 0.3 + fb.rgb[1] * .59 + fb.rgb[2] * .11) <= 0.6; fb.invert = (fb.rgb[0] * 0.3 + fb.rgb[1] * .59 + fb.rgb[2] * .11) <= 0.6;
// Update the solid background fill. // Update the solid background fill.
fb.solidFill.css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5]))); fb.solidFill.css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
// Draw markers // Draw markers
fb.drawMarkers(); fb.drawMarkers();
// Linked elements or callback // Linked elements or callback
if (typeof fb.callback == 'object') { if (typeof fb.callback == 'object') {
// Set background/foreground color // Set background/foreground color
@ -359,7 +310,6 @@ $._farbtastic = function (container, options) {
backgroundColor: fb.color, backgroundColor: fb.color,
color: fb.invert ? '#fff' : '#000' color: fb.invert ? '#fff' : '#000'
}); });
// Change linked value // Change linked value
$(fb.callback).each(function () { $(fb.callback).each(function () {
if ((typeof this.value == 'string') && this.value != fb.color) { if ((typeof this.value == 'string') && this.value != fb.color) {
@ -370,8 +320,7 @@ $._farbtastic = function (container, options) {
else if (typeof fb.callback == 'function') { else if (typeof fb.callback == 'function') {
fb.callback.call(fb, fb.color); fb.callback.call(fb, fb.color);
} }
} };
/** /**
* Helper for returning coordinates relative to the center. * Helper for returning coordinates relative to the center.
*/ */
@ -380,8 +329,7 @@ $._farbtastic = function (container, options) {
x: event.pageX - fb.offset.left - fb.mid, x: event.pageX - fb.offset.left - fb.mid,
y: event.pageY - fb.offset.top - fb.mid y: event.pageY - fb.offset.top - fb.mid
}; };
} };
/** /**
* Mousedown handler * Mousedown handler
*/ */
@ -391,26 +339,21 @@ $._farbtastic = function (container, options) {
$(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup); $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
$._farbtastic.dragging = true; $._farbtastic.dragging = true;
} }
// Update the stored offset for the widget. // Update the stored offset for the widget.
fb.offset = $(container).offset(); fb.offset = $(container).offset();
// Check which area is being dragged // Check which area is being dragged
var pos = fb.widgetCoords(event); var pos = fb.widgetCoords(event);
fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > (fb.square + 2); fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > (fb.square + 2);
// Process // Process
fb.mousemove(event); fb.mousemove(event);
return false; return false;
} };
/** /**
* Mousemove handler * Mousemove handler
*/ */
fb.mousemove = function (event) { fb.mousemove = function (event) {
// Get coordinates relative to color picker center // Get coordinates relative to color picker center
var pos = fb.widgetCoords(event); var pos = fb.widgetCoords(event);
// Set new HSL parameters // Set new HSL parameters
if (fb.circleDrag) { if (fb.circleDrag) {
var hue = Math.atan2(pos.x, -pos.y) / 6.28; var hue = Math.atan2(pos.x, -pos.y) / 6.28;
@ -422,8 +365,7 @@ $._farbtastic = function (container, options) {
fb.setHSL([fb.hsl[0], sat, lum]); fb.setHSL([fb.hsl[0], sat, lum]);
} }
return false; return false;
} };
/** /**
* Mouseup handler * Mouseup handler
*/ */
@ -432,24 +374,20 @@ $._farbtastic = function (container, options) {
$(document).unbind('mousemove', fb.mousemove); $(document).unbind('mousemove', fb.mousemove);
$(document).unbind('mouseup', fb.mouseup); $(document).unbind('mouseup', fb.mouseup);
$._farbtastic.dragging = false; $._farbtastic.dragging = false;
} };
/* Various color utility functions */ /* Various color utility functions */
fb.dec2hex = function (x) { fb.dec2hex = function (x) {
return (x < 16 ? '0' : '') + x.toString(16); return (x < 16 ? '0' : '') + x.toString(16);
} };
fb.packDX = function (c, a) { fb.packDX = function (c, a) {
return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c); return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c);
}; };
fb.pack = function (rgb) { fb.pack = function (rgb) {
var r = Math.round(rgb[0] * 255); var r = Math.round(rgb[0] * 255);
var g = Math.round(rgb[1] * 255); var g = Math.round(rgb[1] * 255);
var b = Math.round(rgb[2] * 255); var b = Math.round(rgb[2] * 255);
return '#' + fb.dec2hex(r) + fb.dec2hex(g) + fb.dec2hex(b); return '#' + fb.dec2hex(r) + fb.dec2hex(g) + fb.dec2hex(b);
}; };
fb.unpack = function (color) { fb.unpack = function (color) {
if (color.length == 7) { if (color.length == 7) {
function x(i) { function x(i) {
@ -464,7 +402,6 @@ $._farbtastic = function (container, options) {
return [x(1), x(2), x(3)]; return [x(1), x(2), x(3)];
} }
}; };
fb.HSLToRGB = function (hsl) { fb.HSLToRGB = function (hsl) {
var m1, m2, r, g, b; var m1, m2, r, g, b;
var h = hsl[0], s = hsl[1], l = hsl[2]; var h = hsl[0], s = hsl[1], l = hsl[2];
@ -476,35 +413,32 @@ $._farbtastic = function (container, options) {
this.hueToRGB(m1, m2, h - 0.33333) this.hueToRGB(m1, m2, h - 0.33333)
]; ];
}; };
fb.hueToRGB = function (m1, m2, h) { fb.hueToRGB = function (m1, m2, h) {
h = (h + 1) % 1; h = (h + 1) % 1;
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; if (h * 6 < 1)
if (h * 2 < 1) return m2; return m1 + (m2 - m1) * h * 6;
if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6; if (h * 2 < 1)
return m2;
if (h * 3 < 2)
return m1 + (m2 - m1) * (0.66666 - h) * 6;
return m1; return m1;
}; };
fb.RGBToHSL = function (rgb) { fb.RGBToHSL = function (rgb) {
var r = rgb[0], g = rgb[1], b = rgb[2], var r = rgb[0], g = rgb[1], b = rgb[2], min = Math.min(r, g, b), max = Math.max(r, g, b), delta = max - min, h = 0, s = 0, l = (min + max) / 2;
min = Math.min(r, g, b),
max = Math.max(r, g, b),
delta = max - min,
h = 0,
s = 0,
l = (min + max) / 2;
if (l > 0 && l < 1) { if (l > 0 && l < 1) {
s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l)); s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
} }
if (delta > 0) { if (delta > 0) {
if (max == r && max != g) h += (g - b) / delta; if (max == r && max != g)
if (max == g && max != b) h += (2 + (b - r) / delta); h += (g - b) / delta;
if (max == b && max != r) h += (4 + (r - g) / delta); if (max == g && max != b)
h += (2 + (b - r) / delta);
if (max == b && max != r)
h += (4 + (r - g) / delta);
h /= 6; h /= 6;
} }
return [h, s, l]; return [h, s, l];
}; };
// Parse options. // Parse options.
if (!options.callback) { if (!options.callback) {
options = { callback: options }; options = { callback: options };
@ -514,19 +448,15 @@ $._farbtastic = function (container, options) {
wheelWidth: (options.width || 300) / 10, wheelWidth: (options.width || 300) / 10,
callback: null callback: null
}, options); }, options);
// Initialize. // Initialize.
fb.initWidget(); fb.initWidget();
// Install mousedown handler (the others are set on the document on-demand) // Install mousedown handler (the others are set on the document on-demand)
$('canvas.farbtastic-overlay', container).mousedown(fb.mousedown); $('canvas.farbtastic-overlay', container).mousedown(fb.mousedown);
// Set linked elements/callback // Set linked elements/callback
if (options.callback) { if (options.callback) {
fb.linkTo(options.callback); fb.linkTo(options.callback);
} }
// Set to gray. // Set to gray.
fb.setColor('#808080'); fb.setColor('#808080');
} };
})(jQuery); })(jQuery);

View file

@ -1,5 +1,4 @@
// WARNING: This file has been modified from the Original // WARNING: This file has been modified from the Original
/* /*
* Gritter for jQuery * Gritter for jQuery
* http://www.boedesign.com/ * http://www.boedesign.com/
@ -16,66 +15,55 @@
* notification title and text, and to treat plain strings as text instead of HTML (to avoid XSS * notification title and text, and to treat plain strings as text instead of HTML (to avoid XSS
* vunlerabilities). * vunlerabilities).
*/ */
(function ($) { (function ($) {
/** /**
* Set it up as an object under the jQuery namespace * Set it up as an object under the jQuery namespace
*/ */
$.gritter = {}; $.gritter = {};
/** /**
* Set up global options that the user can over-ride * Set up global options that the user can over-ride
*/ */
$.gritter.options = { $.gritter.options = {
position: '', position: '',
class_name: '', // could be set to 'gritter-light' to use white notifications class_name: '',
time: 3000 // hang on the screen for... time: 3000 // hang on the screen for...
} };
/** /**
* Add a gritter notification to the screen * Add a gritter notification to the screen
* @see Gritter#add(); * @see Gritter#add();
*/ */
$.gritter.add = function (params) { $.gritter.add = function (params) {
try { try {
return Gritter.add(params || {}); return Gritter.add(params || {});
} catch(e) { }
catch (e) {
var err = 'Gritter Error: ' + e; var err = 'Gritter Error: ' + e;
(typeof (console) != 'undefined' && console.error) ? (typeof (console) != 'undefined' && console.error) ?
console.error(err, params) : console.error(err, params) :
alert(err); alert(err);
} }
};
}
/** /**
* Remove a gritter notification from the screen * Remove a gritter notification from the screen
* @see Gritter#removeSpecific(); * @see Gritter#removeSpecific();
*/ */
$.gritter.remove = function (id, params) { $.gritter.remove = function (id, params) {
Gritter.removeSpecific(id.split('gritter-item-')[1], params || {}); Gritter.removeSpecific(id.split('gritter-item-')[1], params || {});
} };
/** /**
* Remove all notifications * Remove all notifications
* @see Gritter#stop(); * @see Gritter#stop();
*/ */
$.gritter.removeAll = function (params) { $.gritter.removeAll = function (params) {
Gritter.stop(params || {}); Gritter.stop(params || {});
} };
/** /**
* Big fat Gritter object * Big fat Gritter object
* @constructor (not really since its object literal) * @constructor (not really since its object literal)
*/ */
var Gritter = { var Gritter = {
// Public - options to over-ride with $.gritter.options in "add" // Public - options to over-ride with $.gritter.options in "add"
time: '', time: '',
// Private - no touchy the private parts // Private - no touchy the private parts
_custom_timer: 0, _custom_timer: 0,
_item_count: 0, _item_count: 0,
@ -89,8 +77,6 @@
.append($('<div>').addClass('gritter-content')) .append($('<div>').addClass('gritter-content'))
.append($('<div>').addClass('gritter-close') .append($('<div>').addClass('gritter-close')
.append($('<i>').addClass('buttonicon buttonicon-times'))))), .append($('<i>').addClass('buttonicon buttonicon-times'))))),
/** /**
* Add a gritter notification to the screen * Add a gritter notification to the screen
* @param {Object} params The object that contains all the options for drawing the notification * @param {Object} params The object that contains all the options for drawing the notification
@ -101,79 +87,56 @@
if (typeof (params) == 'string') { if (typeof (params) == 'string') {
params = { text: params }; params = { text: params };
} }
// We might have some issues if we don't have a title or text! // We might have some issues if we don't have a title or text!
if (!params.text) { if (!params.text) {
throw 'You must supply "text" parameter.'; throw 'You must supply "text" parameter.';
} }
// Check the options and set them once // Check the options and set them once
if (!this._is_setup) { if (!this._is_setup) {
this._runSetup(); this._runSetup();
} }
// Basics // Basics
var title = params.title, var title = params.title, text = params.text, image = params.image || '', position = params.position || 'top', sticky = params.sticky || false, item_class = params.class_name || $.gritter.options.class_name, time_alive = params.time || '';
text = params.text,
image = params.image || '',
position = params.position || 'top',
sticky = params.sticky || false,
item_class = params.class_name || $.gritter.options.class_name,
time_alive = params.time || '';
this._verifyWrapper(); this._verifyWrapper();
if (sticky) { if (sticky) {
item_class += " sticky"; item_class += " sticky";
} }
this._item_count++; this._item_count++;
var number = this._item_count; var number = this._item_count;
// Assign callbacks // Assign callbacks
$(['before_open', 'after_open', 'before_close', 'after_close']).each(function (i, val) { $(['before_open', 'after_open', 'before_close', 'after_close']).each(function (i, val) {
Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){} Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function () { };
}); });
// Reset // Reset
this._custom_timer = 0; this._custom_timer = 0;
// A custom fade time set // A custom fade time set
if (time_alive) { if (time_alive) {
this._custom_timer = time_alive; this._custom_timer = time_alive;
} }
// String replacements on the template // String replacements on the template
if (title) { if (title) {
title = this._tpl_title.clone().append( title = this._tpl_title.clone().append(typeof title === 'string' ? document.createTextNode(title) : title);
typeof title === 'string' ? document.createTextNode(title) : title); }
}else{ else {
title = ''; title = '';
} }
const tmp = this._tpl_item.clone(); const tmp = this._tpl_item.clone();
tmp.attr('id', `gritter-item-${number}`); tmp.attr('id', `gritter-item-${number}`);
tmp.addClass(item_class); tmp.addClass(item_class);
tmp.find('.gritter-content') tmp.find('.gritter-content')
.append(title) .append(title)
.append(typeof text === 'string' ? $('<p>').text(text) : text); .append(typeof text === 'string' ? $('<p>').text(text) : text);
// If it's false, don't show another gritter message // If it's false, don't show another gritter message
if (this['_before_open_' + number]() === false) { if (this['_before_open_' + number]() === false) {
return false; return false;
} }
if (['top', 'bottom'].indexOf(position) == -1) { if (['top', 'bottom'].indexOf(position) == -1) {
position = 'top'; position = 'top';
} }
$('#gritter-container.' + position).append(tmp); $('#gritter-container.' + position).append(tmp);
var item = $('#gritter-item-' + this._item_count); var item = $('#gritter-item-' + this._item_count);
setTimeout(function () { item.addClass('popup-show'); }, 0); setTimeout(function () { item.addClass('popup-show'); }, 0);
Gritter['_after_open_' + number](item); Gritter['_after_open_' + number](item);
if (!sticky) { if (!sticky) {
this._setFadeTimer(item, number); this._setFadeTimer(item, number);
// Bind the hover/unhover states // Bind the hover/unhover states
@ -184,16 +147,12 @@
Gritter._setFadeTimer($(this), number); Gritter._setFadeTimer($(this), number);
}); });
} }
// Clicking (X) makes the perdy thing close // Clicking (X) makes the perdy thing close
$(item).find('.gritter-close').click(function () { $(item).find('.gritter-close').click(function () {
Gritter.removeSpecific(number, {}, null, true); Gritter.removeSpecific(number, {}, null, true);
}); });
return number; return number;
}, },
/** /**
* If we don't have any more gritter notifications, get rid of the wrapper using this check * If we don't have any more gritter notifications, get rid of the wrapper using this check
* @private * @private
@ -202,19 +161,16 @@
* @param {Boolean} manual_close Did we close the gritter dialog with the (X) button * @param {Boolean} manual_close Did we close the gritter dialog with the (X) button
*/ */
_countRemoveWrapper: function (unique_id, e, manual_close) { _countRemoveWrapper: function (unique_id, e, manual_close) {
// Remove it then run the callback function // Remove it then run the callback function
e.remove(); e.remove();
this['_after_close_' + unique_id](e, manual_close); this['_after_close_' + unique_id](e, manual_close);
// Remove container if empty // Remove container if empty
$('#gritter-container').each(function () { $('#gritter-container').each(function () {
if ($(this).find('.gritter-item').length == 0) { if ($(this).find('.gritter-item').length == 0) {
$(this).remove(); $(this).remove();
} }
}) });
}, },
/** /**
* Fade out an element after it's been on the screen for x amount of time * Fade out an element after it's been on the screen for x amount of time
* @private * @private
@ -224,33 +180,23 @@
* @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X) * @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X)
*/ */
_fade: function (e, unique_id, params, unbind_events) { _fade: function (e, unique_id, params, unbind_events) {
var params = params || {}, fade = (typeof (params.fade) != 'undefined') ? params.fade : true, manual_close = unbind_events;
var params = params || {},
fade = (typeof(params.fade) != 'undefined') ? params.fade : true,
manual_close = unbind_events;
this['_before_close_' + unique_id](e, manual_close); this['_before_close_' + unique_id](e, manual_close);
// If this is true, then we are coming from clicking the (X) // If this is true, then we are coming from clicking the (X)
if (unbind_events) { if (unbind_events) {
e.unbind('mouseenter mouseleave'); e.unbind('mouseenter mouseleave');
} }
// Fade it out or remove it // Fade it out or remove it
if (fade) { if (fade) {
e.removeClass('popup-show'); e.removeClass('popup-show');
setTimeout(function () { setTimeout(function () {
Gritter._countRemoveWrapper(unique_id, e, manual_close); Gritter._countRemoveWrapper(unique_id, e, manual_close);
}, 300) }, 300);
} }
else { else {
this._countRemoveWrapper(unique_id, e); this._countRemoveWrapper(unique_id, e);
} }
}, },
/** /**
* Remove a specific notification based on an ID * Remove a specific notification based on an ID
* @param {Integer} unique_id The ID used to delete a specific notification * @param {Integer} unique_id The ID used to delete a specific notification
@ -259,17 +205,13 @@
* @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave * @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave
*/ */
removeSpecific: function (unique_id, params, e, unbind_events) { removeSpecific: function (unique_id, params, e, unbind_events) {
if (!e) { if (!e) {
var e = $('#gritter-item-' + unique_id); var e = $('#gritter-item-' + unique_id);
} }
// We set the fourth param to let the _fade function know to // We set the fourth param to let the _fade function know to
// unbind the "mouseleave" event. Once you click (X) there's no going back! // unbind the "mouseleave" event. Once you click (X) there's no going back!
this._fade(e, unique_id, params || {}, unbind_events); this._fade(e, unique_id, params || {}, unbind_events);
}, },
/** /**
* If the item is fading out and we hover over it, restore it! * If the item is fading out and we hover over it, restore it!
* @private * @private
@ -277,25 +219,19 @@
* @param {Integer} unique_id The ID of the element * @param {Integer} unique_id The ID of the element
*/ */
_restoreItemIfFading: function (e, unique_id) { _restoreItemIfFading: function (e, unique_id) {
clearTimeout(this['_int_id_' + unique_id]); clearTimeout(this['_int_id_' + unique_id]);
e.stop().css({ opacity: '', height: '' }); e.stop().css({ opacity: '', height: '' });
}, },
/** /**
* Setup the global options - only once * Setup the global options - only once
* @private * @private
*/ */
_runSetup: function () { _runSetup: function () {
for (opt in $.gritter.options) { for (opt in $.gritter.options) {
this[opt] = $.gritter.options[opt]; this[opt] = $.gritter.options[opt];
} }
this._is_setup = 1; this._is_setup = 1;
}, },
/** /**
* Set the notification to fade out after a certain amount of time * Set the notification to fade out after a certain amount of time
* @private * @private
@ -303,33 +239,26 @@
* @param {Integer} unique_id The ID of the element * @param {Integer} unique_id The ID of the element
*/ */
_setFadeTimer: function (item, unique_id) { _setFadeTimer: function (item, unique_id) {
var timer_str = (this._custom_timer) ? this._custom_timer : this.time; var timer_str = (this._custom_timer) ? this._custom_timer : this.time;
this['_int_id_' + unique_id] = setTimeout(function () { this['_int_id_' + unique_id] = setTimeout(function () {
Gritter._fade(item, unique_id); Gritter._fade(item, unique_id);
}, timer_str); }, timer_str);
}, },
/** /**
* Bring everything to a halt * Bring everything to a halt
* @param {Object} params A list of callback functions to pass when all notifications are removed * @param {Object} params A list of callback functions to pass when all notifications are removed
*/ */
stop: function (params) { stop: function (params) {
// callbacks (if passed) // callbacks (if passed)
var before_close = ($.isFunction(params.before_close)) ? params.before_close : function () { }; var before_close = ($.isFunction(params.before_close)) ? params.before_close : function () { };
var after_close = ($.isFunction(params.after_close)) ? params.after_close : function () { }; var after_close = ($.isFunction(params.after_close)) ? params.after_close : function () { };
var wrap = $('#gritter-container'); var wrap = $('#gritter-container');
before_close(wrap); before_close(wrap);
wrap.fadeOut(function () { wrap.fadeOut(function () {
$(this).remove(); $(this).remove();
after_close(); after_close();
}); });
}, },
/** /**
* A check to make sure we have something to wrap our notices with * A check to make sure we have something to wrap our notices with
* @private * @private
@ -338,20 +267,15 @@
if ($('#gritter-container.top').length === 0) { if ($('#gritter-container.top').length === 0) {
$('#editorcontainerbox').append(this._tpl_wrap_top); $('#editorcontainerbox').append(this._tpl_wrap_top);
} }
if ($('#gritter-container.bottom').length === 0) { if ($('#gritter-container.bottom').length === 0) {
$('#editorcontainerbox').append(this._tpl_wrap_bottom); $('#editorcontainerbox').append(this._tpl_wrap_bottom);
} }
} }
};
}
})(jQuery); })(jQuery);
// For Emacs: // For Emacs:
// Local Variables: // Local Variables:
// tab-width: 2 // tab-width: 2
// indent-tabs-mode: t // indent-tabs-mode: t
// End: // End:
// vi: ts=2:noet:sw=2 // vi: ts=2:noet:sw=2

View file

@ -1,5 +1,4 @@
// WARNING: This file has been modified from the Original // WARNING: This file has been modified from the Original
/** /**
* Copyright (c) 2012 Marcel Klehr * Copyright (c) 2012 Marcel Klehr
* Copyright (c) 2011-2012 Fabien Cazenave, Mozilla * Copyright (c) 2011-2012 Fabien Cazenave, Mozilla
@ -23,7 +22,6 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
window.html10n = (function (window, document, undefined) { window.html10n = (function (window, document, undefined) {
// fix console // fix console
(function () { (function () {
var noop = function () { }; var noop = function () { };
@ -35,7 +33,6 @@ window.html10n = (function(window, document, undefined) {
} }
} }
}()); }());
// fix Array#forEach in IE // fix Array#forEach in IE
// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach) { if (!Array.prototype.forEach) {
@ -47,7 +44,6 @@ window.html10n = (function(window, document, undefined) {
} }
}; };
} }
// fix Array#indexOf in, guess what, IE! <3 // fix Array#indexOf in, guess what, IE! <3
// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
if (!Array.prototype.indexOf) { if (!Array.prototype.indexOf) {
@ -66,7 +62,8 @@ window.html10n = (function(window, document, undefined) {
n = Number(arguments[1]); n = Number(arguments[1]);
if (n != n) { // shortcut for verifying if it's NaN if (n != n) { // shortcut for verifying if it's NaN
n = 0; n = 0;
} else if (n != 0 && n != Infinity && n != -Infinity) { }
else if (n != 0 && n != Infinity && n != -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n)); n = (n > 0 || -1) * Math.floor(Math.abs(n));
} }
} }
@ -80,14 +77,12 @@ window.html10n = (function(window, document, undefined) {
} }
} }
return -1; return -1;
};
} }
}
/** /**
* MicroEvent - to make any js object an event emitter (server or browser) * MicroEvent - to make any js object an event emitter (server or browser)
*/ */
var MicroEvent = function () { };
var MicroEvent = function(){}
MicroEvent.prototype = { MicroEvent.prototype = {
bind: function (event, fct) { bind: function (event, fct) {
this._events = this._events || {}; this._events = this._events || {};
@ -96,14 +91,16 @@ window.html10n = (function(window, document, undefined) {
}, },
unbind: function (event, fct) { unbind: function (event, fct) {
this._events = this._events || {}; this._events = this._events || {};
if( event in this._events === false ) return; if (event in this._events === false)
return;
this._events[event].splice(this._events[event].indexOf(fct), 1); this._events[event].splice(this._events[event].indexOf(fct), 1);
}, },
trigger: function (event /* , args... */) { trigger: function (event /* , args... */) {
this._events = this._events || {}; this._events = this._events || {};
if( event in this._events === false ) return; if (event in this._events === false)
return;
for (var i = 0; i < this._events[event].length; i++) { for (var i = 0; i < this._events[event].length; i++) {
this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)) this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
} }
} }
}; };
@ -113,93 +110,86 @@ window.html10n = (function(window, document, undefined) {
*/ */
MicroEvent.mixin = function (destObject) { MicroEvent.mixin = function (destObject) {
var props = ['bind', 'unbind', 'trigger']; var props = ['bind', 'unbind', 'trigger'];
if(!destObject) return; if (!destObject)
return;
for (var i = 0; i < props.length; i++) { for (var i = 0; i < props.length; i++) {
destObject[props[i]] = MicroEvent.prototype[props[i]]; destObject[props[i]] = MicroEvent.prototype[props[i]];
} }
} };
/** /**
* Loader * Loader
* The loader is responsible for loading * The loader is responsible for loading
* and caching all necessary resources * and caching all necessary resources
*/ */
function Loader(resources) { function Loader(resources) {
this.resources = resources this.resources = resources;
this.cache = {} // file => contents this.cache = {}; // file => contents
this.langs = {} // lang => strings this.langs = {}; // lang => strings
} }
Loader.prototype.load = function (lang, cb) { Loader.prototype.load = function (lang, cb) {
if(this.langs[lang]) return cb() if (this.langs[lang])
return cb();
if (this.resources.length > 0) { if (this.resources.length > 0) {
var reqs = 0; var reqs = 0;
for (var i = 0, n = this.resources.length; i < n; i++) { for (var i = 0, n = this.resources.length; i < n; i++) {
this.fetch(this.resources[i], lang, function (e) { this.fetch(this.resources[i], lang, function (e) {
reqs++; reqs++;
if(e) console.warn(e) if (e)
console.warn(e);
if (reqs < n) return;// Call back once all reqs are completed if (reqs < n)
cb && cb() return; // Call back once all reqs are completed
}) cb && cb();
});
} }
} }
} };
Loader.prototype.fetch = function (href, lang, cb) { Loader.prototype.fetch = function (href, lang, cb) {
var that = this var that = this;
if (this.cache[href]) { if (this.cache[href]) {
this.parse(lang, href, this.cache[href], cb) this.parse(lang, href, this.cache[href], cb);
return; return;
} }
var xhr = new XMLHttpRequest();
var xhr = new XMLHttpRequest() xhr.open('GET', href, /*async: */ true);
xhr.open('GET', href, /*async: */true)
if (xhr.overrideMimeType) { if (xhr.overrideMimeType) {
xhr.overrideMimeType('application/json; charset=utf-8'); xhr.overrideMimeType('application/json; charset=utf-8');
} }
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) { if (xhr.status == 200 || xhr.status === 0) {
var data = JSON.parse(xhr.responseText) var data = JSON.parse(xhr.responseText);
that.cache[href] = data that.cache[href] = data;
// Pass on the contents for parsing // Pass on the contents for parsing
that.parse(lang, href, data, cb) that.parse(lang, href, data, cb);
} else { }
cb(new Error('Failed to load '+href)) else {
cb(new Error('Failed to load ' + href));
} }
} }
}; };
xhr.send(null); xhr.send(null);
} };
Loader.prototype.parse = function (lang, currHref, data, cb) { Loader.prototype.parse = function (lang, currHref, data, cb) {
if ('object' != typeof data) { if ('object' != typeof data) {
cb(new Error('A file couldn\'t be parsed as json.')) cb(new Error('A file couldn\'t be parsed as json.'));
return return;
} }
// Check if lang exists // Check if lang exists
if (!data[lang]) { if (!data[lang]) {
// lang not found // lang not found
// This may be due to formatting (expected 'ru' but browser sent 'ru-RU') // This may be due to formatting (expected 'ru' but browser sent 'ru-RU')
// Set err msg before mutating lang (we may need this later) // Set err msg before mutating lang (we may need this later)
var msg = 'Couldn\'t find translations for ' + lang; var msg = 'Couldn\'t find translations for ' + lang;
// Check for '-' ('ROOT-VARIANT') // Check for '-' ('ROOT-VARIANT')
if (lang.indexOf('-') > -1) { if (lang.indexOf('-') > -1) {
// ROOT-VARIANT formatting detected // ROOT-VARIANT formatting detected
lang = lang.split('-')[0]; // set lang to ROOT lang lang = lang.split('-')[0]; // set lang to ROOT lang
} }
// Check if ROOT lang exists (e.g 'ru') // Check if ROOT lang exists (e.g 'ru')
if (!data[lang]) { if (!data[lang]) {
// ROOT lang not found. (e.g 'zh') // ROOT lang not found. (e.g 'zh')
// Loop through langs data. Maybe we have a variant? e.g (zh-hans) // Loop through langs data. Maybe we have a variant? e.g (zh-hans)
var l; // langs item. Declare outside of loop var l; // langs item. Declare outside of loop
for (l in data) { for (l in data) {
// Is not ROOT? // Is not ROOT?
// And index of ROOT equals 0? // And index of ROOT equals 0?
@ -209,52 +199,39 @@ window.html10n = (function(window, document, undefined) {
break; break;
} }
} }
// Did we find a variant? If not, return err. // Did we find a variant? If not, return err.
if (lang != l) { if (lang != l) {
return cb(new Error(msg)); return cb(new Error(msg));
} }
} }
} }
if ('string' == typeof data[lang]) { if ('string' == typeof data[lang]) {
// Import rule // Import rule
// absolute path // absolute path
var importUrl = data[lang] var importUrl = data[lang];
// relative path // relative path
if (data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) { if (data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
importUrl = currHref+"/../"+data[lang] importUrl = currHref + "/../" + data[lang];
} }
this.fetch(importUrl, lang, cb);
this.fetch(importUrl, lang, cb) return;
return
} }
if ('object' != typeof data[lang]) { if ('object' != typeof data[lang]) {
cb(new Error('Translations should be specified as JSON objects!')) cb(new Error('Translations should be specified as JSON objects!'));
return return;
} }
this.langs[lang] = data[lang];
this.langs[lang] = data[lang]
// TODO: Also store accompanying langs // TODO: Also store accompanying langs
cb() cb();
} };
/** /**
* The html10n object * The html10n object
*/ */
var html10n = var html10n = { language: null
{ language : null };
} MicroEvent.mixin(html10n);
MicroEvent.mixin(html10n) html10n.macros = {};
html10n.rtl = ["ar", "dv", "fa", "ha", "he", "ks", "ku", "ps", "ur", "yi"];
html10n.macros = {}
html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
/** /**
* Get rules for plural forms (shared with JetPack), see: * Get rules for plural forms (shared with JetPack), see:
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
@ -443,7 +420,6 @@ window.html10n = (function(window, document, undefined) {
'zh': 0, 'zh': 0,
'zu': 3 'zu': 3
}; };
// utility functions for plural rules methods // utility functions for plural rules methods
function isIn(n, list) { function isIn(n, list) {
return list.indexOf(n) !== -1; return list.indexOf(n) !== -1;
@ -451,7 +427,6 @@ window.html10n = (function(window, document, undefined) {
function isBetween(n, start, end) { function isBetween(n, start, end) {
return start <= n && n <= end; return start <= n && n <= end;
} }
// list of all plural rules methods: // list of all plural rules methods:
// map an integer to the plural form name to use // map an integer to the plural form name to use
var pluralRules = { var pluralRules = {
@ -614,11 +589,9 @@ window.html10n = (function(window, document, undefined) {
return 'other'; return 'other';
}, },
'20': function (n) { '20': function (n) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !( if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 70, 79) || isBetween((n % 100), 70, 79) ||
isBetween((n % 100), 90, 99) isBetween((n % 100), 90, 99)))
))
return 'few'; return 'few';
if ((n % 1000000) === 0 && n !== 0) if ((n % 1000000) === 0 && n !== 0)
return 'many'; return 'many';
@ -655,7 +628,6 @@ window.html10n = (function(window, document, undefined) {
return 'other'; return 'other';
} }
}; };
// return a function that gives the plural form name for a given integer // return a function that gives the plural form name for a given integer
var index = locales2rules[lang.replace(/-.*$/, '')]; var index = locales2rules[lang.replace(/-.*$/, '')];
if (!(index in pluralRules)) { if (!(index in pluralRules)) {
@ -664,59 +636,56 @@ window.html10n = (function(window, document, undefined) {
} }
return pluralRules[index]; return pluralRules[index];
} }
/** /**
* pre-defined 'plural' macro * pre-defined 'plural' macro
*/ */
html10n.macros.plural = function (key, param, opts) { html10n.macros.plural = function (key, param, opts) {
var str var str, n = parseFloat(param);
, n = parseFloat(param);
if (isNaN(n)) if (isNaN(n))
return; return;
// initialize _pluralRules // initialize _pluralRules
if (!this._pluralRules) if (!this._pluralRules)
this._pluralRules = getPluralRules(html10n.language); this._pluralRules = getPluralRules(html10n.language);
var index = this._pluralRules(n); var index = this._pluralRules(n);
// try to find a [zero|one|two] key if it's defined // try to find a [zero|one|two] key if it's defined
if (n === 0 && ('zero') in opts) { if (n === 0 && ('zero') in opts) {
str = opts['zero']; str = opts['zero'];
} else if (n == 1 && ('one') in opts) { }
else if (n == 1 && ('one') in opts) {
str = opts['one']; str = opts['one'];
} else if (n == 2 && ('two') in opts) { }
else if (n == 2 && ('two') in opts) {
str = opts['two']; str = opts['two'];
} else if (index in opts) { }
else if (index in opts) {
str = opts[index]; str = opts[index];
} }
return str; return str;
}; };
/** /**
* Localize a document * Localize a document
* @param langs An array of lang codes defining fallbacks * @param langs An array of lang codes defining fallbacks
*/ */
html10n.localize = function (langs) { html10n.localize = function (langs) {
var that = this var that = this;
// if only one string => create an array // if only one string => create an array
if ('string' == typeof langs) langs = [langs] if ('string' == typeof langs)
langs = [langs];
// Expand two-part locale specs // Expand two-part locale specs
var i=0 var i = 0;
langs.forEach(function (lang) { langs.forEach(function (lang) {
if(!lang) return; if (!lang)
return;
langs[i++] = lang; langs[i++] = lang;
if(~lang.indexOf('-')) langs[i++] = lang.substr(0, lang.indexOf('-')); if (~lang.indexOf('-'))
}) langs[i++] = lang.substr(0, lang.indexOf('-'));
});
this.build(langs, function (er, translations) { this.build(langs, function (er, translations) {
html10n.translations = translations html10n.translations = translations;
html10n.translateElement(translations) html10n.translateElement(translations);
that.trigger('localized') that.trigger('localized');
}) });
} };
/** /**
* Triggers the translation process * Triggers the translation process
* for an element * for an element
@ -724,283 +693,251 @@ window.html10n = (function(window, document, undefined) {
* @param element A DOM element, if omitted, the document element will be used * @param element A DOM element, if omitted, the document element will be used
*/ */
html10n.translateElement = function (translations, element) { html10n.translateElement = function (translations, element) {
element = element || document.documentElement element = element || document.documentElement;
var children = element ? getTranslatableChildren(element) : document.childNodes; var children = element ? getTranslatableChildren(element) : document.childNodes;
for (var i = 0, n = children.length; i < n; i++) { for (var i = 0, n = children.length; i < n; i++) {
this.translateNode(translations, children[i]) this.translateNode(translations, children[i]);
} }
// translate element itself if necessary // translate element itself if necessary
this.translateNode(translations, element) this.translateNode(translations, element);
} };
function asyncForEach(list, iterator, cb) { function asyncForEach(list, iterator, cb) {
var i = 0 var i = 0, n = list.length;
, n = list.length
iterator(list[i], i, function each(err) { iterator(list[i], i, function each(err) {
if(err) console.error(err) if (err)
i++ console.error(err);
if (i < n) return iterator(list[i],i, each); i++;
cb() if (i < n)
}) return iterator(list[i], i, each);
cb();
});
} }
function getTranslatableChildren(element) { function getTranslatableChildren(element) {
if (!document.querySelectorAll) { if (!document.querySelectorAll) {
if (!element) return [] if (!element)
var nodes = element.getElementsByTagName('*') return [];
, l10nElements = [] var nodes = element.getElementsByTagName('*'), l10nElements = [];
for (var i = 0, n = nodes.length; i < n; i++) { for (var i = 0, n = nodes.length; i < n; i++) {
if (nodes[i].getAttribute('data-l10n-id')) if (nodes[i].getAttribute('data-l10n-id'))
l10nElements.push(nodes[i]); l10nElements.push(nodes[i]);
} }
return l10nElements return l10nElements;
} }
return element.querySelectorAll('*[data-l10n-id]') return element.querySelectorAll('*[data-l10n-id]');
} }
html10n.get = function (id, args) { html10n.get = function (id, args) {
var translations = html10n.translations var translations = html10n.translations;
if(!translations) return console.warn('No translations available (yet)') if (!translations)
if(!translations[id]) return console.warn('Could not find string '+id) return console.warn('No translations available (yet)');
if (!translations[id])
return console.warn('Could not find string ' + id);
// apply macros // apply macros
var str = translations[id] var str = translations[id];
str = substMacros(id, str, args);
str = substMacros(id, str, args)
// apply args // apply args
str = substArguments(str, args) str = substArguments(str, args);
return str;
return str };
}
// replace {{arguments}} with their values or the // replace {{arguments}} with their values or the
// associated translation string (based on its key) // associated translation string (based on its key)
function substArguments(str, args) { function substArguments(str, args) {
var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/ var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/, match;
, match
var translations = html10n.translations; var translations = html10n.translations;
while (match = reArgs.exec(str)) { while (match = reArgs.exec(str)) {
if (!match || match.length < 2) if (!match || match.length < 2)
return str // argument key not found return str; // argument key not found
var arg = match[1], sub = '';
var arg = match[1]
, sub = ''
if (args && arg in args) { if (args && arg in args) {
sub = args[arg] sub = args[arg];
} else if (translations && arg in translations) {
sub = translations[arg]
} else {
console.warn('Could not find argument {{' + arg + '}}')
return str
} }
else if (translations && arg in translations) {
str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length) sub = translations[arg];
} }
else {
return str console.warn('Could not find argument {{' + arg + '}}');
return str;
}
str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length);
}
return str;
} }
// replace {[macros]} with their values // replace {[macros]} with their values
function substMacros(key, str, args) { function substMacros(key, str, args) {
var regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}') var regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
, match , match;
while (match = regex.exec(str)) { while (match = regex.exec(str)) {
// a macro has been found // a macro has been found
// Note: at the moment, only one parameter is supported // Note: at the moment, only one parameter is supported
var macroName = match[1] var macroName = match[1], paramName = match[2], optv = match[3], opts = {};
, paramName = match[2] if (!(macroName in html10n.macros))
, optv = match[3] continue;
, opts = {}
if (!(macroName in html10n.macros)) continue
if (optv) { if (optv) {
optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g).forEach(function (arg) { optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g).forEach(function (arg) {
var parts = arg.split(':') var parts = arg.split(':'), name = parts[0], value = parts[1].trim();
, name = parts[0] opts[name] = value;
, value = parts[1].trim() });
opts[name] = value
})
} }
var param;
var param
if (args && paramName in args) { if (args && paramName in args) {
param = args[paramName] param = args[paramName];
} else if (paramName in html10n.translations) { }
param = translations[paramName] else if (paramName in html10n.translations) {
param = translations[paramName];
} }
// there's no macro parser: it has to be defined in html10n.macros // there's no macro parser: it has to be defined in html10n.macros
var macro = html10n.macros[macroName] var macro = html10n.macros[macroName];
str = str.substr(0, match.index) + macro(key, param, opts) + str.substr(match.index+match[0].length) str = str.substr(0, match.index) + macro(key, param, opts) + str.substr(match.index + match[0].length);
} }
return str;
return str
} }
/** /**
* Applies translations to a DOM node (recursive) * Applies translations to a DOM node (recursive)
*/ */
html10n.translateNode = function (translations, node) { html10n.translateNode = function (translations, node) {
var str = {} var str = {};
// get id // get id
str.id = node.getAttribute('data-l10n-id') str.id = node.getAttribute('data-l10n-id');
if (!str.id) return if (!str.id)
return;
if(!translations[str.id]) return console.warn('Couldn\'t find translation key '+str.id) if (!translations[str.id])
return console.warn('Couldn\'t find translation key ' + str.id);
// get args // get args
if (window.JSON) { if (window.JSON) {
str.args = JSON.parse(node.getAttribute('data-l10n-args')) str.args = JSON.parse(node.getAttribute('data-l10n-args'));
}else{ }
else {
try { try {
str.args = eval(node.getAttribute('data-l10n-args')) str.args = eval(node.getAttribute('data-l10n-args'));
}catch(e) { }
console.warn('Couldn\'t parse args for '+str.id) catch (e) {
console.warn('Couldn\'t parse args for ' + str.id);
} }
} }
str.str = html10n.get(str.id, str.args);
str.str = html10n.get(str.id, str.args)
// get attribute name to apply str to // get attribute name to apply str to
var prop var prop, index = str.id.lastIndexOf('.'), attrList = // allowed attributes
, index = str.id.lastIndexOf('.') { "title": 1,
, attrList = // allowed attributes "innerHTML": 1,
{ "title": 1 "alt": 1,
, "innerHTML": 1 "textContent": 1,
, "alt": 1 "value": 1,
, "textContent": 1 "placeholder": 1
, "value": 1 };
, "placeholder": 1
}
if (index > 0 && str.id.substr(index + 1) in attrList) { if (index > 0 && str.id.substr(index + 1) in attrList) {
// an attribute has been specified (example: "my_translation_key.placeholder") // an attribute has been specified (example: "my_translation_key.placeholder")
prop = str.id.substr(index + 1) prop = str.id.substr(index + 1);
} else { // no attribute: assuming text content by default }
prop = document.body.textContent ? 'textContent' : 'innerText' else { // no attribute: assuming text content by default
prop = document.body.textContent ? 'textContent' : 'innerText';
} }
// Apply translation // Apply translation
if (node.children.length === 0 || prop != 'textContent') { if (node.children.length === 0 || prop != 'textContent') {
node[prop] = str.str node[prop] = str.str;
node.setAttribute("aria-label", str.str); // Sets the aria-label node.setAttribute("aria-label", str.str); // Sets the aria-label
// The idea of the above is that we always have an aria value // The idea of the above is that we always have an aria value
// This might be a bit of an abrupt solution but let's see how it goes // This might be a bit of an abrupt solution but let's see how it goes
} else { }
var children = node.childNodes, else {
found = false var children = node.childNodes, found = false;
for (var i = 0, n = children.length; i < n; i++) { for (var i = 0, n = children.length; i < n; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) { if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) {
if (!found) { if (!found) {
children[i].nodeValue = str.str children[i].nodeValue = str.str;
found = true found = true;
} else { }
children[i].nodeValue = '' else {
children[i].nodeValue = '';
} }
} }
} }
if (!found) { if (!found) {
console.warn('Unexpected error: could not translate element content for key '+str.id, node) console.warn('Unexpected error: could not translate element content for key ' + str.id, node);
} }
} }
} };
/** /**
* Builds a translation object from a list of langs (loads the necessary translations) * Builds a translation object from a list of langs (loads the necessary translations)
* @param langs Array - a list of langs sorted by priority (default langs should go last) * @param langs Array - a list of langs sorted by priority (default langs should go last)
*/ */
html10n.build = function (langs, cb) { html10n.build = function (langs, cb) {
var that = this var that = this, build = {};
, build = {}
asyncForEach(langs, function (lang, i, next) { asyncForEach(langs, function (lang, i, next) {
if(!lang) return next(); if (!lang)
that.loader.load(lang, next) return next();
that.loader.load(lang, next);
}, function () { }, function () {
var lang var lang;
langs.reverse() langs.reverse();
// loop through the priority array... // loop through the priority array...
for (var i = 0, n = langs.length; i < n; i++) { for (var i = 0, n = langs.length; i < n; i++) {
lang = langs[i] lang = langs[i];
if (!lang)
if(!lang) continue; continue;
if (!(lang in that.loader.langs)) { // uh, we don't have this lang availbable.. if (!(lang in that.loader.langs)) { // uh, we don't have this lang availbable..
// then check for related langs // then check for related langs
if(~lang.indexOf('-')) lang = lang.split('-')[0]; if (~lang.indexOf('-'))
lang = lang.split('-')[0];
for (var l in that.loader.langs) { for (var l in that.loader.langs) {
if (lang != l && l.indexOf(lang) === 0) { if (lang != l && l.indexOf(lang) === 0) {
lang = l lang = l;
break; break;
} }
} }
if(lang != l) continue; if (lang != l)
continue;
} }
// ... and apply all strings of the current lang in the list // ... and apply all strings of the current lang in the list
// to our build object // to our build object
for (var string in that.loader.langs[lang]) { for (var string in that.loader.langs[lang]) {
build[string] = that.loader.langs[lang][string] build[string] = that.loader.langs[lang][string];
} }
// the last applied lang will be exposed as the // the last applied lang will be exposed as the
// lang the page was translated to // lang the page was translated to
that.language = lang that.language = lang;
} }
cb(null, build) cb(null, build);
}) });
} };
/** /**
* Returns the language that was last applied to the translations hash * Returns the language that was last applied to the translations hash
* thus overriding most of the formerly applied langs * thus overriding most of the formerly applied langs
*/ */
html10n.getLanguage = function () { html10n.getLanguage = function () {
return this.language; return this.language;
} };
/** /**
* Returns the direction of the language returned be html10n#getLanguage * Returns the direction of the language returned be html10n#getLanguage
*/ */
html10n.getDirection = function () { html10n.getDirection = function () {
if(!this.language) return if (!this.language)
var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-')) return;
return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl' var langCode = this.language.indexOf('-') == -1 ? this.language : this.language.substr(0, this.language.indexOf('-'));
} return html10n.rtl.indexOf(langCode) == -1 ? 'ltr' : 'rtl';
};
/** /**
* Index all <link>s * Index all <link>s
*/ */
html10n.index = function () { html10n.index = function () {
// Find all <link>s // Find all <link>s
var links = document.getElementsByTagName('link') var links = document.getElementsByTagName('link'), resources = [];
, resources = []
for (var i = 0, n = links.length; i < n; i++) { for (var i = 0, n = links.length; i < n; i++) {
if (links[i].type != 'application/l10n+json') if (links[i].type != 'application/l10n+json')
continue; continue;
resources.push(links[i].href) resources.push(links[i].href);
} }
this.loader = new Loader(resources) this.loader = new Loader(resources);
this.trigger('indexed') this.trigger('indexed');
} };
if (document.addEventListener) // modern browsers and IE9+ if (document.addEventListener) // modern browsers and IE9+
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
html10n.index() html10n.index();
}, false) }, false);
else if (window.attachEvent) else if (window.attachEvent)
window.attachEvent('onload', function () { window.attachEvent('onload', function () {
html10n.index() html10n.index();
}, false) }, false);
// gettext-like shortcut // gettext-like shortcut
if (window._ === undefined) if (window._ === undefined)
window._ = html10n.get; window._ = html10n.get;
return html10n;
return html10n })(window, document);
})(window, document)

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,10 @@
// WARNING: This file has been modified from the Original // WARNING: This file has been modified from the Original
// TODO: Nice Select seems relatively abandoned, we should consider other options. // TODO: Nice Select seems relatively abandoned, we should consider other options.
/* jQuery Nice Select - v1.1.0 /* jQuery Nice Select - v1.1.0
https://github.com/hernansartorio/jquery-nice-select https://github.com/hernansartorio/jquery-nice-select
Made by Hernán Sartorio */ Made by Hernán Sartorio */
(function ($) { (function ($) {
$.fn.niceSelect = function (method) { $.fn.niceSelect = function (method) {
// Methods // Methods
if (typeof method == 'string') { if (typeof method == 'string') {
if (method == 'update') { if (method == 'update') {
@ -16,21 +12,19 @@
var $select = $(this); var $select = $(this);
var $dropdown = $(this).next('.nice-select'); var $dropdown = $(this).next('.nice-select');
var open = $dropdown.hasClass('open'); var open = $dropdown.hasClass('open');
if ($dropdown.length) { if ($dropdown.length) {
$dropdown.remove(); $dropdown.remove();
create_nice_select($select); create_nice_select($select);
if (open) { if (open) {
$select.next().trigger('click'); $select.next().trigger('click');
} }
} }
}); });
} else if (method == 'destroy') { }
else if (method == 'destroy') {
this.each(function () { this.each(function () {
var $select = $(this); var $select = $(this);
var $dropdown = $(this).next('.nice-select'); var $dropdown = $(this).next('.nice-select');
if ($dropdown.length) { if ($dropdown.length) {
$dropdown.remove(); $dropdown.remove();
$select.css('display', ''); $select.css('display', '');
@ -39,67 +33,52 @@
if ($('.nice-select').length == 0) { if ($('.nice-select').length == 0) {
$(document).off('.nice_select'); $(document).off('.nice_select');
} }
} else { }
console.log('Method "' + method + '" does not exist.') else {
console.log('Method "' + method + '" does not exist.');
} }
return this; return this;
} }
// Hide native select // Hide native select
this.hide(); this.hide();
// Create custom markup // Create custom markup
this.each(function () { this.each(function () {
var $select = $(this); var $select = $(this);
if (!$select.next().hasClass('nice-select')) { if (!$select.next().hasClass('nice-select')) {
create_nice_select($select); create_nice_select($select);
} }
}); });
function create_nice_select($select) { function create_nice_select($select) {
$select.after($('<div></div>') $select.after($('<div></div>')
.addClass('nice-select') .addClass('nice-select')
.addClass($select.attr('class') || '') .addClass($select.attr('class') || '')
.addClass($select.attr('disabled') ? 'disabled' : '') .addClass($select.attr('disabled') ? 'disabled' : '')
.attr('tabindex', $select.attr('disabled') ? null : '0') .attr('tabindex', $select.attr('disabled') ? null : '0')
.html('<span class="current"></span><ul class="list thin-scrollbar"></ul>') .html('<span class="current"></span><ul class="list thin-scrollbar"></ul>'));
);
var $dropdown = $select.next(); var $dropdown = $select.next();
var $options = $select.find('option'); var $options = $select.find('option');
var $selected = $select.find('option:selected'); var $selected = $select.find('option:selected');
$dropdown.find('.current').html($selected.data('display') || $selected.text()); $dropdown.find('.current').html($selected.data('display') || $selected.text());
$options.each(function (i) { $options.each(function (i) {
var $option = $(this); var $option = $(this);
var display = $option.data('display'); var display = $option.data('display');
$dropdown.find('ul').append($('<li></li>') $dropdown.find('ul').append($('<li></li>')
.attr('data-value', $option.val()) .attr('data-value', $option.val())
.attr('data-display', (display || null)) .attr('data-display', (display || null))
.addClass('option' + .addClass('option' +
($option.is(':selected') ? ' selected' : '') + ($option.is(':selected') ? ' selected' : '') +
($option.is(':disabled') ? ' disabled' : '')) ($option.is(':disabled') ? ' disabled' : ''))
.html($option.text()) .html($option.text()));
);
}); });
} }
/* Event listeners */ /* Event listeners */
// Unbind existing events in case that the plugin has been initialized before // Unbind existing events in case that the plugin has been initialized before
$(document).off('.nice_select'); $(document).off('.nice_select');
// Open/close // Open/close
$(document).on('click.nice_select', '.nice-select', function (event) { $(document).on('click.nice_select', '.nice-select', function (event) {
var $dropdown = $(this); var $dropdown = $(this);
$('.nice-select').not($dropdown).removeClass('open'); $('.nice-select').not($dropdown).removeClass('open');
$dropdown.toggleClass('open'); $dropdown.toggleClass('open');
if ($dropdown.hasClass('open')) { if ($dropdown.hasClass('open')) {
$dropdown.find('.option'); $dropdown.find('.option');
$dropdown.find('.focus').removeClass('focus'); $dropdown.find('.focus').removeClass('focus');
@ -109,7 +88,6 @@
$dropdown.find('.list').css('top', $dropdown.offset().top + $dropdown.outerHeight()); $dropdown.find('.list').css('top', $dropdown.offset().top + $dropdown.outerHeight());
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px'); $dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
} }
$listHeight = $dropdown.find('.list').outerHeight(); $listHeight = $dropdown.find('.list').outerHeight();
$top = $dropdown.parent().offset().top; $top = $dropdown.parent().offset().top;
$bottom = $('body').height() - $top; $bottom = $('body').height() - $top;
@ -117,55 +95,52 @@
if ($maxListHeight < 200) { if ($maxListHeight < 200) {
$dropdown.addClass('reverse'); $dropdown.addClass('reverse');
$maxListHeight = 250; $maxListHeight = 250;
} else { }
$dropdown.removeClass('reverse') else {
$dropdown.removeClass('reverse');
} }
$dropdown.find('.list').css('max-height', $maxListHeight + 'px'); $dropdown.find('.list').css('max-height', $maxListHeight + 'px');
}
} else { else {
$dropdown.focus(); $dropdown.focus();
} }
}); });
// Close when clicking outside // Close when clicking outside
$(document).on('click.nice_select', function (event) { $(document).on('click.nice_select', function (event) {
if ($(event.target).closest('.nice-select').length === 0) { if ($(event.target).closest('.nice-select').length === 0) {
$('.nice-select').removeClass('open').find('.option'); $('.nice-select').removeClass('open').find('.option');
} }
}); });
// Option click // Option click
$(document).on('click.nice_select', '.nice-select .option:not(.disabled)', function (event) { $(document).on('click.nice_select', '.nice-select .option:not(.disabled)', function (event) {
var $option = $(this); var $option = $(this);
var $dropdown = $option.closest('.nice-select'); var $dropdown = $option.closest('.nice-select');
$dropdown.find('.selected').removeClass('selected'); $dropdown.find('.selected').removeClass('selected');
$option.addClass('selected'); $option.addClass('selected');
var text = $option.data('display') || $option.text(); var text = $option.data('display') || $option.text();
$dropdown.find('.current').text(text); $dropdown.find('.current').text(text);
$dropdown.prev('select').val($option.data('value')).trigger('change'); $dropdown.prev('select').val($option.data('value')).trigger('change');
}); });
// Keyboard events // Keyboard events
$(document).on('keydown.nice_select', '.nice-select', function (event) { $(document).on('keydown.nice_select', '.nice-select', function (event) {
var $dropdown = $(this); var $dropdown = $(this);
var $focused_option = $($dropdown.find('.focus') || $dropdown.find('.list .option.selected')); var $focused_option = $($dropdown.find('.focus') || $dropdown.find('.list .option.selected'));
// Space or Enter // Space or Enter
if (event.keyCode == 32 || event.keyCode == 13) { if (event.keyCode == 32 || event.keyCode == 13) {
if ($dropdown.hasClass('open')) { if ($dropdown.hasClass('open')) {
$focused_option.trigger('click'); $focused_option.trigger('click');
} else { }
else {
$dropdown.trigger('click'); $dropdown.trigger('click');
} }
return false; return false;
// Down // Down
} else if (event.keyCode == 40) { }
else if (event.keyCode == 40) {
if (!$dropdown.hasClass('open')) { if (!$dropdown.hasClass('open')) {
$dropdown.trigger('click'); $dropdown.trigger('click');
} else { }
else {
var $next = $focused_option.nextAll('.option:not(.disabled)').first(); var $next = $focused_option.nextAll('.option:not(.disabled)').first();
if ($next.length > 0) { if ($next.length > 0) {
$dropdown.find('.focus').removeClass('focus'); $dropdown.find('.focus').removeClass('focus');
@ -174,10 +149,12 @@
} }
return false; return false;
// Up // Up
} else if (event.keyCode == 38) { }
else if (event.keyCode == 38) {
if (!$dropdown.hasClass('open')) { if (!$dropdown.hasClass('open')) {
$dropdown.trigger('click'); $dropdown.trigger('click');
} else { }
else {
var $prev = $focused_option.prevAll('.option:not(.disabled)').first(); var $prev = $focused_option.prevAll('.option:not(.disabled)').first();
if ($prev.length > 0) { if ($prev.length > 0) {
$dropdown.find('.focus').removeClass('focus'); $dropdown.find('.focus').removeClass('focus');
@ -186,27 +163,25 @@
} }
return false; return false;
// Esc // Esc
} else if (event.keyCode == 27) { }
else if (event.keyCode == 27) {
if ($dropdown.hasClass('open')) { if ($dropdown.hasClass('open')) {
$dropdown.trigger('click'); $dropdown.trigger('click');
} }
// Tab // Tab
} else if (event.keyCode == 9) { }
else if (event.keyCode == 9) {
if ($dropdown.hasClass('open')) { if ($dropdown.hasClass('open')) {
return false; return false;
} }
} }
}); });
// Detect CSS pointer-events support, for IE <= 10. From Modernizr. // Detect CSS pointer-events support, for IE <= 10. From Modernizr.
var style = document.createElement('a').style; var style = document.createElement('a').style;
style.cssText = 'pointer-events:auto'; style.cssText = 'pointer-events:auto';
if (style.pointerEvents !== 'auto') { if (style.pointerEvents !== 'auto') {
$('html').addClass('no-csspointerevents'); $('html').addClass('no-csspointerevents');
} }
return this; return this;
}; };
}(jQuery)); }(jQuery));

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
$('#pad_title').show(); $('#pad_title').show();
$('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); }); $('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); });

View file

@ -1,4 +1,3 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
}; };

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict'; 'use strict';
window.customStart = () => { window.customStart = () => {
// define your javascript here // define your javascript here
// jquery is available - except index.js // jquery is available - except index.js

View file

@ -1,32 +1,26 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as exportEtherpad from "../../../node/utils/ExportEtherpad.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const exportEtherpad = require('../../../node/utils/ExportEtherpad');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
describe(__filename, function () { describe(__filename, function () {
let padId; let padId;
beforeEach(async function () { beforeEach(async function () {
padId = common.randomString(); padId = common.randomString();
assert(!await padManager.doesPadExist(padId)); assert(!await padManager.doesPadExist(padId));
}); });
describe('exportEtherpadAdditionalContent', function () { describe('exportEtherpadAdditionalContent', function () {
let hookBackup; let hookBackup;
before(async function () { before(async function () {
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || []; hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }]; plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
}); });
after(async function () { after(async function () {
plugins.hooks.exportEtherpadAdditionalContent = hookBackup; plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
}); });
it('exports custom records', async function () { it('exports custom records', async function () {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.db.set(`custom:${padId}`, 'a'); await pad.db.set(`custom:${padId}`, 'a');
@ -37,7 +31,6 @@ describe(__filename, function () {
assert.equal(data[`custom:${padId}:`], 'b'); assert.equal(data[`custom:${padId}:`], 'b');
assert.equal(data[`custom:${padId}:foo`], 'c'); assert.equal(data[`custom:${padId}:foo`], 'c');
}); });
it('export from read-only pad uses read-only ID', async function () { it('export from read-only pad uses read-only ID', async function () {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
const readOnlyId = await readOnlyManager.getReadOnlyId(padId); const readOnlyId = await readOnlyManager.getReadOnlyId(padId);
@ -52,7 +45,6 @@ describe(__filename, function () {
assert(!(`custom:${padId}:` in data)); assert(!(`custom:${padId}:` in data));
assert(!(`custom:${padId}:foo` in data)); assert(!(`custom:${padId}:foo` in data));
}); });
it('does not export records from pad with similar ID', async function () { it('does not export records from pad with similar ID', async function () {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.db.set(`custom:${padId}x`, 'a'); await pad.db.set(`custom:${padId}x`, 'a');

View file

@ -1,18 +1,16 @@
import assert$0 from "assert";
import * as authorManager from "../../../node/db/AuthorManager.js";
import * as db from "../../../node/db/DB.js";
import * as importEtherpad from "../../../node/utils/ImportEtherpad.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as padUtils from "../../../static/js/pad_utils.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict; const { randomString } = padUtils;
const authorManager = require('../../../node/db/AuthorManager');
const db = require('../../../node/db/DB');
const importEtherpad = require('../../../node/utils/ImportEtherpad');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const {randomString} = require('../../../static/js/pad_utils');
describe(__filename, function () { describe(__filename, function () {
let padId; let padId;
const makeAuthorId = () => `a.${randomString(16)}`; const makeAuthorId = () => `a.${randomString(16)}`;
const makeExport = (authorId) => ({ const makeExport = (authorId) => ({
'pad:testing': { 'pad:testing': {
atext: { atext: {
@ -48,12 +46,10 @@ describe(__filename, function () {
}, },
}, },
}); });
beforeEach(async function () { beforeEach(async function () {
padId = randomString(10); padId = randomString(10);
assert(!await padManager.doesPadExist(padId)); assert(!await padManager.doesPadExist(padId));
}); });
it('unknown db records are ignored', async function () { it('unknown db records are ignored', async function () {
const badKey = `maliciousDbKey${randomString(10)}`; const badKey = `maliciousDbKey${randomString(10)}`;
await importEtherpad.setPadRaw(padId, JSON.stringify({ await importEtherpad.setPadRaw(padId, JSON.stringify({
@ -62,7 +58,6 @@ describe(__filename, function () {
})); }));
assert(await db.get(badKey) == null); assert(await db.get(badKey) == null);
}); });
it('changes are all or nothing', async function () { it('changes are all or nothing', async function () {
const authorId = makeAuthorId(); const authorId = makeAuthorId();
const data = makeExport(authorId); const data = makeExport(authorId);
@ -72,11 +67,9 @@ describe(__filename, function () {
assert(!await authorManager.doesAuthorExist(authorId)); assert(!await authorManager.doesAuthorExist(authorId));
assert(!await padManager.doesPadExist(padId)); assert(!await padManager.doesPadExist(padId));
}); });
describe('author pad IDs', function () { describe('author pad IDs', function () {
let existingAuthorId; let existingAuthorId;
let newAuthorId; let newAuthorId;
beforeEach(async function () { beforeEach(async function () {
existingAuthorId = (await authorManager.createAuthor('existing')).authorID; existingAuthorId = (await authorManager.createAuthor('existing')).authorID;
assert(await authorManager.doesAuthorExist(existingAuthorId)); assert(await authorManager.doesAuthorExist(existingAuthorId));
@ -85,7 +78,6 @@ describe(__filename, function () {
assert.notEqual(newAuthorId, existingAuthorId); assert.notEqual(newAuthorId, existingAuthorId);
assert(!await authorManager.doesAuthorExist(newAuthorId)); assert(!await authorManager.doesAuthorExist(newAuthorId));
}); });
it('author does not yet exist', async function () { it('author does not yet exist', async function () {
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId))); await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
assert(await authorManager.doesAuthorExist(newAuthorId)); assert(await authorManager.doesAuthorExist(newAuthorId));
@ -94,7 +86,6 @@ describe(__filename, function () {
assert.equal(author.colorId, '#000000'); assert.equal(author.colorId, '#000000');
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]); assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
}); });
it('author already exists, no pads', async function () { it('author already exists, no pads', async function () {
newAuthorId = existingAuthorId; newAuthorId = existingAuthorId;
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId))); await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
@ -104,7 +95,6 @@ describe(__filename, function () {
assert.notEqual(author.colorId, '#000000'); assert.notEqual(author.colorId, '#000000');
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]); assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
}); });
it('author already exists, on different pad', async function () { it('author already exists, on different pad', async function () {
const otherPadId = randomString(10); const otherPadId = randomString(10);
await authorManager.addPad(existingAuthorId, otherPadId); await authorManager.addPad(existingAuthorId, otherPadId);
@ -114,11 +104,8 @@ describe(__filename, function () {
const author = await authorManager.getAuthor(newAuthorId); const author = await authorManager.getAuthor(newAuthorId);
assert.equal(author.name, 'existing'); assert.equal(author.name, 'existing');
assert.notEqual(author.colorId, '#000000'); assert.notEqual(author.colorId, '#000000');
assert.deepEqual( assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(), [otherPadId, padId].sort());
(await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(),
[otherPadId, padId].sort());
}); });
it('author already exists, on same pad', async function () { it('author already exists, on same pad', async function () {
await authorManager.addPad(existingAuthorId, padId); await authorManager.addPad(existingAuthorId, padId);
newAuthorId = existingAuthorId; newAuthorId = existingAuthorId;
@ -130,7 +117,6 @@ describe(__filename, function () {
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]); assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
}); });
}); });
describe('enforces consistent pad ID', function () { describe('enforces consistent pad ID', function () {
it('pad record has different pad ID', async function () { it('pad record has different pad ID', async function () {
const data = makeExport(makeAuthorId()); const data = makeExport(makeAuthorId());
@ -138,14 +124,12 @@ describe(__filename, function () {
delete data['pad:testing']; delete data['pad:testing'];
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
}); });
it('globalAuthor record has different pad ID', async function () { it('globalAuthor record has different pad ID', async function () {
const authorId = makeAuthorId(); const authorId = makeAuthorId();
const data = makeExport(authorId); const data = makeExport(authorId);
data[`globalAuthor:${authorId}`].padIDs = 'differentPadId'; data[`globalAuthor:${authorId}`].padIDs = 'differentPadId';
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
}); });
it('pad rev record has different pad ID', async function () { it('pad rev record has different pad ID', async function () {
const data = makeExport(makeAuthorId()); const data = makeExport(makeAuthorId());
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0']; data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
@ -153,34 +137,28 @@ describe(__filename, function () {
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/); assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
}); });
}); });
describe('order of records does not matter', function () { describe('order of records does not matter', function () {
for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) { for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) {
it(JSON.stringify(perm), async function () { it(JSON.stringify(perm), async function () {
const authorId = makeAuthorId(); const authorId = makeAuthorId();
const records = Object.entries(makeExport(authorId)); const records = Object.entries(makeExport(authorId));
assert.equal(records.length, 3); assert.equal(records.length, 3);
await importEtherpad.setPadRaw( await importEtherpad.setPadRaw(padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]); assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]);
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
assert.equal(pad.text(), 'foo\n'); assert.equal(pad.text(), 'foo\n');
}); });
} }
}); });
describe('exportEtherpadAdditionalContent', function () { describe('exportEtherpadAdditionalContent', function () {
let hookBackup; let hookBackup;
before(async function () { before(async function () {
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || []; hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }]; plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
}); });
after(async function () { after(async function () {
plugins.hooks.exportEtherpadAdditionalContent = hookBackup; plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
}); });
it('imports from custom prefix', async function () { it('imports from custom prefix', async function () {
await importEtherpad.setPadRaw(padId, JSON.stringify({ await importEtherpad.setPadRaw(padId, JSON.stringify({
...makeExport(makeAuthorId()), ...makeExport(makeAuthorId()),
@ -191,7 +169,6 @@ describe(__filename, function () {
assert.equal(await pad.db.get(`custom:${padId}`), 'a'); assert.equal(await pad.db.get(`custom:${padId}`), 'a');
assert.equal(await pad.db.get(`custom:${padId}:foo`), 'b'); assert.equal(await pad.db.get(`custom:${padId}:foo`), 'b');
}); });
it('rejects records for pad with similar ID', async function () { it('rejects records for pad with similar ID', async function () {
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({ await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
...makeExport(makeAuthorId()), ...makeExport(makeAuthorId()),

View file

@ -1,37 +1,33 @@
import * as Pad from "../../../node/db/Pad.js";
import assert$0 from "assert";
import * as authorManager from "../../../node/db/AuthorManager.js";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const Pad = require('../../../node/db/Pad');
const assert = require('assert').strict;
const authorManager = require('../../../node/db/AuthorManager');
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () { describe(__filename, function () {
const backups = {}; const backups = {};
let pad; let pad;
let padId; let padId;
before(async function () { before(async function () {
backups.hooks = { backups.hooks = {
padDefaultContent: plugins.hooks.padDefaultContent, padDefaultContent: plugins.hooks.padDefaultContent,
}; };
backups.defaultPadText = settings.defaultPadText; backups.defaultPadText = settings.defaultPadText;
}); });
beforeEach(async function () { beforeEach(async function () {
backups.hooks.padDefaultContent = []; backups.hooks.padDefaultContent = [];
padId = common.randomString(); padId = common.randomString();
assert(!(await padManager.doesPadExist(padId))); assert(!(await padManager.doesPadExist(padId)));
}); });
afterEach(async function () { afterEach(async function () {
Object.assign(plugins.hooks, backups.hooks); Object.assign(plugins.hooks, backups.hooks);
if (pad != null) await pad.remove(); if (pad != null)
await pad.remove();
pad = null; pad = null;
}); });
describe('cleanText', function () { describe('cleanText', function () {
const testCases = [ const testCases = [
['', ''], ['', ''],
@ -49,7 +45,6 @@ describe(__filename, function () {
}); });
} }
}); });
describe('padDefaultContent hook', function () { describe('padDefaultContent hook', function () {
it('runs when a pad is created without specific text', async function () { it('runs when a pad is created without specific text', async function () {
const p = new Promise((resolve) => { const p = new Promise((resolve) => {
@ -58,20 +53,18 @@ describe(__filename, function () {
pad = await padManager.getPad(padId); pad = await padManager.getPad(padId);
await p; await p;
}); });
it('not run if pad is created with specific text', async function () { it('not run if pad is created with specific text', async function () {
plugins.hooks.padDefaultContent.push( plugins.hooks.padDefaultContent.push({ hook_fn: () => { throw new Error('should not be called'); } });
{hook_fn: () => { throw new Error('should not be called'); }});
pad = await padManager.getPad(padId, ''); pad = await padManager.getPad(padId, '');
}); });
it('defaults to settings.defaultPadText', async function () { it('defaults to settings.defaultPadText', async function () {
const p = new Promise((resolve, reject) => { const p = new Promise((resolve, reject) => {
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => { plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
try { try {
assert.equal(ctx.type, 'text'); assert.equal(ctx.type, 'text');
assert.equal(ctx.content, settings.defaultPadText); assert.equal(ctx.content, settings.defaultPadText);
} catch (err) { }
catch (err) {
return reject(err); return reject(err);
} }
resolve(); resolve();
@ -80,7 +73,6 @@ describe(__filename, function () {
pad = await padManager.getPad(padId); pad = await padManager.getPad(padId);
await p; await p;
}); });
it('passes the pad object', async function () { it('passes the pad object', async function () {
const gotP = new Promise((resolve) => { const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { pad }) => resolve(pad) }); plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { pad }) => resolve(pad) });
@ -88,26 +80,21 @@ describe(__filename, function () {
pad = await padManager.getPad(padId); pad = await padManager.getPad(padId);
assert.equal(await gotP, pad); assert.equal(await gotP, pad);
}); });
it('passes empty authorId if not provided', async function () { it('passes empty authorId if not provided', async function () {
const gotP = new Promise((resolve) => { const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push( plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
}); });
pad = await padManager.getPad(padId); pad = await padManager.getPad(padId);
assert.equal(await gotP, ''); assert.equal(await gotP, '');
}); });
it('passes provided authorId', async function () { it('passes provided authorId', async function () {
const want = await authorManager.getAuthor4Token(`t.${padId}`); const want = await authorManager.getAuthor4Token(`t.${padId}`);
const gotP = new Promise((resolve) => { const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push( plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
}); });
pad = await padManager.getPad(padId, null, want); pad = await padManager.getPad(padId, null, want);
assert.equal(await gotP, want); assert.equal(await gotP, want);
}); });
it('uses provided content', async function () { it('uses provided content', async function () {
const want = 'hello world'; const want = 'hello world';
assert.notEqual(want, settings.defaultPadText); assert.notEqual(want, settings.defaultPadText);
@ -118,7 +105,6 @@ describe(__filename, function () {
pad = await padManager.getPad(padId); pad = await padManager.getPad(padId);
assert.equal(pad.text(), `${want}\n`); assert.equal(pad.text(), `${want}\n`);
}); });
it('cleans provided content', async function () { it('cleans provided content', async function () {
const input = 'foo\r\nbar\r\tbaz'; const input = 'foo\r\nbar\r\tbaz';
const want = 'foo\nbar\n baz'; const want = 'foo\nbar\n baz';

View file

@ -1,50 +1,43 @@
import SessionStore from "../../../node/db/SessionStore.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as db from "../../../node/db/DB.js";
import util from "util";
'use strict'; 'use strict';
const assert = assert$0.strict;
const SessionStore = require('../../../node/db/SessionStore');
const assert = require('assert').strict;
const common = require('../common');
const db = require('../../../node/db/DB');
const util = require('util');
describe(__filename, function () { describe(__filename, function () {
let ss; let ss;
let sid; let sid;
const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess); const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess);
const get = async () => await util.promisify(ss.get).call(ss, sid); const get = async () => await util.promisify(ss.get).call(ss, sid);
const destroy = async () => await util.promisify(ss.destroy).call(ss, sid); const destroy = async () => await util.promisify(ss.destroy).call(ss, sid);
const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess); const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess);
before(async function () { before(async function () {
await common.init(); await common.init();
}); });
beforeEach(async function () { beforeEach(async function () {
ss = new SessionStore(); ss = new SessionStore();
sid = common.randomString(); sid = common.randomString();
}); });
afterEach(async function () { afterEach(async function () {
if (ss != null) { if (ss != null) {
if (sid != null) await destroy(); if (sid != null)
await destroy();
ss.shutdown(); ss.shutdown();
} }
sid = null; sid = null;
ss = null; ss = null;
}); });
describe('set', function () { describe('set', function () {
it('set of null is a no-op', async function () { it('set of null is a no-op', async function () {
await set(null); await set(null);
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('set of non-expiring session', async function () { it('set of non-expiring session', async function () {
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } }; const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await set(sess); await set(sess);
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
}); });
it('set of session that expires', async function () { it('set of session that expires', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess); await set(sess);
@ -53,14 +46,12 @@ describe(__filename, function () {
// Writing should start a timeout. // Writing should start a timeout.
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('set of already expired session', async function () { it('set of already expired session', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(1) } }; const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
await set(sess); await set(sess);
// No record should have been created. // No record should have been created.
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('switch from non-expiring to expiring', async function () { it('switch from non-expiring to expiring', async function () {
const sess = { foo: 'bar' }; const sess = { foo: 'bar' };
await set(sess); await set(sess);
@ -69,7 +60,6 @@ describe(__filename, function () {
await new Promise((resolve) => setTimeout(resolve, 110)); await new Promise((resolve) => setTimeout(resolve, 110));
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('switch from expiring to non-expiring', async function () { it('switch from expiring to non-expiring', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess); await set(sess);
@ -79,24 +69,20 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
}); });
}); });
describe('get', function () { describe('get', function () {
it('get of non-existent entry', async function () { it('get of non-existent entry', async function () {
assert(await get() == null); assert(await get() == null);
}); });
it('set+get round trip', async function () { it('set+get round trip', async function () {
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } }; const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await set(sess); await set(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
}); });
it('get of record from previous run (no expiration)', async function () { it('get of record from previous run (no expiration)', async function () {
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } }; const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await db.set(`sessionstorage:${sid}`, sess); await db.set(`sessionstorage:${sid}`, sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
}); });
it('get of record from previous run (not yet expired)', async function () { it('get of record from previous run (not yet expired)', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await db.set(`sessionstorage:${sid}`, sess); await db.set(`sessionstorage:${sid}`, sess);
@ -105,14 +91,12 @@ describe(__filename, function () {
// Reading should start a timeout. // Reading should start a timeout.
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('get of record from previous run (already expired)', async function () { it('get of record from previous run (already expired)', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(1) } }; const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
await db.set(`sessionstorage:${sid}`, sess); await db.set(`sessionstorage:${sid}`, sess);
assert(await get() == null); assert(await get() == null);
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('external expiration update is picked up', async function () { it('external expiration update is picked up', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess); await set(sess);
@ -125,7 +109,6 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
}); });
}); });
describe('shutdown', function () { describe('shutdown', function () {
it('shutdown cancels timeouts', async function () { it('shutdown cancels timeouts', async function () {
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
@ -137,7 +120,6 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
}); });
}); });
describe('destroy', function () { describe('destroy', function () {
it('destroy deletes the database record', async function () { it('destroy deletes the database record', async function () {
const sess = { cookie: { expires: new Date(Date.now() + 100) } }; const sess = { cookie: { expires: new Date(Date.now() + 100) } };
@ -145,7 +127,6 @@ describe(__filename, function () {
await destroy(); await destroy();
assert(await db.get(`sessionstorage:${sid}`) == null); assert(await db.get(`sessionstorage:${sid}`) == null);
}); });
it('destroy cancels the timeout', async function () { it('destroy cancels the timeout', async function () {
const sess = { cookie: { expires: new Date(Date.now() + 100) } }; const sess = { cookie: { expires: new Date(Date.now() + 100) } };
await set(sess); await set(sess);
@ -154,19 +135,16 @@ describe(__filename, function () {
await new Promise((resolve) => setTimeout(resolve, 110)); await new Promise((resolve) => setTimeout(resolve, 110));
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
}); });
it('destroy session that does not exist', async function () { it('destroy session that does not exist', async function () {
await destroy(); await destroy();
}); });
}); });
describe('touch without refresh', function () { describe('touch without refresh', function () {
it('touch before set is equivalent to set if session expires', async function () { it('touch before set is equivalent to set if session expires', async function () {
const sess = { cookie: { expires: new Date(Date.now() + 1000) } }; const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
await touch(sess); await touch(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
}); });
it('touch updates observed expiration but not database', async function () { it('touch updates observed expiration but not database', async function () {
const start = Date.now(); const start = Date.now();
const sess = { cookie: { expires: new Date(start + 200) } }; const sess = { cookie: { expires: new Date(start + 200) } };
@ -177,18 +155,15 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
}); });
}); });
describe('touch with refresh', function () { describe('touch with refresh', function () {
beforeEach(async function () { beforeEach(async function () {
ss = new SessionStore(200); ss = new SessionStore(200);
}); });
it('touch before set is equivalent to set if session expires', async function () { it('touch before set is equivalent to set if session expires', async function () {
const sess = { cookie: { expires: new Date(Date.now() + 1000) } }; const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
await touch(sess); await touch(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
}); });
it('touch before eligible for refresh updates expiration but not DB', async function () { it('touch before eligible for refresh updates expiration but not DB', async function () {
const now = Date.now(); const now = Date.now();
const sess = { foo: 'bar', cookie: { expires: new Date(now + 1000) } }; const sess = { foo: 'bar', cookie: { expires: new Date(now + 1000) } };
@ -198,7 +173,6 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
}); });
it('touch before eligible for refresh updates timeout', async function () { it('touch before eligible for refresh updates timeout', async function () {
const start = Date.now(); const start = Date.now();
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } }; const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
@ -210,7 +184,6 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
}); });
it('touch after eligible for refresh updates db', async function () { it('touch after eligible for refresh updates db', async function () {
const start = Date.now(); const start = Date.now();
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } }; const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
@ -222,7 +195,6 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2)); assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2)); assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
}); });
it('refresh=0 updates db every time', async function () { it('refresh=0 updates db every time', async function () {
ss = new SessionStore(0); ss = new SessionStore(0);
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 1000) } }; const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 1000) } };

View file

@ -1,39 +1,35 @@
import * as Stream from "../../../node/utils/Stream.js";
import assert$0 from "assert";
'use strict'; 'use strict';
const assert = assert$0.strict;
const Stream = require('../../../node/utils/Stream');
const assert = require('assert').strict;
class DemoIterable { class DemoIterable {
constructor() { constructor() {
this.value = 0; this.value = 0;
this.errs = []; this.errs = [];
this.rets = []; this.rets = [];
} }
completed() { return this.errs.length > 0 || this.rets.length > 0; } completed() { return this.errs.length > 0 || this.rets.length > 0; }
next() { next() {
if (this.completed()) return {value: undefined, done: true}; // Mimic standard generators. if (this.completed())
return { value: undefined, done: true }; // Mimic standard generators.
return { value: this.value++, done: false }; return { value: this.value++, done: false };
} }
throw(err) { throw(err) {
const alreadyCompleted = this.completed(); const alreadyCompleted = this.completed();
this.errs.push(err); this.errs.push(err);
if (alreadyCompleted) throw err; // Mimic standard generator objects. if (alreadyCompleted)
throw err; // Mimic standard generator objects.
throw err; throw err;
} }
return(ret) { return(ret) {
const alreadyCompleted = this.completed(); const alreadyCompleted = this.completed();
this.rets.push(ret); this.rets.push(ret);
if (alreadyCompleted) return {value: ret, done: true}; // Mimic standard generator objects. if (alreadyCompleted)
return { value: ret, done: true }; // Mimic standard generator objects.
return { value: ret, done: true }; return { value: ret, done: true };
} }
[Symbol.iterator]() { return this; } [Symbol.iterator]() { return this; }
} }
const assertUnhandledRejection = async (action, want) => { const assertUnhandledRejection = async (action, want) => {
// Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we // Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we
// expect to see don't trigger a test failure (or terminate node). // expect to see don't trigger a test failure (or terminate node).
@ -53,32 +49,29 @@ const assertUnhandledRejection = async (action, want) => {
process.on(event, tempListener); process.on(event, tempListener);
await action(); await action();
await seenErrPromise; await seenErrPromise;
} finally { }
finally {
// Restore the original listeners. // Restore the original listeners.
process.off(event, tempListener); process.off(event, tempListener);
for (const listener of listenersBackup) process.on(event, listener); for (const listener of listenersBackup)
process.on(event, listener);
} }
await assert.rejects(Promise.reject(asyncErr), want); await assert.rejects(Promise.reject(asyncErr), want);
}; };
describe(__filename, function () { describe(__filename, function () {
describe('basic behavior', function () { describe('basic behavior', function () {
it('takes a generator', async function () { it('takes a generator', async function () {
assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]); assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]);
}); });
it('takes an array', async function () { it('takes an array', async function () {
assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]); assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]);
}); });
it('takes an iterator', async function () { it('takes an iterator', async function () {
assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]); assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]);
}); });
it('supports empty iterators', async function () { it('supports empty iterators', async function () {
assert.deepEqual([...new Stream([])], []); assert.deepEqual([...new Stream([])], []);
}); });
it('is resumable', async function () { it('is resumable', async function () {
const s = new Stream((function* () { yield 0; yield 1; yield 2; })()); const s = new Stream((function* () { yield 0; yield 1; yield 2; })());
let iter = s[Symbol.iterator](); let iter = s[Symbol.iterator]();
@ -87,14 +80,12 @@ describe(__filename, function () {
assert.deepEqual(iter.next(), { value: 1, done: false }); assert.deepEqual(iter.next(), { value: 1, done: false });
assert.deepEqual([...s], [2]); assert.deepEqual([...s], [2]);
}); });
it('supports return value', async function () { it('supports return value', async function () {
const s = new Stream((function* () { yield 0; return 1; })()); const s = new Stream((function* () { yield 0; return 1; })());
const iter = s[Symbol.iterator](); const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), { value: 0, done: false }); assert.deepEqual(iter.next(), { value: 0, done: false });
assert.deepEqual(iter.next(), { value: 1, done: true }); assert.deepEqual(iter.next(), { value: 1, done: true });
}); });
it('does not start until needed', async function () { it('does not start until needed', async function () {
let lastYield = null; let lastYield = null;
new Stream((function* () { yield lastYield = 0; })()); new Stream((function* () { yield lastYield = 0; })());
@ -102,7 +93,6 @@ describe(__filename, function () {
// from the stream. // from the stream.
assert.equal(lastYield, null); assert.equal(lastYield, null);
}); });
it('throw is propagated', async function () { it('throw is propagated', async function () {
const underlying = new DemoIterable(); const underlying = new DemoIterable();
const s = new Stream(underlying); const s = new Stream(underlying);
@ -112,7 +102,6 @@ describe(__filename, function () {
assert.throws(() => iter.throw(err), err); assert.throws(() => iter.throw(err), err);
assert.equal(underlying.errs[0], err); assert.equal(underlying.errs[0], err);
}); });
it('return is propagated', async function () { it('return is propagated', async function () {
const underlying = new DemoIterable(); const underlying = new DemoIterable();
const s = new Stream(underlying); const s = new Stream(underlying);
@ -122,44 +111,37 @@ describe(__filename, function () {
assert.equal(underlying.rets[0], 42); assert.equal(underlying.rets[0], 42);
}); });
}); });
describe('range', function () { describe('range', function () {
it('basic', async function () { it('basic', async function () {
assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]); assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]);
}); });
it('empty', async function () { it('empty', async function () {
assert.deepEqual([...Stream.range(0, 0)], []); assert.deepEqual([...Stream.range(0, 0)], []);
}); });
it('positive start', async function () { it('positive start', async function () {
assert.deepEqual([...Stream.range(3, 5)], [3, 4]); assert.deepEqual([...Stream.range(3, 5)], [3, 4]);
}); });
it('negative start', async function () { it('negative start', async function () {
assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]); assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]);
}); });
it('end before start', async function () { it('end before start', async function () {
assert.deepEqual([...Stream.range(3, 0)], []); assert.deepEqual([...Stream.range(3, 0)], []);
}); });
}); });
describe('batch', function () { describe('batch', function () {
it('empty', async function () { it('empty', async function () {
assert.deepEqual([...new Stream([]).batch(10)], []); assert.deepEqual([...new Stream([]).batch(10)], []);
}); });
it('does not start until needed', async function () { it('does not start until needed', async function () {
let lastYield = null; let lastYield = null;
new Stream((function* () { yield lastYield = 0; })()).batch(10); new Stream((function* () { yield lastYield = 0; })()).batch(10);
assert.equal(lastYield, null); assert.equal(lastYield, null);
}); });
it('fewer than batch size', async function () { it('fewer than batch size', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i; for (let i = 0; i < 5; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).batch(10); const s = new Stream(values).batch(10);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -168,11 +150,11 @@ describe(__filename, function () {
assert.deepEqual([...s], [1, 2, 3, 4]); assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4); assert.equal(lastYield, 4);
}); });
it('exactly batch size', async function () { it('exactly batch size', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i; for (let i = 0; i < 5; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).batch(5); const s = new Stream(values).batch(5);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -181,11 +163,11 @@ describe(__filename, function () {
assert.deepEqual([...s], [1, 2, 3, 4]); assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4); assert.equal(lastYield, 4);
}); });
it('multiple batches, last batch is not full', async function () { it('multiple batches, last batch is not full', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 10; i++) yield lastYield = i; for (let i = 0; i < 10; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).batch(3); const s = new Stream(values).batch(3);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -200,7 +182,6 @@ describe(__filename, function () {
assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]); assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]);
assert.equal(lastYield, 9); assert.equal(lastYield, 9);
}); });
it('batched Promise rejections are suppressed while iterating', async function () { it('batched Promise rejections are suppressed while iterating', async function () {
let lastYield = null; let lastYield = null;
const err = new Error('injected'); const err = new Error('injected');
@ -220,7 +201,6 @@ describe(__filename, function () {
await assert.rejects(iter.next().value, err); await assert.rejects(iter.next().value, err);
iter.return(); iter.return();
}); });
it('batched Promise rejections are unsuppressed when iteration completes', async function () { it('batched Promise rejections are unsuppressed when iteration completes', async function () {
let lastYield = null; let lastYield = null;
const err = new Error('injected'); const err = new Error('injected');
@ -239,22 +219,20 @@ describe(__filename, function () {
await assertUnhandledRejection(() => iter.return(), err); await assertUnhandledRejection(() => iter.return(), err);
}); });
}); });
describe('buffer', function () { describe('buffer', function () {
it('empty', async function () { it('empty', async function () {
assert.deepEqual([...new Stream([]).buffer(10)], []); assert.deepEqual([...new Stream([]).buffer(10)], []);
}); });
it('does not start until needed', async function () { it('does not start until needed', async function () {
let lastYield = null; let lastYield = null;
new Stream((function* () { yield lastYield = 0; })()).buffer(10); new Stream((function* () { yield lastYield = 0; })()).buffer(10);
assert.equal(lastYield, null); assert.equal(lastYield, null);
}); });
it('fewer than buffer size', async function () { it('fewer than buffer size', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i; for (let i = 0; i < 5; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).buffer(10); const s = new Stream(values).buffer(10);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -263,11 +241,11 @@ describe(__filename, function () {
assert.deepEqual([...s], [1, 2, 3, 4]); assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4); assert.equal(lastYield, 4);
}); });
it('exactly buffer size', async function () { it('exactly buffer size', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i; for (let i = 0; i < 5; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).buffer(5); const s = new Stream(values).buffer(5);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -276,11 +254,11 @@ describe(__filename, function () {
assert.deepEqual([...s], [1, 2, 3, 4]); assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4); assert.equal(lastYield, 4);
}); });
it('more than buffer size', async function () { it('more than buffer size', async function () {
let lastYield = null; let lastYield = null;
const values = (function* () { const values = (function* () {
for (let i = 0; i < 10; i++) yield lastYield = i; for (let i = 0; i < 10; i++)
yield lastYield = i;
})(); })();
const s = new Stream(values).buffer(3); const s = new Stream(values).buffer(3);
assert.equal(lastYield, null); assert.equal(lastYield, null);
@ -294,7 +272,6 @@ describe(__filename, function () {
assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]); assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]);
assert.equal(lastYield, 9); assert.equal(lastYield, 9);
}); });
it('buffered Promise rejections are suppressed while iterating', async function () { it('buffered Promise rejections are suppressed while iterating', async function () {
let lastYield = null; let lastYield = null;
const err = new Error('injected'); const err = new Error('injected');
@ -314,7 +291,6 @@ describe(__filename, function () {
await assert.rejects(iter.next().value, err); await assert.rejects(iter.next().value, err);
iter.return(); iter.return();
}); });
it('buffered Promise rejections are unsuppressed when iteration completes', async function () { it('buffered Promise rejections are unsuppressed when iteration completes', async function () {
let lastYield = null; let lastYield = null;
const err = new Error('injected'); const err = new Error('injected');
@ -333,25 +309,21 @@ describe(__filename, function () {
await assertUnhandledRejection(() => iter.return(), err); await assertUnhandledRejection(() => iter.return(), err);
}); });
}); });
describe('map', function () { describe('map', function () {
it('empty', async function () { it('empty', async function () {
let called = false; let called = false;
assert.deepEqual([...new Stream([]).map((v) => called = true)], []); assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
assert.equal(called, false); assert.equal(called, false);
}); });
it('does not start until needed', async function () { it('does not start until needed', async function () {
let called = false; let called = false;
assert.deepEqual([...new Stream([]).map((v) => called = true)], []); assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
new Stream((function* () { yield 0; })()).map((v) => called = true); new Stream((function* () { yield 0; })()).map((v) => called = true);
assert.equal(called, false); assert.equal(called, false);
}); });
it('works', async function () { it('works', async function () {
const calls = []; const calls = [];
assert.deepEqual( assert.deepEqual([...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
[...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
assert.deepEqual(calls, [0, 1, 2]); assert.deepEqual(calls, [0, 1, 2]);
}); });
}); });

View file

@ -1,47 +1,32 @@
import * as common from "../../common.js";
import { validate } from "openapi-schema-validation";
'use strict'; 'use strict';
const validateOpenAPI = { validate }.validate;
/**
* API specs
*
* Tests for generic overarching HTTP API related features not related to any
* specific part of the data model or domain. For example: tests for versioning
* and openapi definitions.
*/
const common = require('../../common');
const validateOpenAPI = require('openapi-schema-validation').validate;
let agent; let agent;
const apiKey = common.apiKey; const apiKey = common.apiKey;
let apiVersion = 1; let apiVersion = 1;
const makeid = () => { const makeid = () => {
let text = ''; let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += possible.charAt(Math.floor(Math.random() * possible.length));
} }
return text; return text;
}; };
const testPadId = makeid(); const testPadId = makeid();
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () { describe(__filename, function () {
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
it('can obtain API version', async function () { it('can obtain API version', async function () {
await agent.get('/api/') await agent.get('/api/')
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
apiVersion = res.body.currentVersion; apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API'); if (!res.body.currentVersion)
throw new Error('No version set in API');
return; return;
}); });
}); });
it('can obtain valid openapi definition document', async function () { it('can obtain valid openapi definition document', async function () {
this.timeout(15000); this.timeout(15000);
await agent.get('/api/openapi.json') await agent.get('/api/openapi.json')

View file

@ -1,40 +1,33 @@
import assert$0 from "assert";
import * as common from "../../common.js";
import fs from "fs";
'use strict'; 'use strict';
/* /*
* This file is copied & modified from <basedir>/src/tests/backend/specs/api/pad.js * This file is copied & modified from <basedir>/src/tests/backend/specs/api/pad.js
* *
* TODO: maybe unify those two files and merge in a single one. * TODO: maybe unify those two files and merge in a single one.
*/ */
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../../common');
const fs = require('fs');
const fsp = fs.promises; const fsp = fs.promises;
let agent; let agent;
const apiKey = common.apiKey; const apiKey = common.apiKey;
let apiVersion = 1; let apiVersion = 1;
const testPadId = makeid(); const testPadId = makeid();
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () { describe(__filename, function () {
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
describe('Sanity checks', function () { describe('Sanity checks', function () {
it('can connect', async function () { it('can connect', async function () {
await agent.get('/api/') await agent.get('/api/')
.expect(200) .expect(200)
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
}); });
it('finds the version tag', async function () { it('finds the version tag', async function () {
const res = await agent.get('/api/') const res = await agent.get('/api/')
.expect(200); .expect(200);
apiVersion = res.body.currentVersion; apiVersion = res.body.currentVersion;
assert(apiVersion); assert(apiVersion);
}); });
it('errors with invalid APIKey', async function () { it('errors with invalid APIKey', async function () {
// This is broken because Etherpad doesn't handle HTTP codes properly see #2343 // This is broken because Etherpad doesn't handle HTTP codes properly see #2343
// If your APIKey is password you deserve to fail all tests anyway // If your APIKey is password you deserve to fail all tests anyway
@ -42,7 +35,6 @@ describe(__filename, function () {
.expect(401); .expect(401);
}); });
}); });
describe('Tests', function () { describe('Tests', function () {
it('creates a new Pad', async function () { it('creates a new Pad', async function () {
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
@ -50,7 +42,6 @@ describe(__filename, function () {
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
}); });
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () { it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () {
const res = await agent.post(endPoint('setHTML')) const res = await agent.post(endPoint('setHTML'))
.send({ .send({
@ -61,7 +52,6 @@ describe(__filename, function () {
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
}); });
it('get the HTML of Pad with emojis', async function () { it('get the HTML of Pad with emojis', async function () {
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect(200) .expect(200)
@ -70,17 +60,14 @@ describe(__filename, function () {
}); });
}); });
}); });
/* /*
End of test End of test
*/ */
function makeid() { function makeid() {
let text = ''; let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += possible.charAt(Math.floor(Math.random() * possible.length));
} }

View file

@ -1,35 +1,29 @@
import * as common from "../../common.js";
'use strict'; 'use strict';
const common = require('../../common');
let agent; let agent;
const apiKey = common.apiKey; const apiKey = common.apiKey;
let apiVersion = 1; let apiVersion = 1;
let authorID = ''; let authorID = '';
const padID = makeid(); const padID = makeid();
const timestamp = Date.now(); const timestamp = Date.now();
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`; const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () { describe(__filename, function () {
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
describe('API Versioning', function () { describe('API Versioning', function () {
it('errors if can not connect', function (done) { it('errors if can not connect', function (done) {
agent.get('/api/') agent.get('/api/')
.expect((res) => { .expect((res) => {
apiVersion = res.body.currentVersion; apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API'); if (!res.body.currentVersion)
throw new Error('No version set in API');
return; return;
}) })
.expect(200, done); .expect(200, done);
}); });
}); });
// BEGIN GROUP AND AUTHOR TESTS // BEGIN GROUP AND AUTHOR TESTS
// /////////////////////////////////// // ///////////////////////////////////
// /////////////////////////////////// // ///////////////////////////////////
/* Tests performed /* Tests performed
-> createPad(padID) -> createPad(padID)
-> createAuthor([name]) -- should return an authorID -> createAuthor([name]) -- should return an authorID
@ -37,18 +31,17 @@ describe(__filename, function () {
-> getChatHead(padID) -> getChatHead(padID)
-> getChatHistory(padID) -> getChatHistory(padID)
*/ */
describe('createPad', function () { describe('createPad', function () {
it('creates a new Pad', function (done) { it('creates a new Pad', function (done) {
agent.get(`${endPoint('createPad')}&padID=${padID}`) agent.get(`${endPoint('createPad')}&padID=${padID}`)
.expect((res) => { .expect((res) => {
if (res.body.code !== 0) throw new Error('Unable to create new Pad'); if (res.body.code !== 0)
throw new Error('Unable to create new Pad');
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done); .expect(200, done);
}); });
}); });
describe('createAuthor', function () { describe('createAuthor', function () {
it('Creates an author with a name set', function (done) { it('Creates an author with a name set', function (done) {
agent.get(endPoint('createAuthor')) agent.get(endPoint('createAuthor'))
@ -62,33 +55,31 @@ describe(__filename, function () {
.expect(200, done); .expect(200, done);
}); });
}); });
describe('appendChatMessage', function () { describe('appendChatMessage', function () {
it('Adds a chat message to the pad', function (done) { it('Adds a chat message to the pad', function (done) {
agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` + agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
`&authorID=${authorID}&time=${timestamp}`) `&authorID=${authorID}&time=${timestamp}`)
.expect((res) => { .expect((res) => {
if (res.body.code !== 0) throw new Error('Unable to create chat message'); if (res.body.code !== 0)
throw new Error('Unable to create chat message');
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done); .expect(200, done);
}); });
}); });
describe('getChatHead', function () { describe('getChatHead', function () {
it('Gets the head of chat', function (done) { it('Gets the head of chat', function (done) {
agent.get(`${endPoint('getChatHead')}&padID=${padID}`) agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
.expect((res) => { .expect((res) => {
if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong'); if (res.body.data.chatHead !== 0)
throw new Error('Chat Head Length is wrong');
if (res.body.code !== 0) throw new Error('Unable to get chat head'); if (res.body.code !== 0)
throw new Error('Unable to get chat head');
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done); .expect(200, done);
}); });
}); });
describe('getChatHistory', function () { describe('getChatHistory', function () {
it('Gets Chat History of a Pad', function (done) { it('Gets Chat History of a Pad', function (done) {
agent.get(`${endPoint('getChatHistory')}&padID=${padID}`) agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
@ -96,18 +87,17 @@ describe(__filename, function () {
if (res.body.data.messages.length !== 1) { if (res.body.data.messages.length !== 1) {
throw new Error('Chat History Length is wrong'); throw new Error('Chat History Length is wrong');
} }
if (res.body.code !== 0) throw new Error('Unable to get chat history'); if (res.body.code !== 0)
throw new Error('Unable to get chat history');
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200, done); .expect(200, done);
}); });
}); });
}); });
function makeid() { function makeid() {
let text = ''; let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += possible.charAt(Math.floor(Math.random() * possible.length));
} }

View file

@ -1,3 +1,5 @@
import assert$0 from "assert";
import * as common from "../../common.js";
'use strict'; 'use strict';
/* /*
* ACHTUNG: there is a copied & modified version of this file in * ACHTUNG: there is a copied & modified version of this file in
@ -5,16 +7,11 @@
* *
* TODO: unify those two files, and merge in a single one. * TODO: unify those two files, and merge in a single one.
*/ */
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../../common');
let agent; let agent;
const apiKey = common.apiKey; const apiKey = common.apiKey;
const apiVersion = 1; const apiVersion = 1;
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`; const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
const testImports = { const testImports = {
'malformed': { 'malformed': {
input: '<html><body><li>wtf</ul></body></html>', input: '<html><body><li>wtf</ul></body></html>',
@ -43,7 +40,6 @@ const testImports = {
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1<ol start="2" class="number"><li>foo</ol><li>should be 2</ol><br></body></html>', wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1<ol start="2" class="number"><li>foo</ol><li>should be 2</ol><br></body></html>',
wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n', wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n',
}, },
/* /*
"prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": { "prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": {
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>', input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
@ -225,12 +221,9 @@ const testImports = {
wantText: 'Need more space s !\n\n', wantText: 'Need more space s !\n\n',
}, },
}; };
describe(__filename, function () { describe(__filename, function () {
this.timeout(1000); this.timeout(1000);
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
Object.keys(testImports).forEach((testName) => { Object.keys(testImports).forEach((testName) => {
describe(testName, function () { describe(testName, function () {
const testPadId = makeid(); const testPadId = makeid();
@ -240,14 +233,12 @@ describe(__filename, function () {
done(); done();
}); });
} }
it('createPad', async function () { it('createPad', async function () {
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`) const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
.expect(200) .expect(200)
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
}); });
it('setHTML', async function () { it('setHTML', async function () {
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` + const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
`&html=${encodeURIComponent(test.input)}`) `&html=${encodeURIComponent(test.input)}`)
@ -255,14 +246,12 @@ describe(__filename, function () {
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.code, 0); assert.equal(res.body.code, 0);
}); });
it('getHTML', async function () { it('getHTML', async function () {
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`) const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect(200) .expect(200)
.expect('Content-Type', /json/); .expect('Content-Type', /json/);
assert.equal(res.body.data.html, test.wantHTML); assert.equal(res.body.data.html, test.wantHTML);
}); });
it('getText', async function () { it('getText', async function () {
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`) const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
.expect(200) .expect(200)
@ -272,11 +261,9 @@ describe(__filename, function () {
}); });
}); });
}); });
function makeid() { function makeid() {
let text = ''; let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length)); text += possible.charAt(Math.floor(Math.random() * possible.length));
} }

View file

@ -1,18 +1,10 @@
import * as common from "../common.js";
import assertLegacy from "../assert-legacy.js";
import queryString from "querystring";
import * as settings from "../../../node/utils/Settings.js";
'use strict'; 'use strict';
const assert = assertLegacy.strict;
/**
* caching_middleware is responsible for serving everything under path `/javascripts/`
* That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code
*
*/
const common = require('../common');
const assert = require('../assert-legacy').strict;
const queryString = require('querystring');
const settings = require('../../../node/utils/Settings');
let agent; let agent;
/** /**
* Hack! Returns true if the resource is not plaintext * Hack! Returns true if the resource is not plaintext
* The file should start with the callback method, so we need the * The file should start with the callback method, so we need the
@ -27,11 +19,9 @@ const isPlaintextResponse = (fileContent, resource) => {
const query = (new URL(resource, 'http://localhost')).search.slice(1); const query = (new URL(resource, 'http://localhost')).search.slice(1);
// require.define // require.define
const jsonp = queryString.parse(query).callback; const jsonp = queryString.parse(query).callback;
// returns true if the first letters in fileContent equal the content of `jsonp` // returns true if the first letters in fileContent equal the content of `jsonp`
return fileContent.substring(0, jsonp.length) === jsonp; return fileContent.substring(0, jsonp.length) === jsonp;
}; };
/** /**
* A hack to disable `superagent`'s auto unzip functionality * A hack to disable `superagent`'s auto unzip functionality
* *
@ -40,7 +30,6 @@ const isPlaintextResponse = (fileContent, resource) => {
const disableAutoDeflate = (request) => { const disableAutoDeflate = (request) => {
request._shouldUnzip = () => false; request._shouldUnzip = () => false;
}; };
describe(__filename, function () { describe(__filename, function () {
const backups = {}; const backups = {};
const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved
@ -50,7 +39,6 @@ describe(__filename, function () {
'/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define', '/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define',
'/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define', '/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define',
]; ];
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
backups.settings = {}; backups.settings = {};
@ -59,13 +47,11 @@ describe(__filename, function () {
after(async function () { after(async function () {
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
}); });
for (const minify of [false, true]) { for (const minify of [false, true]) {
context(`when minify is ${minify}`, function () { context(`when minify is ${minify}`, function () {
before(async function () { before(async function () {
settings.minify = minify; settings.minify = minify;
}); });
describe('gets packages uncompressed without Accept-Encoding gzip', function () { describe('gets packages uncompressed without Accept-Encoding gzip', function () {
for (const resource of packages) { for (const resource of packages) {
it(resource, async function () { it(resource, async function () {
@ -81,7 +67,6 @@ describe(__filename, function () {
}); });
} }
}); });
describe('gets packages compressed with Accept-Encoding gzip', function () { describe('gets packages compressed with Accept-Encoding gzip', function () {
for (const resource of packages) { for (const resource of packages) {
it(resource, async function () { it(resource, async function () {
@ -97,7 +82,6 @@ describe(__filename, function () {
}); });
} }
}); });
it('does not cache content-encoding headers', async function () { it('does not cache content-encoding headers', async function () {
await agent.get(packages[0]) await agent.get(packages[0])
.set('Accept-Encoding', fantasyEncoding) .set('Accept-Encoding', fantasyEncoding)

View file

@ -1,27 +1,28 @@
import ChatMessage from "../../../static/js/ChatMessage.js";
import { Pad } from "../../../node/db/Pad.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as pluginDefs from "../../../static/js/pluginfw/plugin_defs.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const ChatMessage = require('../../../static/js/ChatMessage');
const {Pad} = require('../../../node/db/Pad');
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
const logger = common.logger; const logger = common.logger;
const checkHook = async (hookName, checkFn) => { const checkHook = async (hookName, checkFn) => {
if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = []; if (pluginDefs.hooks[hookName] == null)
pluginDefs.hooks[hookName] = [];
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
pluginDefs.hooks[hookName].push({ pluginDefs.hooks[hookName].push({
hook_fn: async (hookName, context) => { hook_fn: async (hookName, context) => {
if (checkFn == null) return; if (checkFn == null)
return;
logger.debug(`hook ${hookName} invoked`); logger.debug(`hook ${hookName} invoked`);
try { try {
// Make sure checkFn is called only once. // Make sure checkFn is called only once.
const _checkFn = checkFn; const _checkFn = checkFn;
checkFn = null; checkFn = null;
await _checkFn(context); await _checkFn(context);
} catch (err) { }
catch (err) {
reject(err); reject(err);
return; return;
} }
@ -30,7 +31,6 @@ const checkHook = async (hookName, checkFn) => {
}); });
}); });
}; };
const sendMessage = (socket, data) => { const sendMessage = (socket, data) => {
socket.send({ socket.send({
type: 'COLLABROOM', type: 'COLLABROOM',
@ -38,52 +38,47 @@ const sendMessage = (socket, data) => {
data, data,
}); });
}; };
const sendChat = (socket, message) => sendMessage(socket, { type: 'CHAT_MESSAGE', message }); const sendChat = (socket, message) => sendMessage(socket, { type: 'CHAT_MESSAGE', message });
describe(__filename, function () { describe(__filename, function () {
const padId = 'testChatPad'; const padId = 'testChatPad';
const hooksBackup = {}; const hooksBackup = {};
before(async function () { before(async function () {
for (const [name, defs] of Object.entries(pluginDefs.hooks)) { for (const [name, defs] of Object.entries(pluginDefs.hooks)) {
if (defs == null) continue; if (defs == null)
continue;
hooksBackup[name] = defs; hooksBackup[name] = defs;
} }
}); });
beforeEach(async function () { beforeEach(async function () {
for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs]; for (const [name, defs] of Object.entries(hooksBackup))
pluginDefs.hooks[name] = [...defs];
for (const name of Object.keys(pluginDefs.hooks)) { for (const name of Object.keys(pluginDefs.hooks)) {
if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; if (hooksBackup[name] == null)
delete pluginDefs.hooks[name];
} }
if (await padManager.doesPadExist(padId)) { if (await padManager.doesPadExist(padId)) {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
await pad.remove(); await pad.remove();
} }
}); });
after(async function () { after(async function () {
Object.assign(pluginDefs.hooks, hooksBackup); Object.assign(pluginDefs.hooks, hooksBackup);
for (const name of Object.keys(pluginDefs.hooks)) { for (const name of Object.keys(pluginDefs.hooks)) {
if (hooksBackup[name] == null) delete pluginDefs.hooks[name]; if (hooksBackup[name] == null)
delete pluginDefs.hooks[name];
} }
}); });
describe('chatNewMessage hook', function () { describe('chatNewMessage hook', function () {
let authorId; let authorId;
let socket; let socket;
beforeEach(async function () { beforeEach(async function () {
socket = await common.connect(); socket = await common.connect();
const { data: clientVars } = await common.handshake(socket, padId); const { data: clientVars } = await common.handshake(socket, padId);
authorId = clientVars.userId; authorId = clientVars.userId;
}); });
afterEach(async function () { afterEach(async function () {
socket.close(); socket.close();
}); });
it('message', async function () { it('message', async function () {
const start = Date.now(); const start = Date.now();
await Promise.all([ await Promise.all([
@ -98,7 +93,6 @@ describe(__filename, function () {
sendChat(socket, { text: this.test.title }), sendChat(socket, { text: this.test.title }),
]); ]);
}); });
it('pad', async function () { it('pad', async function () {
await Promise.all([ await Promise.all([
checkHook('chatNewMessage', ({ pad }) => { checkHook('chatNewMessage', ({ pad }) => {
@ -109,7 +103,6 @@ describe(__filename, function () {
sendChat(socket, { text: this.test.title }), sendChat(socket, { text: this.test.title }),
]); ]);
}); });
it('padId', async function () { it('padId', async function () {
await Promise.all([ await Promise.all([
checkHook('chatNewMessage', (context) => { checkHook('chatNewMessage', (context) => {
@ -118,18 +111,18 @@ describe(__filename, function () {
sendChat(socket, { text: this.test.title }), sendChat(socket, { text: this.test.title }),
]); ]);
}); });
it('mutations propagate', async function () { it('mutations propagate', async function () {
const listen = async (type) => await new Promise((resolve) => { const listen = async (type) => await new Promise((resolve) => {
const handler = (msg) => { const handler = (msg) => {
if (msg.type !== 'COLLABROOM') return; if (msg.type !== 'COLLABROOM')
if (msg.data == null || msg.data.type !== type) return; return;
if (msg.data == null || msg.data.type !== type)
return;
resolve(msg.data); resolve(msg.data);
socket.off('message', handler); socket.off('message', handler);
}; };
socket.on('message', handler); socket.on('message', handler);
}); });
const modifiedText = `${this.test.title} <added changes>`; const modifiedText = `${this.test.title} <added changes>`;
const customMetadata = { foo: this.test.title }; const customMetadata = { foo: this.test.title };
await Promise.all([ await Promise.all([

View file

@ -1,21 +1,11 @@
import AttributePool from "../../../static/js/AttributePool.js";
import * as Changeset from "../../../static/js/Changeset.js";
import assert$0 from "assert";
import * as attributes from "../../../static/js/attributes.js";
import * as contentcollector from "../../../static/js/contentcollector.js";
import * as jsdom from "jsdom";
'use strict'; 'use strict';
const assert = assert$0.strict;
/*
* While importexport tests target the `setHTML` API endpoint, which is nearly identical to what
* happens when a user manually imports a document via the UI, the contentcollector tests here don't
* use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the
* expected results here can differ from importexport.js.
*
* If you add tests here, please also add them to importexport.js
*/
const AttributePool = require('../../../static/js/AttributePool');
const Changeset = require('../../../static/js/Changeset');
const assert = require('assert').strict;
const attributes = require('../../../static/js/attributes');
const contentcollector = require('../../../static/js/contentcollector');
const jsdom = require('jsdom');
// All test case `wantAlines` values must only refer to attributes in this list so that the // All test case `wantAlines` values must only refer to attributes in this list so that the
// attribute numbers do not change due to changes in pool insertion order. // attribute numbers do not change due to changes in pool insertion order.
const knownAttribs = [ const knownAttribs = [
@ -29,7 +19,6 @@ const knownAttribs = [
['start', '1'], ['start', '1'],
['start', '2'], ['start', '2'],
]; ];
const testCases = [ const testCases = [
{ {
description: 'Simple', description: 'Simple',
@ -330,21 +319,21 @@ pre
wantText: ['Need more space s !'], wantText: ['Need more space s !'],
}, },
]; ];
describe(__filename, function () { describe(__filename, function () {
for (const tc of testCases) { for (const tc of testCases) {
describe(tc.description, function () { describe(tc.description, function () {
let apool; let apool;
let result; let result;
before(async function () { before(async function () {
if (tc.disabled) return this.skip(); if (tc.disabled)
return this.skip();
const { window: { document } } = new jsdom.JSDOM(tc.html); const { window: { document } } = new jsdom.JSDOM(tc.html);
apool = new AttributePool(); apool = new AttributePool();
// To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all // To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
// attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute // attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
// numbers do not change if the attribute processing code changes.) // numbers do not change if the attribute processing code changes.)
for (const attrib of knownAttribs) apool.putAttrib(attrib); for (const attrib of knownAttribs)
apool.putAttrib(attrib);
for (const aline of tc.wantAlines) { for (const aline of tc.wantAlines) {
for (const op of Changeset.deserializeOps(aline)) { for (const op of Changeset.deserializeOps(aline)) {
for (const n of attributes.decodeAttribString(op.attribs)) { for (const n of attributes.decodeAttribString(op.attribs)) {
@ -356,15 +345,12 @@ describe(__filename, function () {
cc.collectContent(document.body); cc.collectContent(document.body);
result = cc.finish(); result = cc.finish();
}); });
it('text matches', async function () { it('text matches', async function () {
assert.deepEqual(result.lines, tc.wantText); assert.deepEqual(result.lines, tc.wantText);
}); });
it('alines match', async function () { it('alines match', async function () {
assert.deepEqual(result.lineAttribs, tc.wantAlines); assert.deepEqual(result.lineAttribs, tc.wantAlines);
}); });
it('attributes are sorted in canonical order', async function () { it('attributes are sorted in canonical order', async function () {
const gotAttribs = []; const gotAttribs = [];
const wantAttribs = []; const wantAttribs = [];

View file

@ -1,23 +1,18 @@
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict'; 'use strict';
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () { describe(__filename, function () {
let agent; let agent;
const settingsBackup = {}; const settingsBackup = {};
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
settingsBackup.soffice = settings.soffice; settingsBackup.soffice = settings.soffice;
await padManager.getPad('testExportPad', 'test content'); await padManager.getPad('testExportPad', 'test content');
}); });
after(async function () { after(async function () {
Object.assign(settings, settingsBackup); Object.assign(settings, settingsBackup);
}); });
it('returns 500 on export error', async function () { it('returns 500 on export error', async function () {
settings.soffice = 'false'; // '/bin/false' doesn't work on Windows settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
await agent.get('/p/testExportPad/export/doc') await agent.get('/p/testExportPad/export/doc')

View file

@ -1,13 +1,12 @@
import assert$0 from "assert";
import * as common from "../common.js";
import fs from "fs";
import path from "path";
import * as settings from "../../../node/utils/Settings.js";
import superagent from "superagent";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const fs = require('fs');
const fsp = fs.promises; const fsp = fs.promises;
const path = require('path');
const settings = require('../../../node/utils/Settings');
const superagent = require('superagent');
describe(__filename, function () { describe(__filename, function () {
let agent; let agent;
let backupSettings; let backupSettings;
@ -15,20 +14,17 @@ describe(__filename, function () {
let wantCustomIcon; let wantCustomIcon;
let wantDefaultIcon; let wantDefaultIcon;
let wantSkinIcon; let wantSkinIcon;
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png')); wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png'));
wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico')); wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico'));
wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png')); wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png'));
}); });
beforeEach(async function () { beforeEach(async function () {
backupSettings = { ...settings }; backupSettings = { ...settings };
skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-')); skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-'));
settings.skinName = path.basename(skinDir); settings.skinName = path.basename(skinDir);
}); });
afterEach(async function () { afterEach(async function () {
delete settings.favicon; delete settings.favicon;
delete settings.skinName; delete settings.skinName;
@ -38,9 +34,9 @@ describe(__filename, function () {
// can't rely on it until support for Node.js v10 is dropped. // can't rely on it until support for Node.js v10 is dropped.
await fsp.unlink(path.join(skinDir, 'favicon.ico')); await fsp.unlink(path.join(skinDir, 'favicon.ico'));
await fsp.rmdir(skinDir, { recursive: true }); await fsp.rmdir(skinDir, { recursive: true });
} catch (err) { /* intentionally ignored */ } }
catch (err) { /* intentionally ignored */ }
}); });
it('uses custom favicon if set (relative pathname)', async function () { it('uses custom favicon if set (relative pathname)', async function () {
settings.favicon = settings.favicon =
path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png')); path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png'));
@ -50,7 +46,6 @@ describe(__filename, function () {
.expect(200); .expect(200);
assert(gotIcon.equals(wantCustomIcon)); assert(gotIcon.equals(wantCustomIcon));
}); });
it('uses custom favicon if set (absolute pathname)', async function () { it('uses custom favicon if set (absolute pathname)', async function () {
settings.favicon = path.join(__dirname, 'favicon-test-custom.png'); settings.favicon = path.join(__dirname, 'favicon-test-custom.png');
assert(path.isAbsolute(settings.favicon)); assert(path.isAbsolute(settings.favicon));
@ -59,7 +54,6 @@ describe(__filename, function () {
.expect(200); .expect(200);
assert(gotIcon.equals(wantCustomIcon)); assert(gotIcon.equals(wantCustomIcon));
}); });
it('falls back if custom favicon is missing', async function () { it('falls back if custom favicon is missing', async function () {
// The previous default for settings.favicon was 'favicon.ico', so many users will continue to // The previous default for settings.favicon was 'favicon.ico', so many users will continue to
// have that in their settings.json for a long time. There is unlikely to be a favicon at // have that in their settings.json for a long time. There is unlikely to be a favicon at
@ -71,7 +65,6 @@ describe(__filename, function () {
.expect(200); .expect(200);
assert(gotIcon.equals(wantDefaultIcon)); assert(gotIcon.equals(wantDefaultIcon));
}); });
it('uses skin favicon if present', async function () { it('uses skin favicon if present', async function () {
await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon); await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon);
settings.favicon = null; settings.favicon = null;
@ -80,7 +73,6 @@ describe(__filename, function () {
.expect(200); .expect(200);
assert(gotIcon.equals(wantSkinIcon)); assert(gotIcon.equals(wantSkinIcon));
}); });
it('falls back to default favicon', async function () { it('falls back to default favicon', async function () {
settings.favicon = null; settings.favicon = null;
const { body: gotIcon } = await agent.get('/favicon.ico') const { body: gotIcon } = await agent.get('/favicon.ico')

View file

@ -1,49 +1,41 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as settings from "../../../node/utils/Settings.js";
import superagent from "superagent";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const settings = require('../../../node/utils/Settings');
const superagent = require('superagent');
describe(__filename, function () { describe(__filename, function () {
let agent; let agent;
const backup = {}; const backup = {};
const getHealth = () => agent.get('/health') const getHealth = () => agent.get('/health')
.accept('application/health+json') .accept('application/health+json')
.buffer(true) .buffer(true)
.parse(superagent.parse['application/json']) .parse(superagent.parse['application/json'])
.expect(200) .expect(200)
.expect((res) => assert.equal(res.type, 'application/health+json')); .expect((res) => assert.equal(res.type, 'application/health+json'));
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
}); });
beforeEach(async function () { beforeEach(async function () {
backup.settings = {}; backup.settings = {};
for (const setting of ['requireAuthentication', 'requireAuthorization']) { for (const setting of ['requireAuthentication', 'requireAuthorization']) {
backup.settings[setting] = settings[setting]; backup.settings[setting] = settings[setting];
} }
}); });
afterEach(async function () { afterEach(async function () {
Object.assign(settings, backup.settings); Object.assign(settings, backup.settings);
}); });
it('/health works', async function () { it('/health works', async function () {
const res = await getHealth(); const res = await getHealth();
assert.equal(res.body.status, 'pass'); assert.equal(res.body.status, 'pass');
assert.equal(res.body.releaseId, settings.getEpVersion()); assert.equal(res.body.releaseId, settings.getEpVersion());
}); });
it('auth is not required', async function () { it('auth is not required', async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
const res = await getHealth(); const res = await getHealth();
assert.equal(res.body.status, 'pass'); assert.equal(res.body.status, 'pass');
}); });
// We actually want to test that no express-session state is created, but that is difficult to do // We actually want to test that no express-session state is created, but that is difficult to do
// without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a // without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
// cookie means that no express-session state was created (how would express-session look up the // cookie means that no express-session state was created (how would express-session look up the

View file

@ -1,37 +1,32 @@
import assertLegacy from "../assert-legacy.js";
import * as hooks from "../../../static/js/pluginfw/hooks.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import sinon from "sinon";
'use strict'; 'use strict';
const assert = assertLegacy.strict;
const assert = require('../assert-legacy').strict;
const hooks = require('../../../static/js/pluginfw/hooks');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const sinon = require('sinon');
describe(__filename, function () { describe(__filename, function () {
const hookName = 'testHook'; const hookName = 'testHook';
const hookFnName = 'testPluginFileName:testHookFunctionName'; const hookFnName = 'testPluginFileName:testHookFunctionName';
let testHooks; // Convenience shorthand for plugins.hooks[hookName]. let testHooks; // Convenience shorthand for plugins.hooks[hookName].
let hook; // Convenience shorthand for plugins.hooks[hookName][0]. let hook; // Convenience shorthand for plugins.hooks[hookName][0].
beforeEach(async function () { beforeEach(async function () {
// Make sure these are not already set so that we don't accidentally step on someone else's // Make sure these are not already set so that we don't accidentally step on someone else's
// toes: // toes:
assert(plugins.hooks[hookName] == null); assert(plugins.hooks[hookName] == null);
assert(hooks.deprecationNotices[hookName] == null); assert(hooks.deprecationNotices[hookName] == null);
assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null); assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null);
// Many of the tests only need a single registered hook function. Set that up here to reduce // Many of the tests only need a single registered hook function. Set that up here to reduce
// boilerplate. // boilerplate.
hook = makeHook(); hook = makeHook();
plugins.hooks[hookName] = [hook]; plugins.hooks[hookName] = [hook];
testHooks = plugins.hooks[hookName]; testHooks = plugins.hooks[hookName];
}); });
afterEach(async function () { afterEach(async function () {
sinon.restore(); sinon.restore();
delete plugins.hooks[hookName]; delete plugins.hooks[hookName];
delete hooks.deprecationNotices[hookName]; delete hooks.deprecationNotices[hookName];
delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName]; delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName];
}); });
const makeHook = (ret) => ({ const makeHook = (ret) => ({
hook_name: hookName, hook_name: hookName,
// Many tests will likely want to change this. Unfortunately, we can't use a convenience // Many tests will likely want to change this. Unfortunately, we can't use a convenience
@ -41,7 +36,6 @@ describe(__filename, function () {
hook_fn_name: hookFnName, hook_fn_name: hookFnName,
part: { plugin: 'testPluginName' }, part: { plugin: 'testPluginName' },
}); });
// Hook functions that should work for both synchronous and asynchronous hooks. // Hook functions that should work for both synchronous and asynchronous hooks.
const supportedSyncHookFunctions = [ const supportedSyncHookFunctions = [
{ {
@ -87,30 +81,25 @@ describe(__filename, function () {
syncOk: true, syncOk: true,
}, },
]; ];
describe('callHookFnSync', function () { describe('callHookFnSync', function () {
const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand. const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand.
describe('basic behavior', function () { describe('basic behavior', function () {
it('passes hook name', async function () { it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
callHookFnSync(hook); callHookFnSync(hook);
}); });
it('passes context', async function () { it('passes context', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); };
callHookFnSync(hook, val); callHookFnSync(hook, val);
} }
}); });
it('returns the value provided to the callback', async function () { it('returns the value provided to the callback', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; hook.hook_fn = (hn, ctx, cb) => { cb(ctx); };
assert.equal(callHookFnSync(hook, val), val); assert.equal(callHookFnSync(hook, val), val);
} }
}); });
it('returns the value returned by the hook function', async function () { it('returns the value returned by the hook function', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
// Must not have the cb parameter otherwise returning undefined will error. // Must not have the cb parameter otherwise returning undefined will error.
@ -118,17 +107,14 @@ describe(__filename, function () {
assert.equal(callHookFnSync(hook, val), val); assert.equal(callHookFnSync(hook, val), val);
} }
}); });
it('does not catch exceptions', async function () { it('does not catch exceptions', async function () {
hook.hook_fn = () => { throw new Error('test exception'); }; hook.hook_fn = () => { throw new Error('test exception'); };
assert.throws(() => callHookFnSync(hook), { message: 'test exception' }); assert.throws(() => callHookFnSync(hook), { message: 'test exception' });
}); });
it('callback returns undefined', async function () { it('callback returns undefined', async function () {
hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); };
callHookFnSync(hook); callHookFnSync(hook);
}); });
it('checks for deprecation', async function () { it('checks for deprecation', async function () {
sinon.stub(console, 'warn'); sinon.stub(console, 'warn');
hooks.deprecationNotices[hookName] = 'test deprecation'; hooks.deprecationNotices[hookName] = 'test deprecation';
@ -138,7 +124,6 @@ describe(__filename, function () {
assert.match(console.warn.getCall(0).args[0], /test deprecation/); assert.match(console.warn.getCall(0).args[0], /test deprecation/);
}); });
}); });
describe('supported hook function styles', function () { describe('supported hook function styles', function () {
for (const tc of supportedSyncHookFunctions) { for (const tc of supportedSyncHookFunctions) {
it(tc.name, async function () { it(tc.name, async function () {
@ -148,7 +133,8 @@ describe(__filename, function () {
const call = () => callHookFnSync(hook); const call = () => callHookFnSync(hook);
if (tc.wantErr) { if (tc.wantErr) {
assert.throws(call, { message: tc.wantErr }); assert.throws(call, { message: tc.wantErr });
} else { }
else {
assert.equal(call(), tc.want); assert.equal(call(), tc.want);
} }
assert.equal(console.warn.callCount, 0); assert.equal(console.warn.callCount, 0);
@ -156,11 +142,9 @@ describe(__filename, function () {
}); });
} }
}); });
describe('bad hook function behavior (other than double settle)', function () { describe('bad hook function behavior (other than double settle)', function () {
const promise1 = Promise.resolve('val1'); const promise1 = Promise.resolve('val1');
const promise2 = Promise.resolve('val2'); const promise2 = Promise.resolve('val2');
const testCases = [ const testCases = [
{ {
name: 'never settles -> buggy hook detected', name: 'never settles -> buggy hook detected',
@ -183,25 +167,23 @@ describe(__filename, function () {
wantError: /PROHIBITED PROMISE BUG/, wantError: /PROHIBITED PROMISE BUG/,
}, },
]; ];
for (const tc of testCases) { for (const tc of testCases) {
it(tc.name, async function () { it(tc.name, async function () {
sinon.stub(console, 'error'); sinon.stub(console, 'error');
hook.hook_fn = tc.fn; hook.hook_fn = tc.fn;
assert.equal(callHookFnSync(hook), tc.wantVal); assert.equal(callHookFnSync(hook), tc.wantVal);
assert.equal(console.error.callCount, tc.wantError ? 1 : 0); assert.equal(console.error.callCount, tc.wantError ? 1 : 0);
if (tc.wantError) assert.match(console.error.getCall(0).args[0], tc.wantError); if (tc.wantError)
assert.match(console.error.getCall(0).args[0], tc.wantError);
}); });
} }
}); });
// Test various ways a hook might attempt to settle twice. (Examples: call the callback a second // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
// time, or call the callback and then return a value.) // time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () { describe('bad hook function behavior (double settle)', function () {
beforeEach(async function () { beforeEach(async function () {
sinon.stub(console, 'error'); sinon.stub(console, 'error');
}); });
// Each item in this array codifies a way to settle a synchronous hook function. Each of the // Each item in this array codifies a way to settle a synchronous hook function. Each of the
// test cases below combines two of these behaviors in a single hook function and confirms // test cases below combines two of these behaviors in a single hook function and confirms
// that callHookFnSync both (1) returns the result of the first settle attempt, and // that callHookFnSync both (1) returns the result of the first settle attempt, and
@ -226,22 +208,21 @@ describe(__filename, function () {
async: true, async: true,
}, },
]; ];
for (const step1 of behaviors) { for (const step1 of behaviors) {
// There can't be a second step if the first step is to return or throw. // There can't be a second step if the first step is to return or throw.
if (step1.name.startsWith('return ') || step1.name === 'throw') continue; if (step1.name.startsWith('return ') || step1.name === 'throw')
continue;
for (const step2 of behaviors) { for (const step2 of behaviors) {
// If step1 and step2 are both async then there would be three settle attempts (first an // If step1 and step2 are both async then there would be three settle attempts (first an
// erroneous unsettled return, then async step 1, then async step 2). Handling triple // erroneous unsettled return, then async step 1, then async step 2). Handling triple
// settle would complicate the tests, and it is sufficient to test only double settles. // settle would complicate the tests, and it is sufficient to test only double settles.
if (step1.async && step2.async) continue; if (step1.async && step2.async)
continue;
it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
hook.hook_fn = (hn, ctx, cb) => { hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, new Error(ctx.ret1), ctx.ret1); step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
}; };
// Temporarily remove unhandled error listeners so that the errors we expect to see // Temporarily remove unhandled error listeners so that the errors we expect to see
// don't trigger a test failure (or terminate node). // don't trigger a test failure (or terminate node).
const events = ['uncaughtException', 'unhandledRejection']; const events = ['uncaughtException', 'unhandledRejection'];
@ -250,7 +231,6 @@ describe(__filename, function () {
listenerBackups[event] = process.rawListeners(event); listenerBackups[event] = process.rawListeners(event);
process.removeAllListeners(event); process.removeAllListeners(event);
} }
// We should see an asynchronous error (either an unhandled Promise rejection or an // We should see an asynchronous error (either an unhandled Promise rejection or an
// uncaught exception) if and only if one of the two steps was asynchronous or there was // uncaught exception) if and only if one of the two steps was asynchronous or there was
// a throw (in which case the double settle is deferred so that the caller sees the // a throw (in which case the double settle is deferred so that the caller sees the
@ -265,19 +245,23 @@ describe(__filename, function () {
asyncErr = err; asyncErr = err;
resolve(); resolve();
}; };
if (!wantAsyncErr) resolve(); if (!wantAsyncErr)
resolve();
}); });
events.forEach((event) => process.on(event, tempListener)); events.forEach((event) => process.on(event, tempListener));
const call = () => callHookFnSync(hook, { ret1: 'val1', ret2: 'val2' }); const call = () => callHookFnSync(hook, { ret1: 'val1', ret2: 'val2' });
if (step2.rejects) { if (step2.rejects) {
assert.throws(call, { message: 'val2' }); assert.throws(call, { message: 'val2' });
} else if (!step1.async && !step2.async) { }
else if (!step1.async && !step2.async) {
assert.throws(call, { message: /DOUBLE SETTLE BUG/ }); assert.throws(call, { message: /DOUBLE SETTLE BUG/ });
} else { }
else {
assert.equal(call(), step1.async ? 'val2' : 'val1'); assert.equal(call(), step1.async ? 'val2' : 'val1');
} }
await seenErrPromise; await seenErrPromise;
} finally { }
finally {
// Restore the original listeners. // Restore the original listeners.
for (const event of events) { for (const event of events) {
process.off(event, tempListener); process.off(event, tempListener);
@ -293,24 +277,23 @@ describe(__filename, function () {
assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
} }
}); });
// This next test is the same as the above test, except the second settle attempt is for // This next test is the same as the above test, except the second settle attempt is for
// the same outcome. The two outcomes can't be the same if one step throws and the other // the same outcome. The two outcomes can't be the same if one step throws and the other
// doesn't, so skip those cases. // doesn't, so skip those cases.
if (step1.rejects !== step2.rejects) continue; if (step1.rejects !== step2.rejects)
continue;
it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
const err = new Error('val'); const err = new Error('val');
hook.hook_fn = (hn, ctx, cb) => { hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, err, 'val'); step1.fn(cb, err, 'val');
return step2.fn(cb, err, 'val'); return step2.fn(cb, err, 'val');
}; };
const errorLogged = new Promise((resolve) => console.error.callsFake(resolve)); const errorLogged = new Promise((resolve) => console.error.callsFake(resolve));
const call = () => callHookFnSync(hook); const call = () => callHookFnSync(hook);
if (step2.rejects) { if (step2.rejects) {
assert.throws(call, { message: 'val' }); assert.throws(call, { message: 'val' });
} else { }
else {
assert.equal(call(), 'val'); assert.equal(call(), 'val');
} }
await errorLogged; await errorLogged;
@ -321,7 +304,6 @@ describe(__filename, function () {
} }
}); });
}); });
describe('hooks.callAll', function () { describe('hooks.callAll', function () {
describe('basic behavior', function () { describe('basic behavior', function () {
it('calls all in order', async function () { it('calls all in order', async function () {
@ -329,58 +311,48 @@ describe(__filename, function () {
testHooks.push(makeHook(1), makeHook(2), makeHook(3)); testHooks.push(makeHook(1), makeHook(2), makeHook(3));
assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]); assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]);
}); });
it('passes hook name', async function () { it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
hooks.callAll(hookName); hooks.callAll(hookName);
}); });
it('undefined context -> {}', async function () { it('undefined context -> {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callAll(hookName); hooks.callAll(hookName);
}); });
it('null context -> {}', async function () { it('null context -> {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callAll(hookName, null); hooks.callAll(hookName, null);
}); });
it('context unmodified', async function () { it('context unmodified', async function () {
const wantContext = {}; const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
hooks.callAll(hookName, wantContext); hooks.callAll(hookName, wantContext);
}); });
}); });
describe('result processing', function () { describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () { it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook; delete plugins.hooks.testHook;
assert.deepEqual(hooks.callAll(hookName), []); assert.deepEqual(hooks.callAll(hookName), []);
}); });
it('no registered hooks (empty list) -> []', async function () { it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
assert.deepEqual(hooks.callAll(hookName), []); assert.deepEqual(hooks.callAll(hookName), []);
}); });
it('flattens one level', async function () { it('flattens one level', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]); assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]);
}); });
it('filters out undefined', async function () { it('filters out undefined', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]])); testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [2, [3]]); assert.deepEqual(hooks.callAll(hookName), [2, [3]]);
}); });
it('preserves null', async function () { it('preserves null', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]])); testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]); assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]);
}); });
it('all undefined -> []', async function () { it('all undefined -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook()); testHooks.push(makeHook(), makeHook());
@ -388,39 +360,32 @@ describe(__filename, function () {
}); });
}); });
}); });
describe('hooks.callFirst', function () { describe('hooks.callFirst', function () {
it('no registered hooks (undefined) -> []', async function () { it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook; delete plugins.hooks.testHook;
assert.deepEqual(hooks.callFirst(hookName), []); assert.deepEqual(hooks.callFirst(hookName), []);
}); });
it('no registered hooks (empty list) -> []', async function () { it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
assert.deepEqual(hooks.callFirst(hookName), []); assert.deepEqual(hooks.callFirst(hookName), []);
}); });
it('passes hook name => {}', async function () { it('passes hook name => {}', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
hooks.callFirst(hookName); hooks.callFirst(hookName);
}); });
it('undefined context => {}', async function () { it('undefined context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callFirst(hookName); hooks.callFirst(hookName);
}); });
it('null context => {}', async function () { it('null context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callFirst(hookName, null); hooks.callFirst(hookName, null);
}); });
it('context unmodified', async function () { it('context unmodified', async function () {
const wantContext = {}; const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
hooks.callFirst(hookName, wantContext); hooks.callFirst(hookName, wantContext);
}); });
it('predicate never satisfied -> calls all in order', async function () { it('predicate never satisfied -> calls all in order', async function () {
const gotCalls = []; const gotCalls = [];
testHooks.length = 0; testHooks.length = 0;
@ -432,38 +397,32 @@ describe(__filename, function () {
assert.deepEqual(hooks.callFirst(hookName), []); assert.deepEqual(hooks.callFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]); assert.deepEqual(gotCalls, [0, 1, 2]);
}); });
it('stops when predicate is satisfied', async function () { it('stops when predicate is satisfied', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']); assert.deepEqual(hooks.callFirst(hookName), ['val1']);
}); });
it('skips values that do not satisfy predicate (undefined)', async function () { it('skips values that do not satisfy predicate (undefined)', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1')); testHooks.push(makeHook(), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']); assert.deepEqual(hooks.callFirst(hookName), ['val1']);
}); });
it('skips values that do not satisfy predicate (empty list)', async function () { it('skips values that do not satisfy predicate (empty list)', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook([]), makeHook('val1')); testHooks.push(makeHook([]), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']); assert.deepEqual(hooks.callFirst(hookName), ['val1']);
}); });
it('null satisifes the predicate', async function () { it('null satisifes the predicate', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(null), makeHook('val1')); testHooks.push(makeHook(null), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), [null]); assert.deepEqual(hooks.callFirst(hookName), [null]);
}); });
it('non-empty arrays are returned unmodified', async function () { it('non-empty arrays are returned unmodified', async function () {
const want = ['val1']; const want = ['val1'];
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(want), makeHook(['val2'])); testHooks.push(makeHook(want), makeHook(['val2']));
assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual! assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual!
}); });
it('value can be passed via callback', async function () { it('value can be passed via callback', async function () {
const want = {}; const want = {};
hook.hook_fn = (hn, ctx, cb) => { cb(want); }; hook.hook_fn = (hn, ctx, cb) => { cb(want); };
@ -472,23 +431,19 @@ describe(__filename, function () {
assert.equal(got[0], want); // Note: *NOT* deepEqual! assert.equal(got[0], want); // Note: *NOT* deepEqual!
}); });
}); });
describe('callHookFnAsync', function () { describe('callHookFnAsync', function () {
const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand.
describe('basic behavior', function () { describe('basic behavior', function () {
it('passes hook name', async function () { it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
await callHookFnAsync(hook); await callHookFnAsync(hook);
}); });
it('passes context', async function () { it('passes context', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); }; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); };
await callHookFnAsync(hook, val); await callHookFnAsync(hook, val);
} }
}); });
it('returns the value provided to the callback', async function () { it('returns the value provided to the callback', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx, cb) => { cb(ctx); }; hook.hook_fn = (hn, ctx, cb) => { cb(ctx); };
@ -496,7 +451,6 @@ describe(__filename, function () {
assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
} }
}); });
it('returns the value returned by the hook function', async function () { it('returns the value returned by the hook function', async function () {
for (const val of ['value', null, undefined]) { for (const val of ['value', null, undefined]) {
// Must not have the cb parameter otherwise returning undefined will never resolve. // Must not have the cb parameter otherwise returning undefined will never resolve.
@ -505,27 +459,22 @@ describe(__filename, function () {
assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val); assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
} }
}); });
it('rejects if it throws an exception', async function () { it('rejects if it throws an exception', async function () {
hook.hook_fn = () => { throw new Error('test exception'); }; hook.hook_fn = () => { throw new Error('test exception'); };
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' }); await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
}); });
it('rejects if rejected Promise passed to callback', async function () { it('rejects if rejected Promise passed to callback', async function () {
hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception'))); hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception')));
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' }); await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
}); });
it('rejects if rejected Promise returned', async function () { it('rejects if rejected Promise returned', async function () {
hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception')); hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception'));
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' }); await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
}); });
it('callback returns undefined', async function () { it('callback returns undefined', async function () {
hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); }; hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); };
await callHookFnAsync(hook); await callHookFnAsync(hook);
}); });
it('checks for deprecation', async function () { it('checks for deprecation', async function () {
sinon.stub(console, 'warn'); sinon.stub(console, 'warn');
hooks.deprecationNotices[hookName] = 'test deprecation'; hooks.deprecationNotices[hookName] = 'test deprecation';
@ -535,7 +484,6 @@ describe(__filename, function () {
assert.match(console.warn.getCall(0).args[0], /test deprecation/); assert.match(console.warn.getCall(0).args[0], /test deprecation/);
}); });
}); });
describe('supported hook function styles', function () { describe('supported hook function styles', function () {
const supportedHookFunctions = supportedSyncHookFunctions.concat([ const supportedHookFunctions = supportedSyncHookFunctions.concat([
{ {
@ -616,7 +564,6 @@ describe(__filename, function () {
wantErr: 'test rejection', wantErr: 'test rejection',
}, },
]); ]);
for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) { for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) {
it(tc.name, async function () { it(tc.name, async function () {
sinon.stub(console, 'warn'); sinon.stub(console, 'warn');
@ -625,7 +572,8 @@ describe(__filename, function () {
const p = callHookFnAsync(hook); const p = callHookFnAsync(hook);
if (tc.wantErr) { if (tc.wantErr) {
await assert.rejects(p, { message: tc.wantErr }); await assert.rejects(p, { message: tc.wantErr });
} else { }
else {
assert.equal(await p, tc.want); assert.equal(await p, tc.want);
} }
assert.equal(console.warn.callCount, 0); assert.equal(console.warn.callCount, 0);
@ -633,14 +581,12 @@ describe(__filename, function () {
}); });
} }
}); });
// Test various ways a hook might attempt to settle twice. (Examples: call the callback a second // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
// time, or call the callback and then return a value.) // time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () { describe('bad hook function behavior (double settle)', function () {
beforeEach(async function () { beforeEach(async function () {
sinon.stub(console, 'error'); sinon.stub(console, 'error');
}); });
// Each item in this array codifies a way to settle an asynchronous hook function. Each of the // Each item in this array codifies a way to settle an asynchronous hook function. Each of the
// test cases below combines two of these behaviors in a single hook function and confirms // test cases below combines two of these behaviors in a single hook function and confirms
// that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2) // that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2)
@ -760,23 +706,21 @@ describe(__filename, function () {
when: 3, when: 3,
}, },
]; ];
for (const step1 of behaviors) { for (const step1 of behaviors) {
// There can't be a second step if the first step is to return or throw. // There can't be a second step if the first step is to return or throw.
if (step1.name.startsWith('return ') || step1.name === 'throw') continue; if (step1.name.startsWith('return ') || step1.name === 'throw')
continue;
for (const step2 of behaviors) { for (const step2 of behaviors) {
it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () { it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
hook.hook_fn = (hn, ctx, cb) => { hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, new Error(ctx.ret1), ctx.ret1); step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
return step2.fn(cb, new Error(ctx.ret2), ctx.ret2); return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
}; };
// Temporarily remove unhandled Promise rejection listeners so that the unhandled // Temporarily remove unhandled Promise rejection listeners so that the unhandled
// rejections we expect to see don't trigger a test failure (or terminate node). // rejections we expect to see don't trigger a test failure (or terminate node).
const event = 'unhandledRejection'; const event = 'unhandledRejection';
const listenersBackup = process.rawListeners(event); const listenersBackup = process.rawListeners(event);
process.removeAllListeners(event); process.removeAllListeners(event);
let tempListener; let tempListener;
let asyncErr; let asyncErr;
try { try {
@ -794,30 +738,29 @@ describe(__filename, function () {
const p = callHookFnAsync(hook, { ret1: 'val1', ret2: 'val2' }); const p = callHookFnAsync(hook, { ret1: 'val1', ret2: 'val2' });
if (winningStep.rejects) { if (winningStep.rejects) {
await assert.rejects(p, { message: winningVal }); await assert.rejects(p, { message: winningVal });
} else { }
else {
assert.equal(await p, winningVal); assert.equal(await p, winningVal);
} }
await seenErrPromise; await seenErrPromise;
} finally { }
finally {
// Restore the original listeners. // Restore the original listeners.
process.off(event, tempListener); process.off(event, tempListener);
for (const listener of listenersBackup) { for (const listener of listenersBackup) {
process.on(event, listener); process.on(event, listener);
} }
} }
assert.equal(console.error.callCount, 1, assert.equal(console.error.callCount, 1, `Got errors:\n${console.error.getCalls().map((call) => call.args[0]).join('\n')}`);
`Got errors:\n${
console.error.getCalls().map((call) => call.args[0]).join('\n')}`);
assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/); assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
assert(asyncErr instanceof Error); assert(asyncErr instanceof Error);
assert.match(asyncErr.message, /DOUBLE SETTLE BUG/); assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
}); });
// This next test is the same as the above test, except the second settle attempt is for // This next test is the same as the above test, except the second settle attempt is for
// the same outcome. The two outcomes can't be the same if one step rejects and the other // the same outcome. The two outcomes can't be the same if one step rejects and the other
// doesn't, so skip those cases. // doesn't, so skip those cases.
if (step1.rejects !== step2.rejects) continue; if (step1.rejects !== step2.rejects)
continue;
it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () { it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
const err = new Error('val'); const err = new Error('val');
hook.hook_fn = (hn, ctx, cb) => { hook.hook_fn = (hn, ctx, cb) => {
@ -829,7 +772,8 @@ describe(__filename, function () {
const p = callHookFnAsync(hook); const p = callHookFnAsync(hook);
if (winningStep.rejects) { if (winningStep.rejects) {
await assert.rejects(p, { message: 'val' }); await assert.rejects(p, { message: 'val' });
} else { }
else {
assert.equal(await p, 'val'); assert.equal(await p, 'val');
} }
await errorLogged; await errorLogged;
@ -840,7 +784,6 @@ describe(__filename, function () {
} }
}); });
}); });
describe('hooks.aCallAll', function () { describe('hooks.aCallAll', function () {
describe('basic behavior', function () { describe('basic behavior', function () {
it('calls all asynchronously, returns values in order', async function () { it('calls all asynchronously, returns values in order', async function () {
@ -876,35 +819,29 @@ describe(__filename, function () {
hookPromises[0].resolve(); hookPromises[0].resolve();
assert.deepEqual(await p, [0, 1]); assert.deepEqual(await p, [0, 1]);
}); });
it('passes hook name', async function () { it('passes hook name', async function () {
hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
await hooks.aCallAll(hookName); await hooks.aCallAll(hookName);
}); });
it('undefined context -> {}', async function () { it('undefined context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallAll(hookName); await hooks.aCallAll(hookName);
}); });
it('null context -> {}', async function () { it('null context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallAll(hookName, null); await hooks.aCallAll(hookName, null);
}); });
it('context unmodified', async function () { it('context unmodified', async function () {
const wantContext = {}; const wantContext = {};
hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.aCallAll(hookName, wantContext); await hooks.aCallAll(hookName, wantContext);
}); });
}); });
describe('aCallAll callback', function () { describe('aCallAll callback', function () {
it('exception in callback rejects', async function () { it('exception in callback rejects', async function () {
const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); }); const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); });
await assert.rejects(p, { message: 'test exception' }); await assert.rejects(p, { message: 'test exception' });
}); });
it('propagates error on exception', async function () { it('propagates error on exception', async function () {
hook.hook_fn = () => { throw new Error('test exception'); }; hook.hook_fn = () => { throw new Error('test exception'); };
await hooks.aCallAll(hookName, {}, (err) => { await hooks.aCallAll(hookName, {}, (err) => {
@ -912,54 +849,45 @@ describe(__filename, function () {
assert.equal(err.message, 'test exception'); assert.equal(err.message, 'test exception');
}); });
}); });
it('propagages null error on success', async function () { it('propagages null error on success', async function () {
await hooks.aCallAll(hookName, {}, (err) => { await hooks.aCallAll(hookName, {}, (err) => {
assert(err == null, `got non-null error: ${err}`); assert(err == null, `got non-null error: ${err}`);
}); });
}); });
it('propagages results on success', async function () { it('propagages results on success', async function () {
hook.hook_fn = () => 'val'; hook.hook_fn = () => 'val';
await hooks.aCallAll(hookName, {}, (err, results) => { await hooks.aCallAll(hookName, {}, (err, results) => {
assert.deepEqual(results, ['val']); assert.deepEqual(results, ['val']);
}); });
}); });
it('returns callback return value', async function () { it('returns callback return value', async function () {
assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val'); assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val');
}); });
}); });
describe('result processing', function () { describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () { it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks[hookName]; delete plugins.hooks[hookName];
assert.deepEqual(await hooks.aCallAll(hookName), []); assert.deepEqual(await hooks.aCallAll(hookName), []);
}); });
it('no registered hooks (empty list) -> []', async function () { it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
assert.deepEqual(await hooks.aCallAll(hookName), []); assert.deepEqual(await hooks.aCallAll(hookName), []);
}); });
it('flattens one level', async function () { it('flattens one level', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]); assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]);
}); });
it('filters out undefined', async function () { it('filters out undefined', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]); assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]);
}); });
it('preserves null', async function () { it('preserves null', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]); assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]);
}); });
it('all undefined -> []', async function () { it('all undefined -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook(Promise.resolve())); testHooks.push(makeHook(), makeHook(Promise.resolve()));
@ -967,7 +895,6 @@ describe(__filename, function () {
}); });
}); });
}); });
describe('hooks.callAllSerial', function () { describe('hooks.callAllSerial', function () {
describe('basic behavior', function () { describe('basic behavior', function () {
it('calls all asynchronously, serially, in order', async function () { it('calls all asynchronously, serially, in order', async function () {
@ -991,58 +918,48 @@ describe(__filename, function () {
assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]); assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
assert.deepEqual(gotCalls, [0, 1, 2]); assert.deepEqual(gotCalls, [0, 1, 2]);
}); });
it('passes hook name', async function () { it('passes hook name', async function () {
hook.hook_fn = async (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
await hooks.callAllSerial(hookName); await hooks.callAllSerial(hookName);
}); });
it('undefined context -> {}', async function () { it('undefined context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName); await hooks.callAllSerial(hookName);
}); });
it('null context -> {}', async function () { it('null context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName, null); await hooks.callAllSerial(hookName, null);
}); });
it('context unmodified', async function () { it('context unmodified', async function () {
const wantContext = {}; const wantContext = {};
hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); }; hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.callAllSerial(hookName, wantContext); await hooks.callAllSerial(hookName, wantContext);
}); });
}); });
describe('result processing', function () { describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () { it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks[hookName]; delete plugins.hooks[hookName];
assert.deepEqual(await hooks.callAllSerial(hookName), []); assert.deepEqual(await hooks.callAllSerial(hookName), []);
}); });
it('no registered hooks (empty list) -> []', async function () { it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
assert.deepEqual(await hooks.callAllSerial(hookName), []); assert.deepEqual(await hooks.callAllSerial(hookName), []);
}); });
it('flattens one level', async function () { it('flattens one level', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]])); testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]); assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
}); });
it('filters out undefined', async function () { it('filters out undefined', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve())); testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]); assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
}); });
it('preserves null', async function () { it('preserves null', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null))); testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]); assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
}); });
it('all undefined -> []', async function () { it('all undefined -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook(Promise.resolve())); testHooks.push(makeHook(), makeHook(Promise.resolve()));
@ -1050,39 +967,32 @@ describe(__filename, function () {
}); });
}); });
}); });
describe('hooks.aCallFirst', function () { describe('hooks.aCallFirst', function () {
it('no registered hooks (undefined) -> []', async function () { it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook; delete plugins.hooks.testHook;
assert.deepEqual(await hooks.aCallFirst(hookName), []); assert.deepEqual(await hooks.aCallFirst(hookName), []);
}); });
it('no registered hooks (empty list) -> []', async function () { it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0; testHooks.length = 0;
assert.deepEqual(await hooks.aCallFirst(hookName), []); assert.deepEqual(await hooks.aCallFirst(hookName), []);
}); });
it('passes hook name => {}', async function () { it('passes hook name => {}', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
await hooks.aCallFirst(hookName); await hooks.aCallFirst(hookName);
}); });
it('undefined context => {}', async function () { it('undefined context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallFirst(hookName); await hooks.aCallFirst(hookName);
}); });
it('null context => {}', async function () { it('null context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallFirst(hookName, null); await hooks.aCallFirst(hookName, null);
}); });
it('context unmodified', async function () { it('context unmodified', async function () {
const wantContext = {}; const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.aCallFirst(hookName, wantContext); await hooks.aCallFirst(hookName, wantContext);
}); });
it('default predicate: predicate never satisfied -> calls all in order', async function () { it('default predicate: predicate never satisfied -> calls all in order', async function () {
const gotCalls = []; const gotCalls = [];
testHooks.length = 0; testHooks.length = 0;
@ -1094,7 +1004,6 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName), []); assert.deepEqual(await hooks.aCallFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]); assert.deepEqual(gotCalls, [0, 1, 2]);
}); });
it('calls hook functions serially', async function () { it('calls hook functions serially', async function () {
const gotCalls = []; const gotCalls = [];
testHooks.length = 0; testHooks.length = 0;
@ -1116,31 +1025,26 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName), []); assert.deepEqual(await hooks.aCallFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]); assert.deepEqual(gotCalls, [0, 1, 2]);
}); });
it('default predicate: stops when satisfied', async function () { it('default predicate: stops when satisfied', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
}); });
it('default predicate: skips values that do not satisfy (undefined)', async function () { it('default predicate: skips values that do not satisfy (undefined)', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1')); testHooks.push(makeHook(), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
}); });
it('default predicate: skips values that do not satisfy (empty list)', async function () { it('default predicate: skips values that do not satisfy (empty list)', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook([]), makeHook('val1')); testHooks.push(makeHook([]), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']); assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
}); });
it('default predicate: null satisifes', async function () { it('default predicate: null satisifes', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(null), makeHook('val1')); testHooks.push(makeHook(null), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), [null]); assert.deepEqual(await hooks.aCallFirst(hookName), [null]);
}); });
it('custom predicate: called for each hook function', async function () { it('custom predicate: called for each hook function', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(0), makeHook(1), makeHook(2)); testHooks.push(makeHook(0), makeHook(1), makeHook(2));
@ -1148,7 +1052,6 @@ describe(__filename, function () {
await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; }); await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; });
assert.equal(got, 3); assert.equal(got, 3);
}); });
it('custom predicate: boolean false/true continues/stops iteration', async function () { it('custom predicate: boolean false/true continues/stops iteration', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(1), makeHook(2), makeHook(3)); testHooks.push(makeHook(1), makeHook(2), makeHook(3));
@ -1160,7 +1063,6 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
assert.equal(nCall, 2); assert.equal(nCall, 2);
}); });
it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () { it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () {
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(1), makeHook(2), makeHook(3)); testHooks.push(makeHook(1), makeHook(2), makeHook(3));
@ -1172,32 +1074,27 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]); assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
assert.equal(nCall, 2); assert.equal(nCall, 2);
}); });
it('custom predicate: array value passed unmodified to predicate', async function () { it('custom predicate: array value passed unmodified to predicate', async function () {
const want = [0]; const want = [0];
hook.hook_fn = () => want; hook.hook_fn = () => want;
const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual! const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual!
await hooks.aCallFirst(hookName, null, null, predicate); await hooks.aCallFirst(hookName, null, null, predicate);
}); });
it('custom predicate: normalized value passed to predicate (undefined)', async function () { it('custom predicate: normalized value passed to predicate (undefined)', async function () {
const predicate = (got) => { assert.deepEqual(got, []); }; const predicate = (got) => { assert.deepEqual(got, []); };
await hooks.aCallFirst(hookName, null, null, predicate); await hooks.aCallFirst(hookName, null, null, predicate);
}); });
it('custom predicate: normalized value passed to predicate (null)', async function () { it('custom predicate: normalized value passed to predicate (null)', async function () {
hook.hook_fn = () => null; hook.hook_fn = () => null;
const predicate = (got) => { assert.deepEqual(got, [null]); }; const predicate = (got) => { assert.deepEqual(got, [null]); };
await hooks.aCallFirst(hookName, null, null, predicate); await hooks.aCallFirst(hookName, null, null, predicate);
}); });
it('non-empty arrays are returned unmodified', async function () { it('non-empty arrays are returned unmodified', async function () {
const want = ['val1']; const want = ['val1'];
testHooks.length = 0; testHooks.length = 0;
testHooks.push(makeHook(want), makeHook(['val2'])); testHooks.push(makeHook(want), makeHook(['val2']));
assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual! assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual!
}); });
it('value can be passed via callback', async function () { it('value can be passed via callback', async function () {
const want = {}; const want = {};
hook.hook_fn = (hn, ctx, cb) => { cb(want); }; hook.hook_fn = (hn, ctx, cb) => { cb(want); };

View file

@ -1,11 +1,10 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
describe(__filename, function () { describe(__filename, function () {
let agent; let agent;
let pad; let pad;
@ -15,11 +14,9 @@ describe(__filename, function () {
let socket; let socket;
let roSocket; let roSocket;
const backups = {}; const backups = {};
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
}); });
beforeEach(async function () { beforeEach(async function () {
backups.hooks = { handleMessageSecurity: plugins.hooks.handleMessageSecurity }; backups.hooks = { handleMessageSecurity: plugins.hooks.handleMessageSecurity };
plugins.hooks.handleMessageSecurity = []; plugins.hooks.handleMessageSecurity = [];
@ -33,23 +30,23 @@ describe(__filename, function () {
const { type, data: clientVars } = await common.handshake(socket, padId); const { type, data: clientVars } = await common.handshake(socket, padId);
assert.equal(type, 'CLIENT_VARS'); assert.equal(type, 'CLIENT_VARS');
rev = clientVars.collab_client_vars.rev; rev = clientVars.collab_client_vars.rev;
roPadId = await readOnlyManager.getReadOnlyId(padId); roPadId = await readOnlyManager.getReadOnlyId(padId);
res = await agent.get(`/p/${roPadId}`).expect(200); res = await agent.get(`/p/${roPadId}`).expect(200);
roSocket = await common.connect(res); roSocket = await common.connect(res);
await common.handshake(roSocket, roPadId); await common.handshake(roSocket, roPadId);
}); });
afterEach(async function () { afterEach(async function () {
Object.assign(plugins.hooks, backups.hooks); Object.assign(plugins.hooks, backups.hooks);
if (socket != null) socket.close(); if (socket != null)
socket.close();
socket = null; socket = null;
if (roSocket != null) roSocket.close(); if (roSocket != null)
roSocket.close();
roSocket = null; roSocket = null;
if (pad != null) await pad.remove(); if (pad != null)
await pad.remove();
pad = null; pad = null;
}); });
describe('CHANGESET_REQ', function () { describe('CHANGESET_REQ', function () {
it('users are unable to read changesets from other pads', async function () { it('users are unable to read changesets from other pads', async function () {
const otherPadId = `${padId}other`; const otherPadId = `${padId}other`;
@ -60,7 +57,7 @@ describe(__filename, function () {
const resP = common.waitForSocketEvent(roSocket, 'message'); const resP = common.waitForSocketEvent(roSocket, 'message');
await common.sendMessage(roSocket, { await common.sendMessage(roSocket, {
component: 'pad', component: 'pad',
padId: otherPadId, // The server should ignore this. padId: otherPadId,
type: 'CHANGESET_REQ', type: 'CHANGESET_REQ',
data: { data: {
granularity: 1, granularity: 1,
@ -73,15 +70,14 @@ describe(__filename, function () {
assert.equal(res.data.requestID, 'requestId'); assert.equal(res.data.requestID, 'requestId');
// Should match padId's text, not otherPadId's text. // Should match padId's text, not otherPadId's text.
assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/); assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
} finally { }
finally {
await otherPad.remove(); await otherPad.remove();
} }
}); });
}); });
describe('USER_CHANGES', function () { describe('USER_CHANGES', function () {
const sendUserChanges = const sendUserChanges = async (socket, cs) => await common.sendUserChanges(socket, { baseRev: rev, changeset: cs });
async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
const assertAccepted = async (socket, wantRev) => { const assertAccepted = async (socket, wantRev) => {
await common.waitForAcceptCommit(socket, wantRev); await common.waitForAcceptCommit(socket, wantRev);
rev = wantRev; rev = wantRev;
@ -90,7 +86,6 @@ describe(__filename, function () {
const msg = await common.waitForSocketEvent(socket, 'message'); const msg = await common.waitForSocketEvent(socket, 'message');
assert.deepEqual(msg, { disconnect: 'badChangeset' }); assert.deepEqual(msg, { disconnect: 'badChangeset' });
}; };
it('changes are applied', async function () { it('changes are applied', async function () {
await Promise.all([ await Promise.all([
assertAccepted(socket, rev + 1), assertAccepted(socket, rev + 1),
@ -98,14 +93,12 @@ describe(__filename, function () {
]); ]);
assert.equal(pad.text(), 'hello\n'); assert.equal(pad.text(), 'hello\n');
}); });
it('bad changeset is rejected', async function () { it('bad changeset is rejected', async function () {
await Promise.all([ await Promise.all([
assertRejected(socket), assertRejected(socket),
sendUserChanges(socket, 'this is not a valid changeset'), sendUserChanges(socket, 'this is not a valid changeset'),
]); ]);
}); });
it('retransmission is accepted, has no effect', async function () { it('retransmission is accepted, has no effect', async function () {
const cs = 'Z:1>5+5$hello'; const cs = 'Z:1>5+5$hello';
await Promise.all([ await Promise.all([
@ -119,7 +112,6 @@ describe(__filename, function () {
]); ]);
assert.equal(pad.text(), 'hello\n'); assert.equal(pad.text(), 'hello\n');
}); });
it('identity changeset is accepted, has no effect', async function () { it('identity changeset is accepted, has no effect', async function () {
await Promise.all([ await Promise.all([
assertAccepted(socket, rev + 1), assertAccepted(socket, rev + 1),
@ -131,7 +123,6 @@ describe(__filename, function () {
]); ]);
assert.equal(pad.text(), 'hello\n'); assert.equal(pad.text(), 'hello\n');
}); });
it('non-identity changeset with no net change is accepted, has no effect', async function () { it('non-identity changeset with no net change is accepted, has no effect', async function () {
await Promise.all([ await Promise.all([
assertAccepted(socket, rev + 1), assertAccepted(socket, rev + 1),
@ -143,7 +134,6 @@ describe(__filename, function () {
]); ]);
assert.equal(pad.text(), 'hello\n'); assert.equal(pad.text(), 'hello\n');
}); });
it('handleMessageSecurity can grant one-time write access', async function () { it('handleMessageSecurity can grant one-time write access', async function () {
const cs = 'Z:1>5+5$hello'; const cs = 'Z:1>5+5$hello';
const errRegEx = /write attempt on read-only pad/; const errRegEx = /write attempt on read-only pad/;
@ -152,7 +142,6 @@ describe(__filename, function () {
// sendUserChanges() waits for message ack, so if the message was accepted then head should // sendUserChanges() waits for message ack, so if the message was accepted then head should
// have already incremented by the time we get here. // have already incremented by the time we get here.
assert.equal(pad.head, rev); // Not incremented. assert.equal(pad.head, rev); // Not incremented.
// Now allow the change. // Now allow the change.
plugins.hooks.handleMessageSecurity.push({ hook_fn: () => 'permitOnce' }); plugins.hooks.handleMessageSecurity.push({ hook_fn: () => 'permitOnce' });
await Promise.all([ await Promise.all([
@ -160,7 +149,6 @@ describe(__filename, function () {
sendUserChanges(roSocket, cs), sendUserChanges(roSocket, cs),
]); ]);
assert.equal(pad.text(), 'hello\n'); assert.equal(pad.text(), 'hello\n');
// The next change should be dropped. // The next change should be dropped.
plugins.hooks.handleMessageSecurity = []; plugins.hooks.handleMessageSecurity = [];
await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx); await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);

View file

@ -1,29 +1,24 @@
import assert$0 from "assert";
import { padutils } from "../../../static/js/pad_utils.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const {padutils} = require('../../../static/js/pad_utils');
describe(__filename, function () { describe(__filename, function () {
describe('warnDeprecated', function () { describe('warnDeprecated', function () {
const { warnDeprecated } = padutils; const { warnDeprecated } = padutils;
const backups = {}; const backups = {};
before(async function () { before(async function () {
backups.logger = warnDeprecated.logger; backups.logger = warnDeprecated.logger;
}); });
afterEach(async function () { afterEach(async function () {
warnDeprecated.logger = backups.logger; warnDeprecated.logger = backups.logger;
delete warnDeprecated._rl; // Reset internal rate limiter state. delete warnDeprecated._rl; // Reset internal rate limiter state.
}); });
it('includes the stack', async function () { it('includes the stack', async function () {
let got; let got;
warnDeprecated.logger = { warn: (stack) => got = stack }; warnDeprecated.logger = { warn: (stack) => got = stack };
warnDeprecated(); warnDeprecated();
assert(got.includes(__filename)); assert(got.includes(__filename));
}); });
it('rate limited', async function () { it('rate limited', async function () {
let got = 0; let got = 0;
warnDeprecated.logger = { warn: () => ++got }; warnDeprecated.logger = { warn: () => ++got };

View file

@ -1,21 +1,17 @@
import * as common from "../common.js";
import assertLegacy from "../assert-legacy.js";
'use strict'; 'use strict';
const assert = assertLegacy.strict;
const common = require('../common');
const assert = require('../assert-legacy').strict;
let agent; let agent;
describe(__filename, function () { describe(__filename, function () {
before(async function () { before(async function () {
agent = await common.init(); agent = await common.init();
}); });
it('supports pads with spaces, regression test for #4883', async function () { it('supports pads with spaces, regression test for #4883', async function () {
await agent.get('/p/pads with spaces') await agent.get('/p/pads with spaces')
.expect(302) .expect(302)
.expect('location', 'pads_with_spaces'); .expect('location', 'pads_with_spaces');
}); });
it('supports pads with spaces and query, regression test for #4883', async function () { it('supports pads with spaces and query, regression test for #4883', async function () {
await agent.get('/p/pads with spaces?showChat=true&noColors=false') await agent.get('/p/pads with spaces?showChat=true&noColors=false')
.expect(302) .expect(302)

View file

@ -1,6 +1,6 @@
const assert = require('assert').strict; import assert$0 from "assert";
const promises = require('../../../node/utils/promises'); import * as promises from "../../../node/utils/promises.js";
const assert = assert$0.strict;
describe(__filename, function () { describe(__filename, function () {
describe('promises.timesLimit', function () { describe('promises.timesLimit', function () {
let wantIndex = 0; let wantIndex = 0;
@ -18,22 +18,18 @@ describe(__filename, function () {
testPromises.push(p); testPromises.push(p);
return p.promise; return p.promise;
}; };
const total = 11; const total = 11;
const concurrency = 7; const concurrency = 7;
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise); const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
it('honors concurrency', async function () { it('honors concurrency', async function () {
assert.equal(wantIndex, concurrency); assert.equal(wantIndex, concurrency);
}); });
it('creates another when one completes', async function () { it('creates another when one completes', async function () {
const { promise, resolve } = testPromises.shift(); const { promise, resolve } = testPromises.shift();
resolve(); resolve();
await promise; await promise;
assert.equal(wantIndex, concurrency + 1); assert.equal(wantIndex, concurrency + 1);
}); });
it('creates the expected total number of promises', async function () { it('creates the expected total number of promises', async function () {
while (testPromises.length > 0) { while (testPromises.length > 0) {
// Resolve them in random order to ensure that the resolution order doesn't matter. // Resolve them in random order to ensure that the resolution order doesn't matter.
@ -44,11 +40,9 @@ describe(__filename, function () {
} }
assert.equal(wantIndex, total); assert.equal(wantIndex, total);
}); });
it('resolves', async function () { it('resolves', async function () {
await timesLimitPromise; await timesLimitPromise;
}); });
it('does not create too many promises if total < concurrency', async function () { it('does not create too many promises if total < concurrency', async function () {
wantIndex = 0; wantIndex = 0;
assert.equal(testPromises.length, 0); assert.equal(testPromises.length, 0);
@ -63,21 +57,18 @@ describe(__filename, function () {
await timesLimitPromise; await timesLimitPromise;
assert.equal(wantIndex, total); assert.equal(wantIndex, total);
}); });
it('accepts total === 0, concurrency > 0', async function () { it('accepts total === 0, concurrency > 0', async function () {
wantIndex = 0; wantIndex = 0;
assert.equal(testPromises.length, 0); assert.equal(testPromises.length, 0);
await promises.timesLimit(0, concurrency, makePromise); await promises.timesLimit(0, concurrency, makePromise);
assert.equal(wantIndex, 0); assert.equal(wantIndex, 0);
}); });
it('accepts total === 0, concurrency === 0', async function () { it('accepts total === 0, concurrency === 0', async function () {
wantIndex = 0; wantIndex = 0;
assert.equal(testPromises.length, 0); assert.equal(testPromises.length, 0);
await promises.timesLimit(0, 0, makePromise); await promises.timesLimit(0, 0, makePromise);
assert.equal(wantIndex, 0); assert.equal(wantIndex, 0);
}); });
it('rejects total > 0, concurrency === 0', async function () { it('rejects total > 0, concurrency === 0', async function () {
await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError); await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
}); });

View file

@ -1,28 +1,23 @@
import * as AuthorManager from "../../../node/db/AuthorManager.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as db from "../../../node/db/DB.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const AuthorManager = require('../../../node/db/AuthorManager');
const assert = require('assert').strict;
const common = require('../common');
const db = require('../../../node/db/DB');
describe(__filename, function () { describe(__filename, function () {
let setBackup; let setBackup;
before(async function () { before(async function () {
await common.init(); await common.init();
setBackup = db.set; setBackup = db.set;
db.set = async (...args) => { db.set = async (...args) => {
// delay db.set // delay db.set
await new Promise((resolve) => { setTimeout(() => resolve(), 500); }); await new Promise((resolve) => { setTimeout(() => resolve(), 500); });
return await setBackup.call(db, ...args); return await setBackup.call(db, ...args);
}; };
}); });
after(async function () { after(async function () {
db.set = setBackup; db.set = setBackup;
}); });
it('regression test for missing await in createAuthor (#5000)', async function () { it('regression test for missing await in createAuthor (#5000)', async function () {
const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes. const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
assert(await AuthorManager.doesAuthorExist(authorID)); assert(await AuthorManager.doesAuthorExist(authorID));

View file

@ -1,9 +1,8 @@
import assert$0 from "assert";
import path from "path";
import sanitizePathname from "../../../node/utils/sanitizePathname.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const path = require('path');
const sanitizePathname = require('../../../node/utils/sanitizePathname');
describe(__filename, function () { describe(__filename, function () {
describe('absolute paths rejected', function () { describe('absolute paths rejected', function () {
const testCases = [ const testCases = [
@ -44,7 +43,6 @@ describe(__filename, function () {
}); });
} }
}); });
describe('accepted paths', function () { describe('accepted paths', function () {
const testCases = [ const testCases = [
['posix', '', '.'], ['posix', '', '.'],
@ -89,7 +87,6 @@ describe(__filename, function () {
}); });
} }
}); });
it('default path API', async function () { it('default path API', async function () {
assert.equal(sanitizePathname('foo'), 'foo'); assert.equal(sanitizePathname('foo'), 'foo');
}); });

View file

@ -1,10 +1,10 @@
import assert$0 from "assert";
import { exportedForTestingOnly } from "../../../node/utils/Settings.js";
import path from "path";
import process from "process";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict; const { parseSettings } = { exportedForTestingOnly }.exportedForTestingOnly;
const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly;
const path = require('path');
const process = require('process');
describe(__filename, function () { describe(__filename, function () {
describe('parseSettings', function () { describe('parseSettings', function () {
let settings; let settings;
@ -17,14 +17,13 @@ describe(__filename, function () {
{ name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo' }, { name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo' },
{ name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: '' }, { name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: '' },
]; ];
before(async function () { before(async function () {
for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val; for (const tc of envVarSubstTestCases)
process.env[tc.var] = tc.val;
delete process.env.UNSET_VAR; delete process.env.UNSET_VAR;
settings = parseSettings(path.join(__dirname, 'settings.json'), true); settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert(settings != null); assert(settings != null);
}); });
describe('environment variable substitution', function () { describe('environment variable substitution', function () {
describe('set', function () { describe('set', function () {
for (const tc of envVarSubstTestCases) { for (const tc of envVarSubstTestCases) {
@ -32,25 +31,25 @@ describe(__filename, function () {
const obj = settings['environment variable substitution'].set; const obj = settings['environment variable substitution'].set;
if (tc.name === 'undefined') { if (tc.name === 'undefined') {
assert(!(tc.name in obj)); assert(!(tc.name in obj));
} else { }
else {
assert.equal(obj[tc.name], tc.want); assert.equal(obj[tc.name], tc.want);
} }
}); });
} }
}); });
describe('unset', function () { describe('unset', function () {
it('no default', async function () { it('no default', async function () {
const obj = settings['environment variable substitution'].unset; const obj = settings['environment variable substitution'].unset;
assert.equal(obj['no default'], null); assert.equal(obj['no default'], null);
}); });
for (const tc of envVarSubstTestCases) { for (const tc of envVarSubstTestCases) {
it(tc.name, async function () { it(tc.name, async function () {
const obj = settings['environment variable substitution'].unset; const obj = settings['environment variable substitution'].unset;
if (tc.name === 'undefined') { if (tc.name === 'undefined') {
assert(!(tc.name in obj)); assert(!(tc.name in obj));
} else { }
else {
assert.equal(obj[tc.name], tc.want); assert.equal(obj[tc.name], tc.want);
} }
}); });

View file

@ -1,13 +1,12 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
import * as settings from "../../../node/utils/Settings.js";
import * as socketIoRouter from "../../../node/handler/SocketIORouter.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
const settings = require('../../../node/utils/Settings');
const socketIoRouter = require('../../../node/handler/SocketIORouter');
describe(__filename, function () { describe(__filename, function () {
this.timeout(30000); this.timeout(30000);
let agent; let agent;
@ -23,7 +22,6 @@ describe(__filename, function () {
})); }));
}; };
let socket; let socket;
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
beforeEach(async function () { beforeEach(async function () {
backups.hooks = {}; backups.hooks = {};
@ -48,13 +46,13 @@ describe(__filename, function () {
await cleanUpPads(); await cleanUpPads();
}); });
afterEach(async function () { afterEach(async function () {
if (socket) socket.close(); if (socket)
socket.close();
socket = null; socket = null;
await cleanUpPads(); await cleanUpPads();
Object.assign(plugins.hooks, backups.hooks); Object.assign(plugins.hooks, backups.hooks);
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
}); });
describe('Normal accesses', function () { describe('Normal accesses', function () {
it('!authn anonymous cookie /p/pad -> 200, ok', async function () { it('!authn anonymous cookie /p/pad -> 200, ok', async function () {
const res = await agent.get('/p/pad').expect(200); const res = await agent.get('/p/pad').expect(200);
@ -80,13 +78,13 @@ describe(__filename, function () {
const clientVars = await common.handshake(socket, 'pad'); const clientVars = await common.handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.type, 'CLIENT_VARS');
}); });
for (const authn of [false, true]) { for (const authn of [false, true]) {
const desc = authn ? 'authn user' : '!authn anonymous'; const desc = authn ? 'authn user' : '!authn anonymous';
it(`${desc} read-only /p/pad -> 200, ok`, async function () { it(`${desc} read-only /p/pad -> 200, ok`, async function () {
const get = (ep) => { const get = (ep) => {
let res = agent.get(ep); let res = agent.get(ep);
if (authn) res = res.auth('user', 'user-password'); if (authn)
res = res.auth('user', 'user-password');
return res.expect(200); return res.expect(200);
}; };
settings.requireAuthentication = authn; settings.requireAuthentication = authn;
@ -105,7 +103,6 @@ describe(__filename, function () {
assert.equal(clientVars.data.readonly, true); assert.equal(clientVars.data.readonly, true);
}); });
} }
it('authz user /p/pad -> 200, ok', async function () { it('authz user /p/pad -> 200, ok', async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
@ -128,7 +125,6 @@ describe(__filename, function () {
assert.equal(clientVars.type, 'CLIENT_VARS'); assert.equal(clientVars.type, 'CLIENT_VARS');
}); });
}); });
describe('Abnormal access attempts', function () { describe('Abnormal access attempts', function () {
it('authn anonymous /p/pad -> 401, error', async function () { it('authn anonymous /p/pad -> 401, error', async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
@ -138,7 +134,6 @@ describe(__filename, function () {
const message = await common.handshake(socket, 'pad'); const message = await common.handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny'); assert.equal(message.accessStatus, 'deny');
}); });
it('authn anonymous read-only /p/pad -> 401, error', async function () { it('authn anonymous read-only /p/pad -> 401, error', async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -154,7 +149,6 @@ describe(__filename, function () {
const message = await common.handshake(socket, readOnlyId); const message = await common.handshake(socket, readOnlyId);
assert.equal(message.accessStatus, 'deny'); assert.equal(message.accessStatus, 'deny');
}); });
it('authn !cookie -> error', async function () { it('authn !cookie -> error', async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
socket = await common.connect(null); socket = await common.connect(null);
@ -174,13 +168,11 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny'); assert.equal(message.accessStatus, 'deny');
}); });
}); });
describe('Authorization levels via authorize hook', function () { describe('Authorization levels via authorize hook', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
}); });
it("level='create' -> can create", async function () { it("level='create' -> can create", async function () {
authorize = () => 'create'; authorize = () => 'create';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -239,12 +231,10 @@ describe(__filename, function () {
assert.equal(clientVars.data.readonly, true); assert.equal(clientVars.data.readonly, true);
}); });
}); });
describe('Authorization levels via user settings', function () { describe('Authorization levels via user settings', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
}); });
it('user.canCreate = true -> can create and modify', async function () { it('user.canCreate = true -> can create and modify', async function () {
settings.users.user.canCreate = true; settings.users.user.canCreate = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200); const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -293,13 +283,11 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny'); assert.equal(message.accessStatus, 'deny');
}); });
}); });
describe('Authorization level interaction between authorize hook and user settings', function () { describe('Authorization level interaction between authorize hook and user settings', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
}); });
it('authorize hook does not elevate level from user settings', async function () { it('authorize hook does not elevate level from user settings', async function () {
settings.users.user.readOnly = true; settings.users.user.readOnly = true;
authorize = () => 'create'; authorize = () => 'create';
@ -318,7 +306,6 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny'); assert.equal(message.accessStatus, 'deny');
}); });
}); });
describe('SocketIORouter.js', function () { describe('SocketIORouter.js', function () {
const Module = class { const Module = class {
setSocketIO(io) { } setSocketIO(io) { }
@ -326,12 +313,10 @@ describe(__filename, function () {
handleDisconnect(socket) { } handleDisconnect(socket) { }
handleMessage(socket, message) { } handleMessage(socket, message) { }
}; };
afterEach(async function () { afterEach(async function () {
socketIoRouter.deleteComponent(this.test.fullTitle()); socketIoRouter.deleteComponent(this.test.fullTitle());
socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`); socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`);
}); });
it('setSocketIO', async function () { it('setSocketIO', async function () {
let ioServer; let ioServer;
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
@ -339,7 +324,6 @@ describe(__filename, function () {
}()); }());
assert(ioServer != null); assert(ioServer != null);
}); });
it('handleConnect', async function () { it('handleConnect', async function () {
let serverSocket; let serverSocket;
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
@ -348,7 +332,6 @@ describe(__filename, function () {
socket = await common.connect(); socket = await common.connect();
assert(serverSocket != null); assert(serverSocket != null);
}); });
it('handleDisconnect', async function () { it('handleDisconnect', async function () {
let resolveConnected; let resolveConnected;
const connected = new Promise((resolve) => resolveConnected = resolve); const connected = new Promise((resolve) => resolveConnected = resolve);
@ -362,7 +345,8 @@ describe(__filename, function () {
handleDisconnect(socket) { handleDisconnect(socket) {
assert(socket != null); assert(socket != null);
// There might be lingering disconnect events from sockets created by other tests. // There might be lingering disconnect events from sockets created by other tests.
if (this._socket == null || socket.id !== this._socket.id) return; if (this._socket == null || socket.id !== this._socket.id)
return;
assert.equal(socket, this._socket); assert.equal(socket, this._socket);
resolveDisconnected(); resolveDisconnected();
} }
@ -373,7 +357,6 @@ describe(__filename, function () {
socket = null; socket = null;
await disconnected; await disconnected;
}); });
it('handleMessage (success)', async function () { it('handleMessage (success)', async function () {
let serverSocket; let serverSocket;
const want = { const want = {
@ -393,15 +376,12 @@ describe(__filename, function () {
socket.send(want); socket.send(want);
assert.deepEqual(await got, want); assert.deepEqual(await got, want);
}); });
const tx = async (socket, message = {}) => await new Promise((resolve, reject) => { const tx = async (socket, message = {}) => await new Promise((resolve, reject) => {
const AckErr = class extends Error { const AckErr = class extends Error {
constructor(name, ...args) { super(...args); this.name = name; } constructor(name, ...args) { super(...args); this.name = name; }
}; };
socket.send(message, socket.send(message, (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
(errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
}); });
it('handleMessage with ack (success)', async function () { it('handleMessage with ack (success)', async function () {
const want = 'value'; const want = 'value';
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module { socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
@ -411,7 +391,6 @@ describe(__filename, function () {
const got = await tx(socket, { component: this.test.fullTitle() }); const got = await tx(socket, { component: this.test.fullTitle() });
assert.equal(got, want); assert.equal(got, want);
}); });
it('handleMessage with ack (error)', async function () { it('handleMessage with ack (error)', async function () {
const InjectedError = class extends Error { const InjectedError = class extends Error {
constructor() { super('injected test error'); this.name = 'InjectedError'; } constructor() { super('injected test error'); this.name = 'InjectedError'; }

View file

@ -1,8 +1,6 @@
import * as common from "../common.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict'; 'use strict';
const common = require('../common');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () { describe(__filename, function () {
this.timeout(30000); this.timeout(30000);
let agent; let agent;
@ -19,7 +17,6 @@ describe(__filename, function () {
afterEach(async function () { afterEach(async function () {
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
}); });
describe('/javascript', function () { describe('/javascript', function () {
it('/javascript -> 200', async function () { it('/javascript -> 200', async function () {
await agent.get('/javascript').expect(200); await agent.get('/javascript').expect(200);

View file

@ -1,10 +1,9 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict'; 'use strict';
const assert = assert$0.strict;
const assert = require('assert').strict;
const common = require('../common');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () { describe(__filename, function () {
this.timeout(30000); this.timeout(30000);
let agent; let agent;
@ -17,7 +16,6 @@ describe(__filename, function () {
hook_name: hookName, hook_name: hookName,
part: { plugin: 'fake_plugin' }, part: { plugin: 'fake_plugin' },
}); });
before(async function () { agent = await common.init(); }); before(async function () { agent = await common.init(); });
beforeEach(async function () { beforeEach(async function () {
backups.hooks = {}; backups.hooks = {};
@ -40,7 +38,6 @@ describe(__filename, function () {
Object.assign(plugins.hooks, backups.hooks); Object.assign(plugins.hooks, backups.hooks);
Object.assign(settings, backups.settings); Object.assign(settings, backups.settings);
}); });
describe('webaccess: without plugins', function () { describe('webaccess: without plugins', function () {
it('!authn !authz anonymous / -> 200', async function () { it('!authn !authz anonymous / -> 200', async function () {
settings.requireAuthentication = false; settings.requireAuthentication = false;
@ -102,7 +99,6 @@ describe(__filename, function () {
settings.requireAuthorization = true; settings.requireAuthorization = true;
await agent.get('/admin/').auth('admin', 'admin-password').expect(200); await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
}); });
describe('login fails if password is nullish', function () { describe('login fails if password is nullish', function () {
for (const adminPassword of [undefined, null]) { for (const adminPassword of [undefined, null]) {
// https://tools.ietf.org/html/rfc7617 says that the username and password are sent as // https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
@ -119,7 +115,6 @@ describe(__filename, function () {
} }
}); });
}); });
describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () { describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () {
let callOrder; let callOrder;
const Handler = class { const Handler = class {
@ -144,7 +139,6 @@ describe(__filename, function () {
} }
}; };
const handlers = {}; const handlers = {};
beforeEach(async function () { beforeEach(async function () {
callOrder = []; callOrder = [];
for (const hookName of authHookNames) { for (const hookName of authHookNames) {
@ -158,13 +152,11 @@ describe(__filename, function () {
]; ];
} }
}); });
describe('preAuthorize', function () { describe('preAuthorize', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = false; settings.requireAuthentication = false;
settings.requireAuthorization = false; settings.requireAuthorization = false;
}); });
it('defers if it returns []', async function () { it('defers if it returns []', async function () {
await agent.get('/').expect(200); await agent.get('/').expect(200);
// Note: The preAuthorize hook always runs even if requireAuthorization is false. // Note: The preAuthorize hook always runs even if requireAuthorization is false.
@ -235,13 +227,11 @@ describe(__filename, function () {
await agent.get('/').expect(500); await agent.get('/').expect(500);
}); });
}); });
describe('authenticate', function () { describe('authenticate', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = false; settings.requireAuthorization = false;
}); });
it('is not called if !requireAuthentication and not /admin/*', async function () { it('is not called if !requireAuthentication and not /admin/*', async function () {
settings.requireAuthentication = false; settings.requireAuthentication = false;
await agent.get('/').expect(200); await agent.get('/').expect(200);
@ -324,13 +314,11 @@ describe(__filename, function () {
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']); assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
}); });
}); });
describe('authorize', function () { describe('authorize', function () {
beforeEach(async function () { beforeEach(async function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
}); });
it('is not called if !requireAuthorization (non-/admin)', async function () { it('is not called if !requireAuthorization (non-/admin)', async function () {
settings.requireAuthorization = false; settings.requireAuthorization = false;
await agent.get('/').auth('user', 'user-password').expect(200); await agent.get('/').auth('user', 'user-password').expect(200);
@ -399,7 +387,6 @@ describe(__filename, function () {
}); });
}); });
}); });
describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () { describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () {
const Handler = class { const Handler = class {
constructor(hookName) { constructor(hookName) {
@ -422,7 +409,6 @@ describe(__filename, function () {
} }
}; };
const handlers = {}; const handlers = {};
beforeEach(async function () { beforeEach(async function () {
failHookNames.forEach((hookName) => { failHookNames.forEach((hookName) => {
const handler = new Handler(hookName); const handler = new Handler(hookName);
@ -432,7 +418,6 @@ describe(__filename, function () {
settings.requireAuthentication = true; settings.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;
}); });
// authn failure tests // authn failure tests
it('authn fail, no hooks handle -> 401', async function () { it('authn fail, no hooks handle -> 401', async function () {
await agent.get('/').expect(401); await agent.get('/').expect(401);
@ -461,7 +446,6 @@ describe(__filename, function () {
assert(handlers.authnFailure.called); assert(handlers.authnFailure.called);
assert(!handlers.authFailure.called); assert(!handlers.authFailure.called);
}); });
// authz failure tests // authz failure tests
it('authz fail, no hooks handle -> 403', async function () { it('authz fail, no hooks handle -> 403', async function () {
await agent.get('/').auth('user', 'user-password').expect(403); await agent.get('/').auth('user', 'user-password').expect(403);