mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-20 07:35:05 -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>,
|
||||
getRevisionChangeset: (rev: number)=>Promise<AChangeSet>,
|
||||
appendRevision: (changeset: AChangeSet, author: string)=>Promise<void>,
|
||||
settings:any
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class AttributeMap extends Map {
|
|||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
|
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.
|
||||
*/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Op from "./Op";
|
||||
import {clearOp, copyOp, deserializeOps} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Iterator over a changeset's operations.
|
||||
|
@ -9,19 +10,20 @@ import Op from "./Op";
|
|||
*/
|
||||
export class OpIter {
|
||||
private gen
|
||||
private _next: IteratorResult<Op, void>
|
||||
/**
|
||||
* @param {string} ops - String encoding the change operations to iterate over.
|
||||
*/
|
||||
constructor(ops: string) {
|
||||
this.gen = exports.deserializeOps(ops);
|
||||
this.next = this.gen.next();
|
||||
this.gen = deserializeOps(ops);
|
||||
this._next = this.gen.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether there are any remaining operations.
|
||||
*/
|
||||
hasNext() {
|
||||
return !this.next.done;
|
||||
hasNext(): boolean {
|
||||
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
|
||||
* no more operations.
|
||||
*/
|
||||
next(opOut = new Op()) {
|
||||
next(opOut: Op = new Op()): Op {
|
||||
if (this.hasNext()) {
|
||||
copyOp(this._next.value, opOut);
|
||||
this._next = this._gen.next();
|
||||
copyOp(this._next.value!, opOut);
|
||||
this._next = this.gen.next();
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
|
||||
import {InnerWindow} from "./types/InnerWindow";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -28,21 +30,21 @@ const hooks = require('./pluginfw/hooks');
|
|||
|
||||
const pluginUtils = require('./pluginfw/shared');
|
||||
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 {Cssmanager} = require("./cssmanager");
|
||||
// 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
|
||||
// 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') {
|
||||
predicate = cleanups;
|
||||
cleanups = [];
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
let cleanup;
|
||||
await new Promise((resolve: Function, reject: Function) => {
|
||||
let cleanup: Function;
|
||||
const successCb = () => {
|
||||
if (!predicate()) return;
|
||||
debugLog(`Ace2Editor.init() ${event} event on`, obj);
|
||||
|
@ -57,23 +59,23 @@ const eventFired = async (obj, event, cleanups = [], predicate = () => true) =>
|
|||
};
|
||||
cleanup = () => {
|
||||
cleanup = () => {};
|
||||
obj.removeEventListener(event, successCb);
|
||||
obj.removeEventListener('error', errorCb);
|
||||
obj!.removeEventListener(event, successCb);
|
||||
obj!.removeEventListener('error', errorCb);
|
||||
};
|
||||
cleanups.push(cleanup);
|
||||
obj.addEventListener(event, successCb);
|
||||
obj.addEventListener('error', errorCb);
|
||||
obj!.addEventListener(event, successCb);
|
||||
obj!.addEventListener('error', errorCb);
|
||||
});
|
||||
};
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// the document object after the frame is first created for some reason. ¯\_(ツ)_/¯
|
||||
const doc = () => frame.contentDocument;
|
||||
const cleanups = [];
|
||||
const doc: any = () => frame.contentDocument;
|
||||
const cleanups: Function[] = [];
|
||||
try {
|
||||
await Promise.race([
|
||||
eventFired(frame, 'load', cleanups),
|
||||
|
@ -87,26 +89,39 @@ const frameReady = async (frame) => {
|
|||
}
|
||||
};
|
||||
|
||||
const Ace2Editor = function () {
|
||||
let info = {editor: this};
|
||||
let loaded = false;
|
||||
export class Ace2Editor {
|
||||
info = {editor: this};
|
||||
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);
|
||||
if (loaded) return action();
|
||||
actionsPendingInit.push(action);
|
||||
};
|
||||
if (this.loaded) return action();
|
||||
this.actionsPendingInit.push(action);
|
||||
}
|
||||
|
||||
const doActionsPendingInit = () => {
|
||||
for (const fn of actionsPendingInit) fn();
|
||||
actionsPendingInit = [];
|
||||
};
|
||||
doActionsPendingInit = () => {
|
||||
for (const fn of this.actionsPendingInit) fn();
|
||||
this.actionsPendingInit = [];
|
||||
}
|
||||
|
||||
// The following functions (prefixed by 'ace_') are exposed by editor, but
|
||||
// execution is delayed until init is complete
|
||||
const aceFunctionsPendingInit = [
|
||||
aceFunctionsPendingInit = [
|
||||
'importText',
|
||||
'importAText',
|
||||
'focus',
|
||||
|
@ -124,21 +139,13 @@ const Ace2Editor = function () {
|
|||
'callWithAce',
|
||||
'execCommand',
|
||||
'replaceRange',
|
||||
];
|
||||
]
|
||||
|
||||
for (const fnName of 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.
|
||||
this[fnName] = pendingInit(function (...args) {
|
||||
info[`ace_${fnName}`].apply(this, args);
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
exportText = () => this.loaded ? this.info.ace_exportText() : '(awaiting init)\n';
|
||||
// @ts-ignore
|
||||
getInInternationalComposition = () => this.loaded ? this.info.ace_getInInternationalComposition() : null;
|
||||
|
||||
this.exportText = () => loaded ? info.ace_exportText() : '(awaiting init)\n';
|
||||
|
||||
this.getInInternationalComposition =
|
||||
() => loaded ? info.ace_getInInternationalComposition() : null;
|
||||
|
||||
// prepareUserChangeset:
|
||||
// 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
|
||||
// prepareUserChangeset will return an updated changeset that takes into account the latest user
|
||||
// changes, and modify the changeset to be applied by applyPreparedChangesetToBase accordingly.
|
||||
this.prepareUserChangeset = () => loaded ? info.ace_prepareUserChangeset() : null;
|
||||
|
||||
const addStyleTagsFor = (doc, files) => {
|
||||
// @ts-ignore
|
||||
prepareUserChangeset = () => this.loaded ? this.info.ace_prepareUserChangeset() : null;
|
||||
addStyleTagsFor = (doc: Document, files: string[]) => {
|
||||
for (const file of files) {
|
||||
const link = doc.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
|
@ -158,32 +165,36 @@ const Ace2Editor = function () {
|
|||
link.href = absUrl(encodeURI(file));
|
||||
doc.head.appendChild(link);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.destroy = pendingInit(() => {
|
||||
info.ace_dispose();
|
||||
info.frame.parentNode.removeChild(info.frame);
|
||||
info = null; // prevent IE 6 closure memory leaks
|
||||
destroy = this.pendingInit(() => {
|
||||
// @ts-ignore
|
||||
this.info.ace_dispose();
|
||||
// @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()');
|
||||
// @ts-ignore
|
||||
this.importText(initialCode);
|
||||
|
||||
const includedCSS = [
|
||||
`../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`,
|
||||
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
|
||||
`../static/css/iframe_editor.css?v=${window.clientVars.randomVersionString}`,
|
||||
`../static/css/pad.css?v=${window.clientVars.randomVersionString}`,
|
||||
...hooks.callAll('aceEditorCSS').map(
|
||||
// Allow urls to external CSS - http(s):// and //some/path.css
|
||||
(p) => /\/\//.test(p) ? p : `../static/plugins/${p}`),
|
||||
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
|
||||
// Allow urls to external CSS - http(s):// and //some/path.css
|
||||
(p: string) => /\/\//.test(p) ? p : `../static/plugins/${p}`),
|
||||
`../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');
|
||||
outerFrame.name = 'ace_outer';
|
||||
outerFrame.frameBorder = 0; // for IE
|
||||
outerFrame.frameBorder = String(0); // for IE
|
||||
outerFrame.title = 'Ether';
|
||||
// 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
|
||||
|
@ -195,8 +206,9 @@ const Ace2Editor = function () {
|
|||
// 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
|
||||
outerFrame.src = '../static/empty.html';
|
||||
info.frame = outerFrame;
|
||||
document.getElementById(containerId).appendChild(outerFrame);
|
||||
// @ts-ignore
|
||||
this.info.frame = outerFrame;
|
||||
document.getElementById(containerId)!.appendChild(outerFrame);
|
||||
const outerWindow = outerFrame.contentWindow;
|
||||
|
||||
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
|
||||
// is assigned after the Window's load event.
|
||||
const outerDocument = outerWindow.document;
|
||||
const outerDocument = outerWindow!.document;
|
||||
|
||||
// <html> tag
|
||||
outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants);
|
||||
|
||||
// <head> tag
|
||||
addStyleTagsFor(outerDocument, includedCSS);
|
||||
this.addStyleTagsFor(outerDocument, includedCSS);
|
||||
const outerStyle = outerDocument.createElement('style');
|
||||
outerStyle.type = 'text/css';
|
||||
outerStyle.title = 'dynamicsyntax';
|
||||
|
@ -236,14 +248,11 @@ const Ace2Editor = function () {
|
|||
const innerFrame = outerDocument.createElement('iframe');
|
||||
innerFrame.name = 'ace_inner';
|
||||
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
|
||||
// outerFrame.srcdoc.
|
||||
innerFrame.src = 'empty.html';
|
||||
outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild);
|
||||
const innerWindow = innerFrame.contentWindow;
|
||||
const innerWindow: InnerWindow = innerFrame.contentWindow!;
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for inner frame');
|
||||
await frameReady(innerFrame);
|
||||
|
@ -251,69 +260,47 @@ const Ace2Editor = function () {
|
|||
|
||||
// Firefox might replace the innerWindow.document object after iframe creation so this variable
|
||||
// is assigned after the Window's load event.
|
||||
const innerDocument = innerWindow.document;
|
||||
const innerDocument = innerWindow!.document;
|
||||
|
||||
// <html> tag
|
||||
innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
|
||||
|
||||
// <head> tag
|
||||
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);
|
||||
}*/
|
||||
this.addStyleTagsFor(innerDocument, includedCSS);
|
||||
|
||||
const innerStyle = innerDocument.createElement('style');
|
||||
innerStyle.type = 'text/css';
|
||||
innerStyle.title = 'dynamicsyntax';
|
||||
innerDocument.head.appendChild(innerStyle);
|
||||
const headLines = [];
|
||||
const headLines: string[] = [];
|
||||
hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines});
|
||||
innerDocument.head.appendChild(
|
||||
innerDocument.createRange().createContextualFragment(headLines.join('\n')));
|
||||
innerDocument.createRange().createContextualFragment(headLines.join('\n')));
|
||||
|
||||
// <body> tag
|
||||
innerDocument.body.id = 'innerdocbody';
|
||||
innerDocument.body.classList.add('innerdocbody');
|
||||
innerDocument.body.setAttribute('spellcheck', 'false');
|
||||
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');
|
||||
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));*/
|
||||
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||
await innerWindow.Ace2Inner.init(info, {
|
||||
await innerWindow.Ace2Inner.init(this.info, {
|
||||
inner: new Cssmanager(innerStyle.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');
|
||||
loaded = true;
|
||||
doActionsPendingInit();
|
||||
this.loaded = true;
|
||||
this.doActionsPendingInit();
|
||||
debugLog('Ace2Editor.init() done');
|
||||
};
|
||||
};
|
||||
|
||||
exports.Ace2Editor = Ace2Editor;
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
* 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)
|
||||
*
|
||||
|
@ -22,7 +24,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
let socket;
|
||||
let socket: null | Socket;
|
||||
|
||||
|
||||
// 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 padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
||||
import padcookie from "./pad_cookie";
|
||||
|
||||
const padeditbar = require('./pad_editbar').padeditbar;
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
const padimpexp = require('./pad_impexp').padimpexp;
|
||||
|
@ -45,9 +48,12 @@ const padmodals = require('./pad_modals').padmodals;
|
|||
const padsavedrevs = require('./pad_savedrevs');
|
||||
const paduserlist = require('./pad_userlist').paduserlist;
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const randomString = require('./pad_utils').randomString;
|
||||
import connect from './socketio'
|
||||
import {SocketClientReadyMessage} from "./types/SocketIOMessage";
|
||||
import {MapArrayType} from "../../node/types/MapType";
|
||||
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
|
@ -61,22 +67,22 @@ const getParameters = [
|
|||
{
|
||||
name: 'noColors',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
settings.noColors = true;
|
||||
callback: (val: any) => {
|
||||
pad.settings.noColors = true;
|
||||
$('#clearAuthorship').hide();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'showControls',
|
||||
checkVal: 'true',
|
||||
callback: (val) => {
|
||||
callback: (val: any) => {
|
||||
$('#editbar').css('display', 'flex');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'showChat',
|
||||
checkVal: null,
|
||||
callback: (val) => {
|
||||
callback: (val: any) => {
|
||||
if (val === 'false') {
|
||||
settings.hideChat = true;
|
||||
chat.hide();
|
||||
|
@ -103,7 +109,7 @@ const getParameters = [
|
|||
checkVal: null,
|
||||
callback: (val) => {
|
||||
settings.globalUserName = val;
|
||||
clientVars.userName = val;
|
||||
window.clientVars.userName = val;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -111,7 +117,7 @@ const getParameters = [
|
|||
checkVal: null,
|
||||
callback: (val) => {
|
||||
settings.globalUserColor = val;
|
||||
clientVars.userColor = val;
|
||||
window.clientVars.userColor = val;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -149,7 +155,7 @@ const getParameters = [
|
|||
const getParams = () => {
|
||||
// Tries server enforced options first..
|
||||
for (const setting of getParameters) {
|
||||
let value = clientVars.padOptions[setting.name];
|
||||
let value = window.clientVars.padOptions[setting.name];
|
||||
if (value == null) continue;
|
||||
value = value.toString();
|
||||
if (value === setting.checkVal || setting.checkVal == null) {
|
||||
|
@ -169,7 +175,7 @@ const getParams = () => {
|
|||
|
||||
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);
|
||||
// unescape necessary due to Safari and Opera interpretation of spaces
|
||||
padId = decodeURIComponent(padId);
|
||||
|
@ -196,7 +202,7 @@ const sendClientReady = (isReconnect) => {
|
|||
name: params.get('userName'),
|
||||
};
|
||||
|
||||
const msg = {
|
||||
const msg: SocketClientReadyMessage = {
|
||||
component: 'pad',
|
||||
type: 'CLIENT_READY',
|
||||
padId,
|
||||
|
@ -207,11 +213,11 @@ const sendClientReady = (isReconnect) => {
|
|||
|
||||
// this is a reconnect, lets tell the server our revisionnumber
|
||||
if (isReconnect) {
|
||||
msg.client_rev = pad.collabClient.getCurrentRevisionNumber();
|
||||
msg.client_rev = this.collabClient!.getCurrentRevisionNumber();
|
||||
msg.reconnect = true;
|
||||
}
|
||||
|
||||
socket.emit("message", msg);
|
||||
socket!.emit("message", msg);
|
||||
};
|
||||
|
||||
const handshake = async () => {
|
||||
|
@ -261,7 +267,7 @@ const handshake = async () => {
|
|||
|
||||
|
||||
socket.on('shout', (obj) => {
|
||||
if(obj.type === "COLLABROOM") {
|
||||
if (obj.type === "COLLABROOM") {
|
||||
let date = new Date(obj.data.payload.timestamp);
|
||||
$.gritter.add({
|
||||
// (string | mandatory) the heading of the notification
|
||||
|
@ -315,12 +321,13 @@ const handshake = async () => {
|
|||
window.clientVars = obj.data;
|
||||
if (window.clientVars.sessionRefreshInterval) {
|
||||
const ping =
|
||||
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
|
||||
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {
|
||||
});
|
||||
setInterval(ping, window.clientVars.sessionRefreshInterval);
|
||||
}
|
||||
if(window.clientVars.mode === "development") {
|
||||
if (window.clientVars.mode === "development") {
|
||||
console.warn('Enabling development mode with live update')
|
||||
socket.on('liveupdate', ()=>{
|
||||
socket.on('liveupdate', () => {
|
||||
|
||||
console.log('Live reload update received')
|
||||
location.reload()
|
||||
|
@ -381,30 +388,52 @@ class MessageQueue {
|
|||
}
|
||||
}
|
||||
|
||||
const pad = {
|
||||
// don't access these directly from outside this file, except
|
||||
// for debugging
|
||||
collabClient: null,
|
||||
myUserInfo: null,
|
||||
diagnosticInfo: {},
|
||||
initTime: 0,
|
||||
clientTimeOffset: null,
|
||||
padOptions: {},
|
||||
_messageQ: new MessageQueue(),
|
||||
export class Pad {
|
||||
private collabClient: null;
|
||||
private myUserInfo: null | {
|
||||
userId: string,
|
||||
name: string,
|
||||
ip: string,
|
||||
colorId: string,
|
||||
};
|
||||
private diagnosticInfo: {};
|
||||
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
|
||||
getPadId: () => clientVars.padId,
|
||||
getClientIp: () => clientVars.clientIp,
|
||||
getColorPalette: () => clientVars.colorPalette,
|
||||
getPrivilege: (name) => clientVars.accountPrivs[name],
|
||||
getUserId: () => pad.myUserInfo.userId,
|
||||
getUserName: () => pad.myUserInfo.name,
|
||||
userList: () => paduserlist.users(),
|
||||
sendClientMessage: (msg) => {
|
||||
pad.collabClient.sendClientMessage(msg);
|
||||
},
|
||||
|
||||
init() {
|
||||
getPadId = () => window.clientVars.padId
|
||||
getClientIp = () => window.clientVars.clientIp
|
||||
getColorPalette = () => window.clientVars.colorPalette
|
||||
getPrivilege = (name: string) => window.clientVars.accountPrivs[name]
|
||||
getUserId = () => this.myUserInfo!.userId
|
||||
getUserName = () => this.myUserInfo!.name
|
||||
userList = () => paduserlist.users()
|
||||
sendClientMessage = (msg: string) => {
|
||||
this.collabClient.sendClientMessage(msg);
|
||||
}
|
||||
init = () => {
|
||||
padutils.setupGlobalExceptionHandler();
|
||||
|
||||
// $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is
|
||||
|
@ -412,28 +441,32 @@ const pad = {
|
|||
// function.
|
||||
$(() => (async () => {
|
||||
if (window.customStart != null) window.customStart();
|
||||
// @ts-ignore
|
||||
$('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220});
|
||||
$('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); });
|
||||
$('#readonlyinput').on('click', () => {
|
||||
padeditbar.setEmbedLinks();
|
||||
});
|
||||
padcookie.init();
|
||||
await handshake();
|
||||
this._afterHandshake();
|
||||
})());
|
||||
},
|
||||
}
|
||||
|
||||
_afterHandshake() {
|
||||
pad.clientTimeOffset = Date.now() - clientVars.serverTimestamp;
|
||||
this.clientTimeOffset = Date.now() - window.clientVars.serverTimestamp;
|
||||
// initialize the chat
|
||||
chat.init(this);
|
||||
getParams();
|
||||
|
||||
padcookie.init(); // initialize the cookies
|
||||
pad.initTime = +(new Date());
|
||||
pad.padOptions = clientVars.initialOptions;
|
||||
this.initTime = +(new Date());
|
||||
this.padOptions = window.clientVars.initialOptions;
|
||||
|
||||
pad.myUserInfo = {
|
||||
userId: clientVars.userId,
|
||||
name: clientVars.userName,
|
||||
ip: pad.getClientIp(),
|
||||
colorId: clientVars.userColor,
|
||||
this.myUserInfo = {
|
||||
userId: window.clientVars.userId,
|
||||
name: window.clientVars.userName,
|
||||
ip: this.getClientIp(),
|
||||
colorId: window.clientVars.userColor,
|
||||
};
|
||||
|
||||
const postAceInit = () => {
|
||||
|
@ -442,7 +475,9 @@ const pad = {
|
|||
padeditor.ace.focus();
|
||||
}, 0);
|
||||
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 (padcookie.getPref('chatAlwaysVisible')) {
|
||||
chat.stickToScreen(true); // stick it to the screen
|
||||
|
@ -454,15 +489,16 @@ const pad = {
|
|||
$('#options-chatandusers').prop('checked', true); // set the checkbox to on
|
||||
}
|
||||
if (padcookie.getPref('showAuthorshipColors') === false) {
|
||||
pad.changeViewOption('showAuthorColors', false);
|
||||
this.changeViewOption('showAuthorColors', false);
|
||||
}
|
||||
if (padcookie.getPref('showLineNumbers') === false) {
|
||||
pad.changeViewOption('showLineNumbers', false);
|
||||
this.changeViewOption('showLineNumbers', false);
|
||||
}
|
||||
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');
|
||||
|
||||
// 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)');
|
||||
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');
|
||||
|
||||
hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars, pad});
|
||||
hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars: window.clientVars, pad});
|
||||
};
|
||||
|
||||
// order of inits is important here:
|
||||
padimpexp.init(this);
|
||||
padsavedrevs.init(this);
|
||||
padeditor.init(pad.padOptions.view || {}, this).then(postAceInit);
|
||||
paduserlist.init(pad.myUserInfo, this);
|
||||
padeditor.init(this.padOptions.view || {}, this).then(postAceInit);
|
||||
paduserlist.init(this.myUserInfo, this);
|
||||
padconnectionstatus.init();
|
||||
padmodals.init(this);
|
||||
|
||||
pad.collabClient = getCollabClient(
|
||||
padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo,
|
||||
{colorPalette: pad.getColorPalette()}, pad);
|
||||
this.collabClient = getCollabClient(
|
||||
padeditor.ace, window.clientVars.collab_client_vars, this.myUserInfo,
|
||||
{colorPalette: this.getColorPalette()}, pad);
|
||||
this._messageQ.setCollabClient(this.collabClient);
|
||||
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
|
||||
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
|
||||
pad.collabClient.setOnUserLeave(pad.handleUserLeave);
|
||||
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
|
||||
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
|
||||
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
|
||||
this.collabClient.setOnUserJoin(this.handleUserJoin);
|
||||
this.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
|
||||
this.collabClient.setOnUserLeave(pad.handleUserLeave);
|
||||
this.collabClient.setOnClientMessage(pad.handleClientMessage);
|
||||
this.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
|
||||
this.collabClient.setOnInternalAction(pad.handleCollabAction);
|
||||
|
||||
// load initial chat-messages
|
||||
if (clientVars.chatHead !== -1) {
|
||||
const chatHead = clientVars.chatHead;
|
||||
if (window.clientVars.chatHead !== -1) {
|
||||
const chatHead = window.clientVars.chatHead;
|
||||
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 {
|
||||
// there are no messages
|
||||
$('#chatloadmessagesbutton').css('display', 'none');
|
||||
|
@ -517,7 +555,9 @@ const pad = {
|
|||
$('#chaticon').hide();
|
||||
$('#options-chatandusers').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');
|
||||
|
||||
|
@ -558,223 +598,259 @@ const pad = {
|
|||
this.notifyChangeColor(settings.globalUserColor); // Updates this.myUserInfo.colorId
|
||||
paduserlist.setMyUserInfo(this.myUserInfo);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
dispose: () => {
|
||||
dispose = () => {
|
||||
padeditor.dispose();
|
||||
},
|
||||
notifyChangeName: (newName) => {
|
||||
pad.myUserInfo.name = newName;
|
||||
pad.collabClient.updateUserInfo(pad.myUserInfo);
|
||||
},
|
||||
notifyChangeColor: (newColorId) => {
|
||||
pad.myUserInfo.colorId = newColorId;
|
||||
pad.collabClient.updateUserInfo(pad.myUserInfo);
|
||||
},
|
||||
changePadOption: (key, value) => {
|
||||
const options = {};
|
||||
}
|
||||
notifyChangeName = (newName) => {
|
||||
this.myUserInfo.name = newName;
|
||||
this.collabClient.updateUserInfo(this.myUserInfo);
|
||||
}
|
||||
notifyChangeColor = (newColorId) => {
|
||||
this.myUserInfo.colorId = newColorId;
|
||||
this.collabClient.updateUserInfo(this.myUserInfo);
|
||||
}
|
||||
|
||||
changePadOption = (key: string, value: string) => {
|
||||
const options: MapArrayType<string> = {};
|
||||
options[key] = value;
|
||||
pad.handleOptionsChange(options);
|
||||
pad.collabClient.sendClientMessage(
|
||||
{
|
||||
type: 'padoptions',
|
||||
options,
|
||||
changedBy: pad.myUserInfo.name || 'unnamed',
|
||||
});
|
||||
},
|
||||
changeViewOption: (key, value) => {
|
||||
const options = {
|
||||
view: {},
|
||||
};
|
||||
this.handleOptionsChange(options);
|
||||
this.collabClient.sendClientMessage(
|
||||
{
|
||||
type: 'padoptions',
|
||||
options,
|
||||
changedBy: this.myUserInfo.name || 'unnamed',
|
||||
})
|
||||
}
|
||||
|
||||
changeViewOption = (key: string, value: string) => {
|
||||
const options: MapArrayType<MapArrayType<any>> =
|
||||
{
|
||||
view: {}
|
||||
,
|
||||
}
|
||||
;
|
||||
options.view[key] = value;
|
||||
pad.handleOptionsChange(options);
|
||||
},
|
||||
handleOptionsChange: (opts) => {
|
||||
this.handleOptionsChange(options);
|
||||
}
|
||||
|
||||
handleOptionsChange = (opts: MapArrayType<MapArrayType<string>>) => {
|
||||
// opts object is a full set of options or just
|
||||
// some options to change
|
||||
if (opts.view) {
|
||||
if (!pad.padOptions.view) {
|
||||
pad.padOptions.view = {};
|
||||
if (!this.padOptions.view) {
|
||||
this.padOptions.view = {};
|
||||
}
|
||||
for (const [k, v] of Object.entries(opts.view)) {
|
||||
pad.padOptions.view[k] = v;
|
||||
this.padOptions.view[k] = v;
|
||||
padcookie.setPref(k, v);
|
||||
}
|
||||
padeditor.setViewOptions(pad.padOptions.view);
|
||||
padeditor.setViewOptions(this.padOptions.view);
|
||||
}
|
||||
},
|
||||
// caller shouldn't mutate the object
|
||||
getPadOptions: () => pad.padOptions,
|
||||
suggestUserName: (userId, name) => {
|
||||
pad.collabClient.sendClientMessage(
|
||||
}
|
||||
getPadOptions = () => this.padOptions
|
||||
suggestUserName =
|
||||
(userId: string, name: string) => {
|
||||
this.collabClient.sendClientMessage(
|
||||
{
|
||||
type: 'suggestUserName',
|
||||
unnamedId: userId,
|
||||
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);
|
||||
}
|
||||
},
|
||||
handleChannelStateChange: (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;
|
||||
handleUserJoin = (userInfo) => {
|
||||
paduserlist.userJoinOrUpdate(userInfo);
|
||||
}
|
||||
handleUserUpdate = (userInfo) => {
|
||||
paduserlist.userJoinOrUpdate(userInfo);
|
||||
}
|
||||
handleUserLeave =
|
||||
(userInfo) => {
|
||||
paduserlist.userLeave(userInfo);
|
||||
}
|
||||
// caller shouldn't mutate the object
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pad.asyncSendDiagnosticInfo();
|
||||
if (typeof window.ajlog === 'string') {
|
||||
window.ajlog += (`Disconnected: ${message}\n`);
|
||||
handleChannelStateChange
|
||||
=
|
||||
(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();
|
||||
if (newFullyConnected !== oldFullyConnected) {
|
||||
pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
|
||||
handleIsFullyConnected
|
||||
=
|
||||
(isConnected, isInitialConnect) => {
|
||||
pad.determineChatVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineAuthorshipColorsVisibility();
|
||||
setTimeout(() => {
|
||||
padeditbar.toggleDropDown('none');
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
handleIsFullyConnected: (isConnected, isInitialConnect) => {
|
||||
pad.determineChatVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineChatAndUsersVisibility(isConnected && !isInitialConnect);
|
||||
pad.determineAuthorshipColorsVisibility();
|
||||
setTimeout(() => {
|
||||
padeditbar.toggleDropDown('none');
|
||||
}, 1000);
|
||||
},
|
||||
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
|
||||
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: (asNowConnectedFeedback) => {
|
||||
const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible');
|
||||
if (chatAUVisCookie) { // if the cookie is set for chat always visible
|
||||
chat.chatAndUsers(true); // stick it to the screen
|
||||
$('#options-chatandusers').prop('checked', true); // set the checkbox to on
|
||||
} else {
|
||||
$('#options-chatandusers').prop('checked', false); // set the checkbox for off
|
||||
determineChatAndUsersVisibility
|
||||
=
|
||||
(asNowConnectedFeedback) => {
|
||||
const chatAUVisCookie = padcookie.getPref('chatAndUsersVisible');
|
||||
if (chatAUVisCookie) { // if the cookie is set for chat always visible
|
||||
chat.chatAndUsers(true); // stick it to the screen
|
||||
$('#options-chatandusers').prop('checked', true); // set the checkbox to on
|
||||
} else {
|
||||
$('#options-chatandusers').prop('checked', false); // set the checkbox for off
|
||||
}
|
||||
}
|
||||
},
|
||||
determineAuthorshipColorsVisibility: () => {
|
||||
const authColCookie = padcookie.getPref('showAuthorshipColors');
|
||||
if (authColCookie) {
|
||||
pad.changeViewOption('showAuthorColors', true);
|
||||
$('#options-colorscheck').prop('checked', true);
|
||||
} else {
|
||||
$('#options-colorscheck').prop('checked', false);
|
||||
determineAuthorshipColorsVisibility
|
||||
=
|
||||
() => {
|
||||
const authColCookie = padcookie.getPref('showAuthorshipColors');
|
||||
if (authColCookie) {
|
||||
pad.changeViewOption('showAuthorColors', true);
|
||||
$('#options-colorscheck').prop('checked', true);
|
||||
} else {
|
||||
$('#options-colorscheck').prop('checked', false);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleCollabAction: (action) => {
|
||||
if (action === 'commitPerformed') {
|
||||
padeditbar.setSyncStatus('syncing');
|
||||
} else if (action === 'newlyIdle') {
|
||||
padeditbar.setSyncStatus('done');
|
||||
handleCollabAction
|
||||
=
|
||||
(action) => {
|
||||
if (action === 'commitPerformed') {
|
||||
padeditbar.setSyncStatus('syncing');
|
||||
} else if (action === 'newlyIdle') {
|
||||
padeditbar.setSyncStatus('done');
|
||||
}
|
||||
}
|
||||
},
|
||||
asyncSendDiagnosticInfo: () => {
|
||||
window.setTimeout(() => {
|
||||
$.ajax(
|
||||
asyncSendDiagnosticInfo
|
||||
=
|
||||
() => {
|
||||
window.setTimeout(() => {
|
||||
$.ajax(
|
||||
{
|
||||
type: 'post',
|
||||
url: '../ep/pad/connection-diagnostic-info',
|
||||
data: {
|
||||
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
|
||||
},
|
||||
success: () => {},
|
||||
error: () => {},
|
||||
success: () => {
|
||||
},
|
||||
error: () => {
|
||||
},
|
||||
});
|
||||
}, 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);
|
||||
}, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const init = () => pad.init();
|
||||
|
||||
const settings = {
|
||||
LineNumbersDisabled: false,
|
||||
noColors: false,
|
||||
useMonospaceFontGlobal: false,
|
||||
globalUserName: false,
|
||||
globalUserColor: false,
|
||||
rtlIsTrue: false,
|
||||
};
|
||||
export type PadSettings = {
|
||||
LineNumbersDisabled: boolean,
|
||||
noColors: boolean,
|
||||
useMonospaceFontGlobal: boolean,
|
||||
globalUserName: string | boolean,
|
||||
globalUserColor: string | boolean,
|
||||
rtlIsTrue: boolean,
|
||||
hideChat?: boolean,
|
||||
}
|
||||
|
||||
export const pad = new Pad()
|
||||
|
||||
pad.settings = settings;
|
||||
|
||||
exports.baseURL = '';
|
||||
exports.settings = settings;
|
||||
exports.randomString = randomString;
|
||||
exports.getParams = getParams;
|
||||
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,
|
||||
};
|
||||
}
|
||||
getCheckbox = (node: JQueryNode) => $(node).is(':checked')
|
||||
getCheckbox = (node: string) => $(node).is(':checked')
|
||||
setCheckbox =
|
||||
(node: JQueryNode, value: string) => {
|
||||
(node: JQueryNode, value: boolean) => {
|
||||
if (value) {
|
||||
$(node).attr('checked', 'checked');
|
||||
} 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 = {
|
||||
type: 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 = {
|
||||
data: {
|
||||
sessionRefreshInterval: number
|
||||
}
|
||||
data: ClientVarData,
|
||||
type: 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 {
|
||||
interface Window {
|
||||
clientVars: any;
|
||||
$: any
|
||||
clientVars: ClientVarData;
|
||||
$: any,
|
||||
customStart?:any
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue