mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-20 15:36:16 -04:00
Continued writing
This commit is contained in:
parent
d1ffd5d02f
commit
cef2af15b9
18 changed files with 1455 additions and 1230 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
132
src/static/js/AttributionLinesMutator.ts
Normal file
132
src/static/js/AttributionLinesMutator.ts
Normal 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
109
src/static/js/Builder.ts
Normal 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
|
@ -1,3 +1,6 @@
|
||||||
|
export type OpCode = ''|'='|'+'|'-';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An operation to apply to a shared document.
|
* An operation to apply to a shared document.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
335
src/static/js/TextLinesMutator.ts
Normal file
335
src/static/js/TextLinesMutator.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')); //
|
innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
|
||||||
/*
|
|
||||||
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;
|
|
|
@ -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;
|
|
@ -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
229
src/static/js/pad_editor.ts
Normal 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();
|
|
@ -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 {
|
||||||
|
|
4
src/static/js/types/AText.ts
Normal file
4
src/static/js/types/AText.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export type AText = {
|
||||||
|
text: string,
|
||||||
|
attribs: string,
|
||||||
|
}
|
6
src/static/js/types/ChangeSet.ts
Normal file
6
src/static/js/types/ChangeSet.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export type ChangeSet = {
|
||||||
|
oldLen: number;
|
||||||
|
newLen: number;
|
||||||
|
ops: string;
|
||||||
|
charBank: string;
|
||||||
|
}
|
5
src/static/js/types/InnerWindow.ts
Normal file
5
src/static/js/types/InnerWindow.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type InnerWindow = Window & {
|
||||||
|
Ace2Inner?: any,
|
||||||
|
plugins?: any,
|
||||||
|
jQuery?: any
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import {ClientVarData} from "./SocketIOMessage";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
clientVars: any;
|
clientVars: ClientVarData;
|
||||||
$: any
|
$: any,
|
||||||
|
customStart?:any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue