Continued writing

This commit is contained in:
SamTV12345 2024-07-18 22:13:33 +02:00
parent d1ffd5d02f
commit cef2af15b9
18 changed files with 1455 additions and 1230 deletions

View file

@ -19,6 +19,7 @@ export type PadType = {
getRevisionDate: (rev: number)=>Promise<number>, getRevisionDate: (rev: number)=>Promise<number>,
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>, getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,
appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>, appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>,
settings:any
} }

View file

@ -31,7 +31,7 @@ class AttributeMap extends Map {
* @param {AttributePool} pool - Attribute pool. * @param {AttributePool} pool - Attribute pool.
* @returns {AttributeMap} * @returns {AttributeMap}
*/ */
public static fromString(str: string, pool: AttributePool): AttributeMap { public static fromString(str: string, pool?: AttributePool|null): AttributeMap {
return new AttributeMap(pool).updateFromString(str); return new AttributeMap(pool).updateFromString(str);
} }

View file

@ -0,0 +1,132 @@
import {TextLinesMutator} from "./TextLinesMutator";
import AttributePool from "./AttributePool";
import {assert, copyOp, deserializeOps, slicerZipperFunc, unpack} from "./Changeset";
import Op from "./Op";
import {MergingOpAssembler} from "./MergingOpAssembler";
/**
* Applies a changeset to an array of attribute lines.
*
* @param {string} cs - The encoded changeset.
* @param {Array<string>} lines - Attribute lines. Modified in place.
* @param {AttributePool.ts} pool - Attribute pool.
*/
export class AttributionLinesMutator {
private unpacked
private csOps: Generator<Op>|Op
private csOpsNext: IteratorResult<Op>
private csBank: string
private csBankIndex: number
private mut: TextLinesMutator
private lineOps: Generator<Op>|null
private lineOpsNext: IteratorResult<Op>|null
private lineAssem: null|MergingOpAssembler
private attOp: Op
private csOp: Op
constructor(cs: string, lines:string[], pool: AttributePool) {
this.unpacked = unpack(cs);
this.csOps = deserializeOps(this.unpacked.ops);
this.csOpsNext = this.csOps.next();
this.csBank = this.unpacked.charBank;
this.csBankIndex = 0;
// treat the attribution lines as text lines, mutating a line at a time
this.mut = new TextLinesMutator(lines);
/**
* The Ops in the current line from `lines`.
*
* @type {?Generator<Op>}
*/
this.lineOps = null;
this.lineOpsNext = null;
this.lineAssem = null
this.csOp = new Op()
this.attOp = new Op()
while (this.csOp.opcode || !this.csOpsNext.done || this.attOp.opcode || this.isNextMutOp()) {
if (!this.csOp.opcode && !this.csOpsNext.done) {
// coOp done, but more ops in cs.
this.csOp = this.csOpsNext.value;
this.csOpsNext = this.csOps.next();
}
if (!this.csOp.opcode && !this.attOp.opcode && !this.lineAssem && !this.lineOpsHasNext()) {
break; // done
} else if (this.csOp.opcode === '=' && this.csOp.lines > 0 && !this.csOp.attribs && !this.attOp.opcode &&
!this.lineAssem && !this.lineOpsHasNext()) {
// Skip multiple lines without attributes; this is what makes small changes not order of the
// document size.
this.mut.skipLines(this.csOp.lines);
this.csOp.opcode = '';
} else if (this.csOp.opcode === '+') {
const opOut = copyOp(this.csOp);
if (this.csOp.lines > 1) {
// Copy the first line from `csOp` to `opOut`.
const firstLineLen = this.csBank.indexOf('\n', this.csBankIndex) + 1 - this.csBankIndex;
this.csOp.chars -= firstLineLen;
this.csOp.lines--;
opOut.lines = 1;
opOut.chars = firstLineLen;
} else {
// Either one or no newlines in '+' `csOp`, copy to `opOut` and reset `csOp`.
this.csOp.opcode = '';
}
this.outputMutOp(opOut);
this.csBankIndex += opOut.chars;
} else {
if (!this.attOp.opcode && this.isNextMutOp()) {
this.attOp = this.nextMutOp();
}
const opOut = slicerZipperFunc(this.attOp, this.csOp, pool);
if (opOut.opcode) {
this.outputMutOp(opOut);
}
}
}
assert(!this.lineAssem, `line assembler not finished:${cs}`);
this.mut.close();
}
lineOpsHasNext = () => this.lineOpsNext && !this.lineOpsNext.done;
/**
* Returns false if we are on the last attribute line in `lines` and there is no additional op in
* that line.
*
* @returns {boolean} True if there are more ops to go through.
*/
isNextMutOp = () => this.lineOpsHasNext() || this.mut.hasMore();
/**
* @returns {Op} The next Op from `lineIter`. If there are no more Ops, `lineIter` is reset to
* iterate over the next line, which is consumed from `mut`. If there are no more lines,
* returns a null Op.
*/
nextMutOp = () => {
if (!this.lineOpsHasNext() && this.mut.hasMore()) {
// There are more attribute lines in `lines` to do AND either we just started so `lineIter` is
// still null or there are no more ops in current `lineIter`.
const line = this.mut.removeLines(1);
this.lineOps = deserializeOps(line);
this.lineOpsNext = this.lineOps.next();
}
if (!this.lineOpsHasNext()) return new Op(); // No more ops and no more lines.
const op = this.lineOpsNext!.value;
this.lineOpsNext = this.lineOps!.next();
return op;
}
/**
* Appends an op to `lineAssem`. In case `lineAssem` includes one single newline, adds it to the
* `lines` mutator.
*/
outputMutOp = (op: Op) => {
if (!this.lineAssem) {
this.lineAssem = new MergingOpAssembler()
}
this.lineAssem.append(op);
if (op.lines <= 0) return;
assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`);
// ship it to the mut
this.mut.insert(this.lineAssem.toString(), 1);
this.lineAssem = null;
};
}

109
src/static/js/Builder.ts Normal file
View file

@ -0,0 +1,109 @@
/**
* Incrementally builds a Changeset.
*
* @typedef {object} Builder
* @property {Function} insert -
* @property {Function} keep -
* @property {Function} keepText -
* @property {Function} remove -
* @property {Function} toString -
*/
import {SmartOpAssembler} from "./SmartOpAssembler";
import Op from "./Op";
import {StringAssembler} from "./StringAssembler";
import AttributeMap from "./AttributeMap";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText} from "./Changeset";
/**
* @param {number} oldLen - Old length
* @returns {Builder}
*/
export class Builder {
private readonly oldLen: number;
private assem: SmartOpAssembler;
private readonly o: Op;
private charBank: StringAssembler;
constructor(oldLen: number) {
this.oldLen = oldLen
this.assem = new SmartOpAssembler()
this.o = new Op()
this.charBank = new StringAssembler()
}
/**
* @param {number} N - Number of characters to keep.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keep = (N: number, L: number, attribs?: string|Attribute[], pool?: AttributePool): Builder => {
this.o.opcode = '=';
this.o.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || []).toString();
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
/**
* @param {string} text - Text to keep.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keepText= (text: string, attribs: string|Attribute[], pool?: AttributePool): Builder=> {
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
return this;
}
/**
* @param {string} text - Text to insert.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
insert= (text: string, attribs: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): Builder => {
for (const op of opsFromText('+', text, attribs, pool)) this.assem.append(op);
this.charBank.append(text);
return this;
}
/**
* @param {number} N - Number of characters to remove.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @returns {Builder} this
*/
remove= (N: number, L: number): Builder => {
this.o.opcode = '-';
this.o.attribs = '';
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
toString= () => {
this.assem.endDocument();
const newLen = this.oldLen + this.assem.getLengthChange();
return exports.pack(this.oldLen, newLen, this.assem.toString(), this.charBank.toString());
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
export type OpCode = ''|'='|'+'|'-';
/** /**
* An operation to apply to a shared document. * An operation to apply to a shared document.
*/ */

View file

@ -1,4 +1,5 @@
import Op from "./Op"; import Op from "./Op";
import {clearOp, copyOp, deserializeOps} from "./Changeset";
/** /**
* Iterator over a changeset's operations. * Iterator over a changeset's operations.
@ -9,19 +10,20 @@ import Op from "./Op";
*/ */
export class OpIter { export class OpIter {
private gen private gen
private _next: IteratorResult<Op, void>
/** /**
* @param {string} ops - String encoding the change operations to iterate over. * @param {string} ops - String encoding the change operations to iterate over.
*/ */
constructor(ops: string) { constructor(ops: string) {
this.gen = exports.deserializeOps(ops); this.gen = deserializeOps(ops);
this.next = this.gen.next(); this._next = this.gen.next();
} }
/** /**
* @returns {boolean} Whether there are any remaining operations. * @returns {boolean} Whether there are any remaining operations.
*/ */
hasNext() { hasNext(): boolean {
return !this.next.done; return !this._next.done;
} }
/** /**
@ -33,10 +35,10 @@ export class OpIter {
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are * @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
* no more operations. * no more operations.
*/ */
next(opOut = new Op()) { next(opOut: Op = new Op()): Op {
if (this.hasNext()) { if (this.hasNext()) {
copyOp(this._next.value, opOut); copyOp(this._next.value!, opOut);
this._next = this._gen.next(); this._next = this.gen.next();
} else { } else {
clearOp(opOut); clearOp(opOut);
} }

View file

@ -0,0 +1,335 @@
/**
* Class to iterate and modify texts which have several lines. It is used for applying Changesets on
* arrays of lines.
*
* Mutation operations have the same constraints as exports operations with respect to newlines, but
* not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability,
* final newline). Can be used to mutate lists of strings where the last char of each string is not
* actually a newline, but for the purposes of N and L values, the caller should pretend it is, and
* for things to work right in that case, the input to the `insert` method should be a single line
* with no newlines.
*/
export class TextLinesMutator {
private readonly lines: string[]
private readonly curSplice: [number, number]
private inSplice: boolean
private curLine: number
private curCol: number
/**
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
*/
constructor(lines: string[]) {
this.lines = lines;
/**
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
* insert, delete, or change lines:
* - this._curSplice[0] is an index into the this._lines array.
* - this._curSplice[1] is the number of lines that will be removed from the this._lines array
* starting at the index.
* - The other elements represent mutated (changed by ops) lines or new lines (added by ops)
* to insert at the index.
*
* @type {[number, number?, ...string[]?]}
*/
this.curSplice = [0, 0];
this.inSplice = false;
// position in lines after curSplice is applied:
this.curLine = 0;
this.curCol = 0;
// invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
// curLine >= curSplice[0]
// invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
// curCol == 0
}
/**
* Get a line from `lines` at given index.
*
* @param {number} idx - an index
* @returns {string}
*/
private linesGet(idx: number) {
if ('get' in this.lines) {
// @ts-ignore
return this.lines.get(idx) as string;
} else {
return this.lines[idx];
}
}
/**
* Return a slice from `lines`.
*
* @param {number} start - the start index
* @param {number} end - the end index
* @returns {string[]}
*/
private linesSlice(start: number, end: number): string[] {
// can be unimplemented if removeLines's return value not needed
if (this.lines.slice) {
return this.lines.slice(start, end);
} else {
return [];
}
}
/**
* Return the length of `lines`.
*
* @returns {number}
*/
private linesLength() {
if (typeof this.lines.length === 'number') {
return this.lines.length;
} else {
// @ts-ignore
return this.lines.length();
}
}
/**
* Starts a new splice.
*/
enterSplice() {
this.curSplice[0] = this.curLine;
this.curSplice[1] = 0;
// TODO(doc) when is this the case?
// check all enterSplice calls and changes to curCol
if (this.curCol > 0) this.putCurLineInSplice();
this.inSplice = true;
}
/**
* Changes the lines array according to the values in curSplice and resets curSplice. Called via
* close or TODO(doc).
*/
private leaveSplice() {
this.lines.splice(...this.curSplice);
this.curSplice.length = 2;
this.curSplice[0] = this.curSplice[1] = 0;
this.inSplice = false;
}
/**
* Indicates if curLine is already in the splice. This is necessary because the last element in
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
*
* @returns {boolean} true if curLine is in splice
*/
private isCurLineInSplice() {
// The value of `this._curSplice[1]` does not matter when determining the return value because
// `this._curLine` refers to the line number *after* the splice is applied (so after those lines
// are deleted).
return this.curLine - this.curSplice[0] < this.curSplice.length - 2;
}
/**
* Incorporates current line into the splice and marks its old position to be deleted.
*
* @returns {number} the index of the added line in curSplice
*/
private putCurLineInSplice() {
if (!this.isCurLineInSplice()) {
this.curSplice.push(Number(this.linesGet(this.curSplice[0] + this.curSplice[1]!)));
this.curSplice[1]!++;
}
// TODO should be the same as this._curSplice.length - 1
return 2 + this.curLine - this.curSplice[0];
}
/**
* It will skip some newlines by putting them into the splice.
*
* @param {number} L -
* @param {boolean} includeInSplice - Indicates that attributes are present.
*/
public skipLines(L: number, includeInSplice?: boolean) {
if (!L) return;
if (includeInSplice) {
if (!this.inSplice) this.enterSplice();
// TODO(doc) should this count the number of characters that are skipped to check?
for (let i = 0; i < L; i++) {
this.curCol = 0;
this.putCurLineInSplice();
this.curLine++;
}
} else {
if (this.inSplice) {
if (L > 1) {
// TODO(doc) figure out why single lines are incorporated into splice instead of ignored
this.leaveSplice();
} else {
this.putCurLineInSplice();
}
}
this.curLine += L;
this.curCol = 0;
}
// tests case foo in remove(), which isn't otherwise covered in current impl
}
/**
* Skip some characters. Can contain newlines.
*
* @param {number} N - number of characters to skip
* @param {number} L - number of newlines to skip
* @param {boolean} includeInSplice - indicates if attributes are present
*/
skip(N: number, L: number, includeInSplice: boolean) {
if (!N) return;
if (L) {
this.skipLines(L, includeInSplice);
} else {
if (includeInSplice && !this.inSplice) this.enterSplice();
if (this.inSplice) {
// although the line is put into splice curLine is not increased, because
// only some chars are skipped, not the whole line
this.putCurLineInSplice();
}
this.curCol += N;
}
}
/**
* Remove whole lines from lines array.
*
* @param {number} L - number of lines to remove
* @returns {string}
*/
removeLines(L: number):string {
if (!L) return '';
if (!this.inSplice) this.enterSplice();
/**
* Gets a string of joined lines after the end of the splice.
*
* @param {number} k - number of lines
* @returns {string} joined lines
*/
const nextKLinesText = (k: number): string => {
const m = this.curSplice[0] + this.curSplice[1]!;
return this.linesSlice(m, m + k).join('');
};
let removed: any = '';
if (this.isCurLineInSplice()) {
if (this.curCol === 0) {
removed = this.curSplice[this.curSplice.length - 1];
this.curSplice.length--;
removed += nextKLinesText(L - 1);
this.curSplice[1]! += L - 1;
} else {
removed = nextKLinesText(L - 1);
this.curSplice[1]! += L - 1;
const sline = this.curSplice.length - 1;
// @ts-ignore
removed = this.curSplice[sline]!.substring(this.curCol) + removed;
// @ts-ignore
this.curSplice[sline] = this.curSplice[sline]!.substring(0, this.curCol) +
this.linesGet(this.curSplice[0] + this.curSplice[1]!);
// @ts-ignore
this.curSplice[1] += 1;
}
} else {
removed = nextKLinesText(L);
this.curSplice[1]! += L;
}
return removed;
}
/**
* Remove text from lines array.
*
* @param {number} N - characters to delete
* @param {number} L - lines to delete
* @returns {string}
*/
remove(N: number, L: number) {
if (!N) return '';
if (L) return this.removeLines(L);
if (!this.inSplice) this.enterSplice();
// although the line is put into splice, curLine is not increased, because
// only some chars are removed not the whole line
const sline = this.putCurLineInSplice();
// @ts-ignore
const removed = this.curSplice[sline].substring(this.curCol, this.curCol + N);
// @ts-ignore
this.curSplice[sline] = this.curSplice[sline]!.substring(0, this.curCol) +
// @ts-ignore
this.curSplice[sline].substring(this.curCol + N);
return removed;
}
/**
* Inserts text into lines array.
*
* @param {string} text - the text to insert
* @param {number} L - number of newlines in text
*/
insert(text: string, L: number) {
if (!text) return;
if (!this.inSplice) this.enterSplice();
if (L) {
const newLines = exports.splitTextLines(text);
if (this.isCurLineInSplice()) {
const sline = this.curSplice.length - 1;
/** @type {string} */
const theLine = this.curSplice[sline];
const lineCol = this.curCol;
// Insert the chars up to `curCol` and the first new line.
// @ts-ignore
this.curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
this.curLine++;
newLines.splice(0, 1);
// insert the remaining new lines
this.curSplice.push(...newLines);
this.curLine += newLines.length;
// insert the remaining chars from the "old" line (e.g. the line we were in
// when we started to insert new lines)
// @ts-ignore
this.curSplice.push(theLine.substring(lineCol));
this.curCol = 0; // TODO(doc) why is this not set to the length of last line?
} else {
this.curSplice.push(...newLines);
this.curLine += newLines.length;
}
} else {
// There are no additional lines. Although the line is put into splice, curLine is not
// increased because there may be more chars in the line (newline is not reached).
const sline = this.putCurLineInSplice();
if (!this.curSplice[sline]) {
const err = new Error(
'curSplice[sline] not populated, actual curSplice contents is ' +
`${JSON.stringify(this.curSplice)}. Possibly related to ` +
'https://github.com/ether/etherpad-lite/issues/2802');
console.error(err.stack || err.toString());
}
// @ts-ignore
this.curSplice[sline] = this.curSplice[sline].substring(0, this.curCol) + text +
// @ts-ignore
this.curSplice[sline].substring(this.curCol);
this.curCol += text.length;
}
}
/**
* Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`.
*
* @returns {boolean} indicates if there are lines left
*/
hasMore(): boolean {
let docLines = this.linesLength();
if (this.inSplice) {
docLines += this.curSplice.length - 2 - this.curSplice[1];
}
return this.curLine < docLines;
}
/**
* Closes the splice
*/
close() {
if (this.inSplice) this.leaveSplice();
}
}

View file

@ -5,6 +5,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {InnerWindow} from "./types/InnerWindow";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -28,21 +30,21 @@ const hooks = require('./pluginfw/hooks');
const pluginUtils = require('./pluginfw/shared'); const pluginUtils = require('./pluginfw/shared');
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner') const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
const debugLog = (...args) => {}; const debugLog = (...args: string[]|Object[]|null[]) => {};
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins') const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
const {Cssmanager} = require("./cssmanager"); const {Cssmanager} = require("./cssmanager");
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that. // The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari // Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
// errors out unless given an absolute URL for a JavaScript-created element. // errors out unless given an absolute URL for a JavaScript-created element.
const absUrl = (url) => new URL(url, window.location.href).href; const absUrl = (url: string) => new URL(url, window.location.href).href;
const eventFired = async (obj, event, cleanups = [], predicate = () => true) => { const eventFired = async (obj: any, event: string, cleanups: Function[] = [], predicate = () => true) => {
if (typeof cleanups === 'function') { if (typeof cleanups === 'function') {
predicate = cleanups; predicate = cleanups;
cleanups = []; cleanups = [];
} }
await new Promise((resolve, reject) => { await new Promise((resolve: Function, reject: Function) => {
let cleanup; let cleanup: Function;
const successCb = () => { const successCb = () => {
if (!predicate()) return; if (!predicate()) return;
debugLog(`Ace2Editor.init() ${event} event on`, obj); debugLog(`Ace2Editor.init() ${event} event on`, obj);
@ -57,23 +59,23 @@ const eventFired = async (obj, event, cleanups = [], predicate = () => true) =>
}; };
cleanup = () => { cleanup = () => {
cleanup = () => {}; cleanup = () => {};
obj.removeEventListener(event, successCb); obj!.removeEventListener(event, successCb);
obj.removeEventListener('error', errorCb); obj!.removeEventListener('error', errorCb);
}; };
cleanups.push(cleanup); cleanups.push(cleanup);
obj.addEventListener(event, successCb); obj!.addEventListener(event, successCb);
obj.addEventListener('error', errorCb); obj!.addEventListener('error', errorCb);
}); });
}; };
// Resolves when the frame's document is ready to be mutated. Browsers seem to be quirky about // Resolves when the frame's document is ready to be mutated. Browsers seem to be quirky about
// iframe ready events so this function throws the kitchen sink at the problem. Maybe one day we'll // iframe ready events so this function throws the kitchen sink at the problem. Maybe one day we'll
// find a concise general solution. // find a concise general solution.
const frameReady = async (frame) => { const frameReady = async (frame: HTMLIFrameElement) => {
// Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace // Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace
// the document object after the frame is first created for some reason. ¯\_(ツ)_/¯ // the document object after the frame is first created for some reason. ¯\_(ツ)_/¯
const doc = () => frame.contentDocument; const doc: any = () => frame.contentDocument;
const cleanups = []; const cleanups: Function[] = [];
try { try {
await Promise.race([ await Promise.race([
eventFired(frame, 'load', cleanups), eventFired(frame, 'load', cleanups),
@ -87,26 +89,39 @@ const frameReady = async (frame) => {
} }
}; };
const Ace2Editor = function () { export class Ace2Editor {
let info = {editor: this}; info = {editor: this};
let loaded = false; loaded = false;
actionsPendingInit: Function[] = [];
let actionsPendingInit = [];
const pendingInit = (func) => function (...args) { constructor() {
for (const fnName of this.aceFunctionsPendingInit) {
// Note: info[`ace_${fnName}`] does not exist yet, so it can't be passed directly to
// pendingInit(). A simple wrapper is used to defer the info[`ace_${fnName}`] lookup until
// method invocation.
// @ts-ignore
this[fnName] = this.pendingInit(function (...args) {
// @ts-ignore
this.info[`ace_${fnName}`].apply(this, args);
});
}
}
pendingInit = (func: Function) => (...args: any[])=> {
const action = () => func.apply(this, args); const action = () => func.apply(this, args);
if (loaded) return action(); if (this.loaded) return action();
actionsPendingInit.push(action); this.actionsPendingInit.push(action);
}; }
const doActionsPendingInit = () => { doActionsPendingInit = () => {
for (const fn of actionsPendingInit) fn(); for (const fn of this.actionsPendingInit) fn();
actionsPendingInit = []; this.actionsPendingInit = [];
}; }
// The following functions (prefixed by 'ace_') are exposed by editor, but // The following functions (prefixed by 'ace_') are exposed by editor, but
// execution is delayed until init is complete // execution is delayed until init is complete
const aceFunctionsPendingInit = [ aceFunctionsPendingInit = [
'importText', 'importText',
'importAText', 'importAText',
'focus', 'focus',
@ -124,21 +139,13 @@ const Ace2Editor = function () {
'callWithAce', 'callWithAce',
'execCommand', 'execCommand',
'replaceRange', 'replaceRange',
]; ]
for (const fnName of aceFunctionsPendingInit) { // @ts-ignore
// Note: info[`ace_${fnName}`] does not exist yet, so it can't be passed directly to exportText = () => this.loaded ? this.info.ace_exportText() : '(awaiting init)\n';
// pendingInit(). A simple wrapper is used to defer the info[`ace_${fnName}`] lookup until // @ts-ignore
// method invocation. getInInternationalComposition = () => this.loaded ? this.info.ace_getInInternationalComposition() : null;
this[fnName] = pendingInit(function (...args) {
info[`ace_${fnName}`].apply(this, args);
});
}
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
this.getInInternationalComposition =
() => loaded ? info.ace_getInInternationalComposition() : null;
// prepareUserChangeset: // prepareUserChangeset:
// Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes // Returns null if no new changes or ACE not ready. Otherwise, bundles up all user changes
@ -148,9 +155,9 @@ const Ace2Editor = function () {
// be called again before applyPreparedChangesetToBase. Multiple consecutive calls to // be called again before applyPreparedChangesetToBase. Multiple consecutive calls to
// prepareUserChangeset will return an updated changeset that takes into account the latest user // prepareUserChangeset will return an updated changeset that takes into account the latest user
// changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly. // changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly.
this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null; // @ts-ignore
prepareUserChangeset = () => this.loaded ? this.info.ace_prepareUserChangeset() : null;
const addStyleTagsFor = (doc, files) => { addStyleTagsFor = (doc: Document, files: string[]) => {
for (const file of files) { for (const file of files) {
const link = doc.createElement('link'); const link = doc.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
@ -158,32 +165,36 @@ const Ace2Editor = function () {
link.href = absUrl(encodeURI(file)); link.href = absUrl(encodeURI(file));
doc.head.appendChild(link); doc.head.appendChild(link);
} }
}; }
this.destroy = pendingInit(() => { destroy = this.pendingInit(() => {
info.ace_dispose(); // @ts-ignore
info.frame.parentNode.removeChild(info.frame); this.info.ace_dispose();
info = null; // prevent IE 6 closure memory leaks // @ts-ignore
this.info.frame.parentNode.removeChild(this.info.frame);
// @ts-ignore
this.info = null; // prevent IE 6 closure memory leaks
}); });
this.init = async function (containerId, initialCode) { init = async (containerId: string, initialCode: string)=> {
debugLog('Ace2Editor.init()'); debugLog('Ace2Editor.init()');
// @ts-ignore
this.importText(initialCode); this.importText(initialCode);
const includedCSS = [ const includedCSS = [
`../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`, `../static/css/iframe_editor.css?v=${window.clientVars.randomVersionString}`,
`../static/css/pad.css?v=${clientVars.randomVersionString}`, `../static/css/pad.css?v=${window.clientVars.randomVersionString}`,
...hooks.callAll('aceEditorCSS').map( ...hooks.callAll('aceEditorCSS').map(
// Allow urls to external CSS - http(s):// and //some/path.css // Allow urls to external CSS - http(s):// and //some/path.css
(p) => /\/\//.test(p) ? p : `../static/plugins/${p}`), (p: string) => /\/\//.test(p) ? p : `../static/plugins/${p}`),
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`, `../static/skins/${window.clientVars.skinName}/pad.css?v=${window.clientVars.randomVersionString}`,
]; ];
const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== ''); const skinVariants = window.clientVars.skinVariants.split(' ').filter((x: string) => x !== '');
const outerFrame = document.createElement('iframe'); const outerFrame = document.createElement('iframe');
outerFrame.name = 'ace_outer'; outerFrame.name = 'ace_outer';
outerFrame.frameBorder = 0; // for IE outerFrame.frameBorder = String(0); // for IE
outerFrame.title = 'Ether'; outerFrame.title = 'Ether';
// Some browsers do strange things unless the iframe has a src or srcdoc property: // Some browsers do strange things unless the iframe has a src or srcdoc property:
// - Firefox replaces the frame's contentWindow.document object with a different object after // - Firefox replaces the frame's contentWindow.document object with a different object after
@ -195,8 +206,9 @@ const Ace2Editor = function () {
// srcdoc is avoided because Firefox's Content Security Policy engine does not properly handle // srcdoc is avoided because Firefox's Content Security Policy engine does not properly handle
// 'self' with nested srcdoc iframes: https://bugzilla.mozilla.org/show_bug.cgi?id=1721296 // 'self' with nested srcdoc iframes: https://bugzilla.mozilla.org/show_bug.cgi?id=1721296
outerFrame.src = '../static/empty.html'; outerFrame.src = '../static/empty.html';
info.frame = outerFrame; // @ts-ignore
document.getElementById(containerId).appendChild(outerFrame); this.info.frame = outerFrame;
document.getElementById(containerId)!.appendChild(outerFrame);
const outerWindow = outerFrame.contentWindow; const outerWindow = outerFrame.contentWindow;
debugLog('Ace2Editor.init() waiting for outer frame'); debugLog('Ace2Editor.init() waiting for outer frame');
@ -205,13 +217,13 @@ const Ace2Editor = function () {
// Firefox might replace the outerWindow.document object after iframe creation so this variable // Firefox might replace the outerWindow.document object after iframe creation so this variable
// is assigned after the Window's load event. // is assigned after the Window's load event.
const outerDocument = outerWindow.document; const outerDocument = outerWindow!.document;
// <html> tag // <html> tag
outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants); outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants);
// <head> tag // <head> tag
addStyleTagsFor(outerDocument, includedCSS); this.addStyleTagsFor(outerDocument, includedCSS);
const outerStyle = outerDocument.createElement('style'); const outerStyle = outerDocument.createElement('style');
outerStyle.type = 'text/css'; outerStyle.type = 'text/css';
outerStyle.title = 'dynamicsyntax'; outerStyle.title = 'dynamicsyntax';
@ -236,14 +248,11 @@ const Ace2Editor = function () {
const innerFrame = outerDocument.createElement('iframe'); const innerFrame = outerDocument.createElement('iframe');
innerFrame.name = 'ace_inner'; innerFrame.name = 'ace_inner';
innerFrame.title = 'pad'; innerFrame.title = 'pad';
innerFrame.scrolling = 'no';
innerFrame.frameBorder = 0;
innerFrame.allowTransparency = true; // for IE
// The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above // The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above
// outerFrame.srcdoc. // outerFrame.srcdoc.
innerFrame.src = 'empty.html'; innerFrame.src = 'empty.html';
outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild); outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild);
const innerWindow = innerFrame.contentWindow; const innerWindow: InnerWindow = innerFrame.contentWindow!;
debugLog('Ace2Editor.init() waiting for inner frame'); debugLog('Ace2Editor.init() waiting for inner frame');
await frameReady(innerFrame); await frameReady(innerFrame);
@ -251,69 +260,47 @@ const Ace2Editor = function () {
// Firefox might replace the innerWindow.document object after iframe creation so this variable // Firefox might replace the innerWindow.document object after iframe creation so this variable
// is assigned after the Window's load event. // is assigned after the Window's load event.
const innerDocument = innerWindow.document; const innerDocument = innerWindow!.document;
// <html> tag // <html> tag
innerDocument.documentElement.classList.add('inner-editor', ...skinVariants); innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
// <head> tag // <head> tag
addStyleTagsFor(innerDocument, includedCSS); this.addStyleTagsFor(innerDocument, includedCSS);
//const requireKernel = innerDocument.createElement('script');
//requireKernel.type = 'text/javascript';
//requireKernel.src =
// absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
//innerDocument.head.appendChild(requireKernel);
// Pre-fetch modules to improve load performance.
/*for (const module of ['ace2_inner', 'ace2_common']) {
const script = innerDocument.createElement('script');
script.type = 'text/javascript';
script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
`?callback=require.define&v=${clientVars.randomVersionString}`);
innerDocument.head.appendChild(script);
}*/
const innerStyle = innerDocument.createElement('style'); const innerStyle = innerDocument.createElement('style');
innerStyle.type = 'text/css'; innerStyle.type = 'text/css';
innerStyle.title = 'dynamicsyntax'; innerStyle.title = 'dynamicsyntax';
innerDocument.head.appendChild(innerStyle); innerDocument.head.appendChild(innerStyle);
const headLines = []; const headLines: string[] = [];
hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines}); hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines});
innerDocument.head.appendChild( innerDocument.head.appendChild(
innerDocument.createRange().createContextualFragment(headLines.join('\n'))); innerDocument.createRange().createContextualFragment(headLines.join('\n')));
// <body> tag // <body> tag
innerDocument.body.id = 'innerdocbody'; innerDocument.body.id = 'innerdocbody';
innerDocument.body.classList.add('innerdocbody'); innerDocument.body.classList.add('innerdocbody');
innerDocument.body.setAttribute('spellcheck', 'false'); innerDocument.body.setAttribute('spellcheck', 'false');
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp; innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); // &nbsp;
/*
debugLog('Ace2Editor.init() waiting for require kernel load');
await eventFired(requireKernel, 'load');
debugLog('Ace2Editor.init() require kernel loaded');
const require = innerWindow.require;
require.setRootURI(absUrl('../javascripts/src'));
require.setLibraryURI(absUrl('../javascripts/lib'));
require.setGlobalKeyPath('require');
*/
// intentially moved before requiring client_plugins to save a 307
innerWindow.Ace2Inner = ace2_inner;
innerWindow.plugins = cl_plugins;
innerWindow.$ = innerWindow.jQuery = window.$; // intentially moved before requiring client_plugins to save a 307
innerWindow!.Ace2Inner = ace2_inner;
innerWindow!.plugins = cl_plugins;
innerWindow!.$ = innerWindow.jQuery = window.$;
debugLog('Ace2Editor.init() waiting for plugins'); debugLog('Ace2Editor.init() waiting for plugins');
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));*/
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
await innerWindow.Ace2Inner.init(info, { await innerWindow.Ace2Inner.init(this.info, {
inner: new Cssmanager(innerStyle.sheet), inner: new Cssmanager(innerStyle.sheet),
outer: new Cssmanager(outerStyle.sheet), outer: new Cssmanager(outerStyle.sheet),
parent: new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet), parent: new Cssmanager((document.querySelector('style[title="dynamicsyntax"]') as HTMLStyleElement)!.sheet),
}); });
debugLog('Ace2Editor.init() Ace2Inner.init() returned'); debugLog('Ace2Editor.init() Ace2Inner.init() returned');
loaded = true; this.loaded = true;
doActionsPendingInit(); this.doActionsPendingInit();
debugLog('Ace2Editor.init() done'); debugLog('Ace2Editor.init() done');
}; }
}; }
exports.Ace2Editor = Ace2Editor;

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {Socket} from "socket.io";
/** /**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd) * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
* *
@ -22,7 +24,7 @@
* limitations under the License. * limitations under the License.
*/ */
let socket; let socket: null | Socket;
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
@ -38,6 +40,7 @@ const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient; const getCollabClient = require('./collab_client').getCollabClient;
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
import padcookie from "./pad_cookie"; import padcookie from "./pad_cookie";
const padeditbar = require('./pad_editbar').padeditbar; const padeditbar = require('./pad_editbar').padeditbar;
const padeditor = require('./pad_editor').padeditor; const padeditor = require('./pad_editor').padeditor;
const padimpexp = require('./pad_impexp').padimpexp; const padimpexp = require('./pad_impexp').padimpexp;
@ -45,9 +48,12 @@ const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs'); const padsavedrevs = require('./pad_savedrevs');
const paduserlist = require('./pad_userlist').paduserlist; const paduserlist = require('./pad_userlist').paduserlist;
import {padUtils as padutils} from "./pad_utils"; import {padUtils as padutils} from "./pad_utils";
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString; const randomString = require('./pad_utils').randomString;
import connect from './socketio' import connect from './socketio'
import {SocketClientReadyMessage} from "./types/SocketIOMessage";
import {MapArrayType} from "../../node/types/MapType";
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
@ -61,22 +67,22 @@ const getParameters = [
{ {
name: 'noColors', name: 'noColors',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
settings.noColors = true; pad.settings.noColors = true;
$('#clearAuthorship').hide(); $('#clearAuthorship').hide();
}, },
}, },
{ {
name: 'showControls', name: 'showControls',
checkVal: 'true', checkVal: 'true',
callback: (val) => { callback: (val: any) => {
$('#editbar').css('display', 'flex'); $('#editbar').css('display', 'flex');
}, },
}, },
{ {
name: 'showChat', name: 'showChat',
checkVal: null, checkVal: null,
callback: (val) => { callback: (val: any) => {
if (val === 'false') { if (val === 'false') {
settings.hideChat = true; settings.hideChat = true;
chat.hide(); chat.hide();
@ -103,7 +109,7 @@ const getParameters = [
checkVal: null, checkVal: null,
callback: (val) => { callback: (val) => {
settings.globalUserName = val; settings.globalUserName = val;
clientVars.userName = val; window.clientVars.userName = val;
}, },
}, },
{ {
@ -111,7 +117,7 @@ const getParameters = [
checkVal: null, checkVal: null,
callback: (val) => { callback: (val) => {
settings.globalUserColor = val; settings.globalUserColor = val;
clientVars.userColor = val; window.clientVars.userColor = val;
}, },
}, },
{ {
@ -149,7 +155,7 @@ 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 = window.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) {
@ -169,7 +175,7 @@ const getParams = () => {
const getUrlVars = () => new URL(window.location.href).searchParams; const getUrlVars = () => new URL(window.location.href).searchParams;
const sendClientReady = (isReconnect) => { const sendClientReady = (isReconnect: boolean) => {
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape necessary due to Safari and Opera interpretation of spaces // unescape necessary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId); padId = decodeURIComponent(padId);
@ -196,7 +202,7 @@ const sendClientReady = (isReconnect) => {
name: params.get('userName'), name: params.get('userName'),
}; };
const msg = { const msg: SocketClientReadyMessage = {
component: 'pad', component: 'pad',
type: 'CLIENT_READY', type: 'CLIENT_READY',
padId, padId,
@ -207,11 +213,11 @@ const sendClientReady = (isReconnect) => {
// 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 = this.collabClient!.getCurrentRevisionNumber();
msg.reconnect = true; msg.reconnect = true;
} }
socket.emit("message", msg); socket!.emit("message", msg);
}; };
const handshake = async () => { const handshake = async () => {
@ -261,7 +267,7 @@ const handshake = async () => {
socket.on('shout', (obj) => { socket.on('shout', (obj) => {
if(obj.type === "COLLABROOM") { if (obj.type === "COLLABROOM") {
let date = new Date(obj.data.payload.timestamp); let date = new Date(obj.data.payload.timestamp);
$.gritter.add({ $.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
@ -315,12 +321,13 @@ const handshake = async () => {
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);
} }
if(window.clientVars.mode === "development") { if (window.clientVars.mode === "development") {
console.warn('Enabling development mode with live update') console.warn('Enabling development mode with live update')
socket.on('liveupdate', ()=>{ socket.on('liveupdate', () => {
console.log('Live reload update received') console.log('Live reload update received')
location.reload() location.reload()
@ -381,30 +388,52 @@ class MessageQueue {
} }
} }
const pad = { export class Pad {
// don't access these directly from outside this file, except private collabClient: null;
// for debugging private myUserInfo: null | {
collabClient: null, userId: string,
myUserInfo: null, name: string,
diagnosticInfo: {}, ip: string,
initTime: 0, colorId: string,
clientTimeOffset: null, };
padOptions: {}, private diagnosticInfo: {};
_messageQ: new MessageQueue(), private initTime: number;
private clientTimeOffset: null | number;
private _messageQ: MessageQueue;
private padOptions: MapArrayType<MapArrayType<string>>;
settings: PadSettings = {
LineNumbersDisabled: false,
noColors: false,
useMonospaceFontGlobal: false,
globalUserName: false,
globalUserColor: false,
rtlIsTrue: false,
}
constructor() {
// don't access these directly from outside this file, except
// for debugging
this.collabClient = null
this.myUserInfo = null
this.diagnosticInfo = {}
this.initTime = 0
this.clientTimeOffset = null
this.padOptions = {}
this._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 = () => window.clientVars.padId
getClientIp: () => clientVars.clientIp, getClientIp = () => window.clientVars.clientIp
getColorPalette: () => clientVars.colorPalette, getColorPalette = () => window.clientVars.colorPalette
getPrivilege: (name) => clientVars.accountPrivs[name], getPrivilege = (name: string) => window.clientVars.accountPrivs[name]
getUserId: () => pad.myUserInfo.userId, getUserId = () => this.myUserInfo!.userId
getUserName: () => pad.myUserInfo.name, getUserName = () => this.myUserInfo!.name
userList: () => paduserlist.users(), userList = () => paduserlist.users()
sendClientMessage: (msg) => { sendClientMessage = (msg: string) => {
pad.collabClient.sendClientMessage(msg); this.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
@ -412,28 +441,32 @@ const pad = {
// function. // function.
$(() => (async () => { $(() => (async () => {
if (window.customStart != null) window.customStart(); if (window.customStart != null) window.customStart();
// @ts-ignore
$('#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();
await handshake(); await handshake();
this._afterHandshake(); this._afterHandshake();
})()); })());
}, }
_afterHandshake() { _afterHandshake() {
pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp; this.clientTimeOffset = Date.now() - window.clientVars.serverTimestamp;
// 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()); this.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions; this.padOptions = window.clientVars.initialOptions;
pad.myUserInfo = { this.myUserInfo = {
userId: clientVars.userId, userId: window.clientVars.userId,
name: clientVars.userName, name: window.clientVars.userName,
ip: pad.getClientIp(), ip: this.getClientIp(),
colorId: clientVars.userColor, colorId: window.clientVars.userColor,
}; };
const postAceInit = () => { const postAceInit = () => {
@ -442,7 +475,9 @@ const pad = {
padeditor.ace.focus(); padeditor.ace.focus();
}, 0); }, 0);
const optionsStickyChat = $('#options-stickychat'); const optionsStickyChat = $('#options-stickychat');
optionsStickyChat.on('click', () => { chat.stickToScreen(); }); optionsStickyChat.on('click', () => {
chat.stickToScreen();
});
// if we have a cookie for always showing chat then show it // if we have a cookie for always showing chat then show it
if (padcookie.getPref('chatAlwaysVisible')) { if (padcookie.getPref('chatAlwaysVisible')) {
chat.stickToScreen(true); // stick it to the screen chat.stickToScreen(true); // stick it to the screen
@ -454,15 +489,16 @@ const pad = {
$('#options-chatandusers').prop('checked', true); // set the checkbox to on $('#options-chatandusers').prop('checked', true); // set the checkbox to on
} }
if (padcookie.getPref('showAuthorshipColors') === false) { if (padcookie.getPref('showAuthorshipColors') === false) {
pad.changeViewOption('showAuthorColors', false); this.changeViewOption('showAuthorColors', false);
} }
if (padcookie.getPref('showLineNumbers') === false) { if (padcookie.getPref('showLineNumbers') === false) {
pad.changeViewOption('showLineNumbers', false); this.changeViewOption('showLineNumbers', false);
} }
if (padcookie.getPref('rtlIsTrue') === true) { if (padcookie.getPref('rtlIsTrue') === true) {
pad.changeViewOption('rtlIsTrue', true); this.changeViewOption('rtlIsTrue', true);
} }
pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily')); this.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily'));
// @ts-ignore
$('#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
@ -474,37 +510,39 @@ 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: window.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);
padeditor.init(pad.padOptions.view || {}, this).then(postAceInit); padeditor.init(this.padOptions.view || {}, this).then(postAceInit);
paduserlist.init(pad.myUserInfo, this); paduserlist.init(this.myUserInfo, this);
padconnectionstatus.init(); padconnectionstatus.init();
padmodals.init(this); padmodals.init(this);
pad.collabClient = getCollabClient( this.collabClient = getCollabClient(
padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, padeditor.ace, window.clientVars.collab_client_vars, this.myUserInfo,
{colorPalette: pad.getColorPalette()}, pad); {colorPalette: this.getColorPalette()}, pad);
this._messageQ.setCollabClient(this.collabClient); this._messageQ.setCollabClient(this.collabClient);
pad.collabClient.setOnUserJoin(pad.handleUserJoin); this.collabClient.setOnUserJoin(this.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate); this.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
pad.collabClient.setOnUserLeave(pad.handleUserLeave); this.collabClient.setOnUserLeave(pad.handleUserLeave);
pad.collabClient.setOnClientMessage(pad.handleClientMessage); this.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange); this.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction); this.collabClient.setOnInternalAction(pad.handleCollabAction);
// load initial chat-messages // load initial chat-messages
if (clientVars.chatHead !== -1) { if (window.clientVars.chatHead !== -1) {
const chatHead = clientVars.chatHead; const chatHead = window.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}); this.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');
@ -517,7 +555,9 @@ 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');
@ -558,223 +598,259 @@ const pad = {
this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId
paduserlist.setMyUserInfo(this.myUserInfo); paduserlist.setMyUserInfo(this.myUserInfo);
} }
}, }
dispose: () => { dispose = () => {
padeditor.dispose(); padeditor.dispose();
}, }
notifyChangeName: (newName) => { notifyChangeName = (newName) => {
pad.myUserInfo.name = newName; this.myUserInfo.name = newName;
pad.collabClient.updateUserInfo(pad.myUserInfo); this.collabClient.updateUserInfo(this.myUserInfo);
}, }
notifyChangeColor: (newColorId) => { notifyChangeColor = (newColorId) => {
pad.myUserInfo.colorId = newColorId; this.myUserInfo.colorId = newColorId;
pad.collabClient.updateUserInfo(pad.myUserInfo); this.collabClient.updateUserInfo(this.myUserInfo);
}, }
changePadOption: (key, value) => {
const options = {}; changePadOption = (key: string, value: string) => {
const options: MapArrayType<string> = {};
options[key] = value; options[key] = value;
pad.handleOptionsChange(options); this.handleOptionsChange(options);
pad.collabClient.sendClientMessage( this.collabClient.sendClientMessage(
{ {
type: 'padoptions', type: 'padoptions',
options, options,
changedBy: pad.myUserInfo.name || 'unnamed', changedBy: this.myUserInfo.name || 'unnamed',
}); })
}, }
changeViewOption: (key, value) => {
const options = { changeViewOption = (key: string, value: string) => {
view: {}, const options: MapArrayType<MapArrayType<any>> =
}; {
view: {}
,
}
;
options.view[key] = value; options.view[key] = value;
pad.handleOptionsChange(options); this.handleOptionsChange(options);
}, }
handleOptionsChange: (opts) => {
handleOptionsChange = (opts: MapArrayType<MapArrayType<string>>) => {
// opts object is a full set of options or just // opts object is a full set of options or just
// some options to change // some options to change
if (opts.view) { if (opts.view) {
if (!pad.padOptions.view) { if (!this.padOptions.view) {
pad.padOptions.view = {}; this.padOptions.view = {};
} }
for (const [k, v] of Object.entries(opts.view)) { for (const [k, v] of Object.entries(opts.view)) {
pad.padOptions.view[k] = v; this.padOptions.view[k] = v;
padcookie.setPref(k, v); padcookie.setPref(k, v);
} }
padeditor.setViewOptions(pad.padOptions.view); padeditor.setViewOptions(this.padOptions.view);
} }
}, }
// caller shouldn't mutate the object getPadOptions = () => this.padOptions
getPadOptions: () => pad.padOptions, suggestUserName =
suggestUserName: (userId, name) => { (userId: string, name: string) => {
pad.collabClient.sendClientMessage( this.collabClient.sendClientMessage(
{ {
type: 'suggestUserName', type: 'suggestUserName',
unnamedId: userId, unnamedId: userId,
newName: name, newName: name,
}); });
},
handleUserJoin: (userInfo) => {
paduserlist.userJoinOrUpdate(userInfo);
},
handleUserUpdate: (userInfo) => {
paduserlist.userJoinOrUpdate(userInfo);
},
handleUserLeave: (userInfo) => {
paduserlist.userLeave(userInfo);
},
handleClientMessage: (msg) => {
if (msg.type === 'suggestUserName') {
if (msg.unnamedId === pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) {
pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo);
}
} else if (msg.type === 'newRevisionList') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'revisionLabel') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'padoptions') {
const opts = msg.options;
pad.handleOptionsChange(opts);
} }
}, handleUserJoin = (userInfo) => {
handleChannelStateChange: (newState, message) => { paduserlist.userJoinOrUpdate(userInfo);
const oldFullyConnected = !!padconnectionstatus.isFullyConnected(); }
const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting'); handleUserUpdate = (userInfo) => {
if (newState === 'CONNECTED') { paduserlist.userJoinOrUpdate(userInfo);
padeditor.enable(); }
padeditbar.enable(); handleUserLeave =
padimpexp.enable(); (userInfo) => {
padconnectionstatus.connected(); paduserlist.userLeave(userInfo);
} else if (newState === 'RECONNECTING') { }
padeditor.disable(); // caller shouldn't mutate the object
padeditbar.disable(); handleClientMessage =
padimpexp.disable(); (msg) => {
padconnectionstatus.reconnecting(); if (msg.type === 'suggestUserName') {
} else if (newState === 'DISCONNECTED') { if (msg.unnamedId === pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) {
pad.diagnosticInfo.disconnectedMessage = message; pad.notifyChangeName(msg.newName);
pad.diagnosticInfo.padId = pad.getPadId(); paduserlist.setMyUserInfo(pad.myUserInfo);
pad.diagnosticInfo.socket = {};
// 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
for (const [i, value] of Object.entries(socket.socket || {})) {
const type = typeof value;
if (type === 'string' || type === 'number') {
pad.diagnosticInfo.socket[i] = value;
} }
} else if (msg.type === 'newRevisionList') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'revisionLabel') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'padoptions') {
const opts = msg.options;
pad.handleOptionsChange(opts);
} }
}
pad.asyncSendDiagnosticInfo(); handleChannelStateChange
if (typeof window.ajlog === 'string') { =
window.ajlog += (`Disconnected: ${message}\n`); (newState, message) => {
const oldFullyConnected = !!padconnectionstatus.isFullyConnected();
const wasConnecting = (padconnectionstatus.getStatus().what === 'connecting');
if (newState === 'CONNECTED') {
padeditor.enable();
padeditbar.enable();
padimpexp.enable();
padconnectionstatus.connected();
} else if (newState === 'RECONNECTING') {
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
padconnectionstatus.reconnecting();
} else if (newState === 'DISCONNECTED') {
pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padId = pad.getPadId();
pad.diagnosticInfo.socket = {};
// 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
for (const [i, value] of Object.entries(socket.socket || {})) {
const type = typeof value;
if (type === 'string' || type === 'number') {
pad.diagnosticInfo.socket[i] = value;
}
}
pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog === 'string') {
window.ajlog += (`Disconnected: ${message}\n`);
}
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
padconnectionstatus.disconnected(message);
}
const newFullyConnected = !!padconnectionstatus.isFullyConnected();
if (newFullyConnected !== oldFullyConnected) {
pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
} }
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
padconnectionstatus.disconnected(message);
} }
const newFullyConnected = !!padconnectionstatus.isFullyConnected(); handleIsFullyConnected
if (newFullyConnected !== oldFullyConnected) { =
pad.handleIsFullyConnected(newFullyConnected, wasConnecting); (isConnected, isInitialConnect) => {
pad.determineChatVisibility(isConnected && !isInitialConnect);
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect);
pad.determineAuthorshipColorsVisibility();
setTimeout(() => {
padeditbar.toggleDropDown('none');
}, 1000);
} }
}, determineChatVisibility
handleIsFullyConnected: (isConnected, isInitialConnect) => { =
pad.determineChatVisibility(isConnected && !isInitialConnect); (asNowConnectedFeedback) => {
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect); const chatVisCookie = padcookie.getPref('chatAlwaysVisible');
pad.determineAuthorshipColorsVisibility(); if (chatVisCookie) { // if the cookie is set for chat always visible
setTimeout(() => { chat.stickToScreen(true); // stick it to the screen
padeditbar.toggleDropDown('none'); $('#options-stickychat').prop('checked', true); // set the checkbox to on
}, 1000); } else {
}, $('#options-stickychat').prop('checked', false); // set the checkbox for off
determineChatVisibility: (asNowConnectedFeedback) => { }
const chatVisCookie = padcookie.getPref('chatAlwaysVisible');
if (chatVisCookie) { // if the cookie is set for chat always visible
chat.stickToScreen(true); // stick it to the screen
$('#options-stickychat').prop('checked', true); // set the checkbox to on
} else {
$('#options-stickychat').prop('checked', false); // set the checkbox for off
} }
}, determineChatAndUsersVisibility
determineChatAndUsersVisibility: (asNowConnectedFeedback) => { =
const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible'); (asNowConnectedFeedback) => {
if (chatAUVisCookie) { // if the cookie is set for chat always visible const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible');
chat.chatAndUsers(true); // stick it to the screen if (chatAUVisCookie) { // if the cookie is set for chat always visible
$('#options-chatandusers').prop('checked', true); // set the checkbox to on chat.chatAndUsers(true); // stick it to the screen
} else { $('#options-chatandusers').prop('checked', true); // set the checkbox to on
$('#options-chatandusers').prop('checked', false); // set the checkbox for off } else {
$('#options-chatandusers').prop('checked', false); // set the checkbox for off
}
} }
}, determineAuthorshipColorsVisibility
determineAuthorshipColorsVisibility: () => { =
const authColCookie = padcookie.getPref('showAuthorshipColors'); () => {
if (authColCookie) { const authColCookie = padcookie.getPref('showAuthorshipColors');
pad.changeViewOption('showAuthorColors', true); if (authColCookie) {
$('#options-colorscheck').prop('checked', true); pad.changeViewOption('showAuthorColors', true);
} else { $('#options-colorscheck').prop('checked', true);
$('#options-colorscheck').prop('checked', false); } else {
$('#options-colorscheck').prop('checked', false);
}
} }
}, handleCollabAction
handleCollabAction: (action) => { =
if (action === 'commitPerformed') { (action) => {
padeditbar.setSyncStatus('syncing'); if (action === 'commitPerformed') {
} else if (action === 'newlyIdle') { padeditbar.setSyncStatus('syncing');
padeditbar.setSyncStatus('done'); } else if (action === 'newlyIdle') {
padeditbar.setSyncStatus('done');
}
} }
}, asyncSendDiagnosticInfo
asyncSendDiagnosticInfo: () => { =
window.setTimeout(() => { () => {
$.ajax( window.setTimeout(() => {
$.ajax(
{ {
type: 'post', type: 'post',
url: '../ep/pad/connection-diagnostic-info', url: '../ep/pad/connection-diagnostic-info',
data: { data: {
diagnosticInfo: JSON.stringify(pad.diagnosticInfo), diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
}, },
success: () => {}, success: () => {
error: () => {}, },
error: () => {
},
}); });
}, 0); }, 0);
},
forceReconnect: () => {
$('form#reconnectform input.padId').val(pad.getPadId());
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
$('form#reconnectform input.missedChanges')
.val(JSON.stringify(pad.collabClient.getMissedChanges()));
$('form#reconnectform').trigger('submit');
},
callWhenNotCommitting: (f) => {
pad.collabClient.callWhenNotCommitting(f);
},
getCollabRevisionNumber: () => pad.collabClient.getCurrentRevisionNumber(),
isFullyConnected: () => padconnectionstatus.isFullyConnected(),
addHistoricalAuthors: (data) => {
if (!pad.collabClient) {
window.setTimeout(() => {
pad.addHistoricalAuthors(data);
}, 1000);
} else {
pad.collabClient.addHistoricalAuthors(data);
} }
}, forceReconnect
}; =
() => {
$('form#reconnectform input.padId').val(pad.getPadId());
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
$('form#reconnectform input.diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
$('form#reconnectform input.missedChanges')
.val(JSON.stringify(pad.collabClient.getMissedChanges()));
$('form#reconnectform').trigger('submit');
}
callWhenNotCommitting
=
(f) => {
pad.collabClient.callWhenNotCommitting(f);
}
getCollabRevisionNumber
=
() => pad.collabClient.getCurrentRevisionNumber()
isFullyConnected
=
() => padconnectionstatus.isFullyConnected()
addHistoricalAuthors
=
(data) => {
if (!pad.collabClient) {
window.setTimeout(() => {
pad.addHistoricalAuthors(data);
}, 1000);
} else {
pad.collabClient.addHistoricalAuthors(data);
}
}
}
const init = () => pad.init();
const settings = { export type PadSettings = {
LineNumbersDisabled: false, LineNumbersDisabled: boolean,
noColors: false, noColors: boolean,
useMonospaceFontGlobal: false, useMonospaceFontGlobal: boolean,
globalUserName: false, globalUserName: string | boolean,
globalUserColor: false, globalUserColor: string | boolean,
rtlIsTrue: false, rtlIsTrue: boolean,
}; hideChat?: boolean,
}
export const pad = new Pad()
pad.settings = settings;
exports.baseURL = ''; exports.baseURL = '';
exports.settings = settings;
exports.randomString = randomString; exports.randomString = randomString;
exports.getParams = getParams; exports.getParams = getParams;
exports.pad = pad; exports.pad = pad;

View file

@ -1,211 +0,0 @@
'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 Cookies = require('./pad_utils').Cookies;
import padcookie from "./pad_cookie";
import {padUtils as padutils} from "./pad_utils";
const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n'
const padeditor = (() => {
let pad = undefined;
let settings = undefined;
const self = {
ace: null,
// this is accessed directly from other files
viewZoom: 100,
init: async (initialViewOptions, _pad) => {
pad = _pad;
settings = pad.settings;
self.ace = new Ace2Editor();
await self.ace.init('editorcontainer', '');
$('#editorloadingbox').hide();
// Listen for clicks on sidediv items
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
const targetLineNumber = $(this).index() + 1;
window.location.hash = `L${targetLineNumber}`;
});
exports.focusOnLine(self.ace);
self.ace.setProperty('wraps', true);
self.initViewOptions();
self.setViewOptions(initialViewOptions);
// view bar
$('#viewbarcontents').show();
},
initViewOptions: () => {
// Line numbers
padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck')));
});
// Author colors
padutils.bindCheckboxChange($('#options-colorscheck'), () => {
padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck'));
pad.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck'));
});
// Right to left
padutils.bindCheckboxChange($('#options-rtlcheck'), () => {
pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck')));
});
html10n.bind('localized', () => {
pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection()));
});
// font family change
$('#viewfontmenu').on('change', () => {
pad.changeViewOption('padFontFamily', $('#viewfontmenu').val());
});
// Language
html10n.bind('localized', () => {
$('#languagemenu').val(html10n.getLanguage());
// translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist
// this does not interfere with html10n's normal value-setting because
// html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten
// since a user-edited <input> does *not* have the editempty-class
$('input[data-l10n-id]').each((key, input) => {
input = $(input);
if (input.hasClass('editempty')) {
input.val(html10n.get(input.attr('data-l10n-id')));
}
});
});
$('#languagemenu').val(html10n.getLanguage());
$('#languagemenu').on('change', () => {
Cookies.set('language', $('#languagemenu').val());
html10n.localize([$('#languagemenu').val(), 'en']);
if ($('select').niceSelect) {
$('select').niceSelect('update');
}
});
},
setViewOptions: (newOptions) => {
const getOption = (key, defaultValue) => {
const value = String(newOptions[key]);
if (value === 'true') return true;
if (value === 'false') return false;
return defaultValue;
};
let v;
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
self.ace.setProperty('rtlIsTrue', v);
padutils.setCheckbox($('#options-rtlcheck'), v);
v = getOption('showLineNumbers', true);
self.ace.setProperty('showslinenumbers', v);
padutils.setCheckbox($('#options-linenoscheck'), v);
v = getOption('showAuthorColors', true);
self.ace.setProperty('showsauthorcolors', v);
$('#chattext').toggleClass('authorColors', v);
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
padutils.setCheckbox($('#options-colorscheck'), v);
// Override from parameters if true
if (settings.noColors !== false) {
self.ace.setProperty('showsauthorcolors', !settings.noColors);
}
self.ace.setProperty('textface', newOptions.padFontFamily || '');
},
dispose: () => {
if (self.ace) {
self.ace.destroy();
self.ace = null;
}
},
enable: () => {
if (self.ace) {
self.ace.setEditable(true);
}
},
disable: () => {
if (self.ace) {
self.ace.setEditable(false);
}
},
restoreRevisionText: (dataFromServer) => {
pad.addHistoricalAuthors(dataFromServer.historicalAuthorData);
self.ace.importAText(dataFromServer.atext, dataFromServer.apool, true);
},
};
return self;
})();
exports.padeditor = padeditor;
exports.focusOnLine = (ace) => {
// If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1);
if (lineNumber) {
if (lineNumber[0] === 'L') {
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
const lineNumberInt = parseInt(lineNumber.substr(1));
if (lineNumberInt) {
const $inner = $('iframe[name="ace_outer"]').contents().find('iframe')
.contents().find('#innerdocbody');
const line = $inner.find(`div:nth-child(${lineNumberInt})`);
if (line.length !== 0) {
let offsetTop = line.offset().top;
offsetTop += parseInt($outerdoc.css('padding-top').replace('px', ''));
const hasMobileLayout = $('body').hasClass('mobile-layout');
if (!hasMobileLayout) {
offsetTop += parseInt($inner.css('padding-top').replace('px', ''));
}
const $outerdocHTML = $('iframe[name="ace_outer"]').contents()
.find('#outerdocbody').parent();
$outerdoc.css({top: `${offsetTop}px`}); // Chrome
$outerdocHTML.animate({scrollTop: offsetTop}); // needed for FF
const node = line[0];
ace.callWithAce((ace) => {
const selection = {
startPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
endPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
};
ace.ace_setSelection(selection);
});
}
}
}
}
// End of setSelection / set Y position of editor
};

229
src/static/js/pad_editor.ts Normal file
View file

@ -0,0 +1,229 @@
'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
*/
import {PadType} from "../../node/types/PadType";
/**
* 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.
*/
import {Cookies} from "./pad_utils";
import padcookie from "./pad_cookie";
import {padUtils as padutils} from "./pad_utils";
import {Ace2Editor} from "./ace";
import html10n from '../js/vendors/html10n'
import {MapArrayType} from "../../node/types/MapType";
import {ClientVarData, ClientVarMessage} from "./types/SocketIOMessage";
export class PadEditor {
private pad?: PadType
private settings: undefined| ClientVarData
private ace: any
private viewZoom: number
constructor() {
this.pad = undefined;
this.settings = undefined;
this.ace = null
// this is accessed directly from other files
this.viewZoom = 100
}
init = async (initialViewOptions: MapArrayType<string>, _pad: PadType) => {
this.pad = _pad;
this.settings = this.pad.settings;
this.ace = new Ace2Editor();
await this.ace.init('editorcontainer', '');
$('#editorloadingbox').hide();
// Listen for clicks on sidediv items
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
const targetLineNumber = $(this).index() + 1;
window.location.hash = `L${targetLineNumber}`;
});
this.focusOnLine(this.ace);
this.ace.setProperty('wraps', true);
this.initViewOptions();
this.setViewOptions(initialViewOptions);
// view bar
$('#viewbarcontents').show();
}
initViewOptions = () => {
// Line numbers
padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
this.pad!.changeViewOption('showLineNumbers', padutils.getCheckbox('#options-linenoscheck'));
});
// Author colors
padutils.bindCheckboxChange($('#options-colorscheck'), () => {
padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck') as any);
this.pad!.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck'));
});
// Right to left
padutils.bindCheckboxChange($('#options-rtlcheck'), () => {
this.pad!.changeViewOption('rtlIsTrue', padutils.getCheckbox('#options-rtlcheck'));
});
html10n.bind('localized', () => {
this.pad!.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection()));
});
// font family change
$('#viewfontmenu').on('change', () => {
this.pad!.changeViewOption('padFontFamily', $('#viewfontmenu').val());
});
// Language
html10n.bind('localized', () => {
$('#languagemenu').val(html10n.getLanguage()!);
// translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist
// this does not interfere with html10n's normal value-setting because
// html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten
// since a user-edited <input> does *not* have the editempty-class
$('input[data-l10n-id]').each((key, input) => {
// @ts-ignore
input = $(input);
// @ts-ignore
if (input.hasClass('editempty')) {
// @ts-ignore
input.val(html10n.get(input.attr('data-l10n-id')));
}
});
});
$('#languagemenu').val(html10n.getLanguage()!);
$('#languagemenu').on('change', () => {
Cookies.set('language', $('#languagemenu').val() as string);
html10n.localize([$('#languagemenu').val() as string, 'en']);
// @ts-ignore
if ($('select').niceSelect) {
// @ts-ignore
$('select').niceSelect('update');
}
});
}
setViewOptions = (newOptions: MapArrayType<string>) => {
const getOption = (key: string, defaultValue: boolean) => {
const value = String(newOptions[key]);
if (value === 'true') return true;
if (value === 'false') return false;
return defaultValue;
};
let v;
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
this.ace.setProperty('rtlIsTrue', v);
padutils.setCheckbox($('#options-rtlcheck'), v);
v = getOption('showLineNumbers', true);
this.ace.setProperty('showslinenumbers', v);
padutils.setCheckbox($('#options-linenoscheck'), v);
v = getOption('showAuthorColors', true);
this.ace.setProperty('showsauthorcolors', v);
$('#chattext').toggleClass('authorColors', v);
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
padutils.setCheckbox($('#options-colorscheck'), v);
// Override from parameters if true
if (this.settings!.noColors !== false) {
this.ace.setProperty('showsauthorcolors', !settings.noColors);
}
this.ace.setProperty('textface', newOptions.padFontFamily || '');
}
dispose = () => {
if (this.ace) {
this.ace.destroy();
this.ace = null;
}
}
enable = () => {
if (this.ace) {
this.ace.setEditable(true);
}
}
disable = () => {
if (this.ace) {
this.ace.setEditable(false);
}
}
restoreRevisionText= (dataFromServer: ClientVarData) => {
this.pad!.addHistoricalAuthors(dataFromServer.historicalAuthorData);
this.ace.importAText(dataFromServer.atext, dataFromServer.apool, true);
}
focusOnLine = (ace) => {
// If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1);
if (lineNumber) {
if (lineNumber[0] === 'L') {
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
const lineNumberInt = parseInt(lineNumber.substr(1));
if (lineNumberInt) {
const $inner = $('iframe[name="ace_outer"]').contents().find('iframe')
.contents().find('#innerdocbody');
const line = $inner.find(`div:nth-child(${lineNumberInt})`);
if (line.length !== 0) {
let offsetTop = line.offset()!.top;
offsetTop += parseInt($outerdoc.css('padding-top').replace('px', ''));
const hasMobileLayout = $('body').hasClass('mobile-layout');
if (!hasMobileLayout) {
offsetTop += parseInt($inner.css('padding-top').replace('px', ''));
}
const $outerdocHTML = $('iframe[name="ace_outer"]').contents()
.find('#outerdocbody').parent();
$outerdoc.css({top: `${offsetTop}px`}); // Chrome
$outerdocHTML.animate({scrollTop: offsetTop}); // needed for FF
const node = line[0];
ace.callWithAce((ace) => {
const selection = {
startPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
endPoint: {
index: 0,
focusAtStart: true,
maxIndex: 1,
node,
},
};
ace.ace_setSelection(selection);
});
}
}
}
}
// End of setSelection / set Y position of editor
}
}
export const padEditor = new PadEditor();

View file

@ -343,9 +343,9 @@ class PadUtils {
clear, clear,
}; };
} }
getCheckbox = (node: JQueryNode) => $(node).is(':checked') getCheckbox = (node: string) => $(node).is(':checked')
setCheckbox = setCheckbox =
(node: JQueryNode, value: string) => { (node: JQueryNode, value: boolean) => {
if (value) { if (value) {
$(node).attr('checked', 'checked'); $(node).attr('checked', 'checked');
} else { } else {

View file

@ -0,0 +1,4 @@
export type AText = {
text: string,
attribs: string,
}

View file

@ -0,0 +1,6 @@
export type ChangeSet = {
oldLen: number;
newLen: number;
ops: string;
charBank: string;
}

View file

@ -0,0 +1,5 @@
export type InnerWindow = Window & {
Ace2Inner?: any,
plugins?: any,
jQuery?: any
}

View file

@ -1,13 +1,55 @@
import {MapArrayType} from "../../../node/types/MapType";
import {AText} from "./AText";
import AttributePool from "../AttributePool";
export type SocketIOMessage = { export type SocketIOMessage = {
type: string type: string
accessStatus: string accessStatus: string
} }
export type ClientVarData = {
sessionRefreshInterval: number,
historicalAuthorData:MapArrayType<{
name: string;
colorId: string;
}>,
atext: AText,
apool: AttributePool,
noColors: boolean,
userName: string,
userColor:string,
hideChat: boolean,
padOptions: MapArrayType<string>,
padId: string,
clientIp: string,
colorPalette: MapArrayType<string>,
accountPrivs: MapArrayType<string>,
collab_client_vars: MapArrayType<string>,
chatHead: number,
readonly: boolean,
serverTimestamp: number,
initialOptions: MapArrayType<string>,
userId: string,
}
export type ClientVarMessage = { export type ClientVarMessage = {
data: { data: ClientVarData,
sessionRefreshInterval: number
}
type: string type: string
accessStatus: string accessStatus: string
} }
export type SocketClientReadyMessage = {
type: string
component: string
padId: string
sessionID: string
token: string
userInfo: {
colorId: string|null
name: string|null
},
reconnect?: boolean
client_rev?: number
}

View file

@ -1,6 +1,9 @@
import {ClientVarData} from "./SocketIOMessage";
declare global { declare global {
interface Window { interface Window {
clientVars: any; clientVars: ClientVarData;
$: any $: any,
customStart?:any
} }
} }