UNPKG

superpowers-game-ftext-plugin

Version:

Generic text asset for the Superpowers Game system of Superpowers, the extensible HTML5 2D+3D game engine.

401 lines (328 loc) 14.9 kB
import fTextSettingsResource from "../data/fTextSettingsResource"; import * as OT from "operational-transform"; (<any>window).CodeMirror = require("codemirror"); require("codemirror/addon/search/search"); require("codemirror/addon/search/searchcursor"); require("codemirror/addon/edit/closebrackets"); require("codemirror/addon/comment/comment"); require("codemirror/addon/hint/show-hint"); require("codemirror/addon/selection/active-line"); require("codemirror/keymap/sublime"); require("codemirror/mode/javascript/javascript"); require("codemirror/mode/clike/clike"); // added for fText plugin require("codemirror/addon/fold/foldcode"); require("codemirror/addon/fold/foldgutter"); require("codemirror/addon/fold/brace-fold"); require("codemirror/addon/fold/comment-fold"); require("codemirror/addon/fold/indent-fold"); require("codemirror/addon/fold/xml-fold"); require("codemirror/addon/fold/markdown-fold"); require("codemirror/addon/search/match-highlighter"); require("codemirror/addon/edit/matchtags"); // depends on xml-fold require("codemirror/addon/edit/trailingspace"); require("codemirror/addon/edit/closetag"); // depends on xml-fold require("codemirror/addon/selection/active-line"); require("codemirror/addon/hint/anyword-hint"); require("../codemirror-linters/lint"); (<any>window).CSSLint = require("csslint").CSSLint; require("codemirror/addon/lint/css-lint"); (<any>window).JSHINT = require("jshint").JSHINT; require("codemirror/addon/lint/javascript-lint"); (<any>window).jsonlint = require("jsonlint"); require("../codemirror-linters/json-lint"); (<any>window).csonparser = require("cson-parser"); require("../codemirror-linters/cson-lint"); (<any>window).jade = require("jade"); require("../codemirror-linters/jade-lint"); (<any>window).stylus = require("stylus"); require("../codemirror-linters/stylus-lint"); (<any>window).jsyaml = require("js-yaml"); require("../codemirror-linters/yaml-lint"); require("codemirror/keymap/emacs"); require("codemirror/keymap/vim"); require("codemirror/mode/htmlmixed/htmlmixed"); // load js, css, xml require("codemirror/mode/jade/jade"); require("codemirror/mode/markdown/markdown"); // load xml require("codemirror/mode/coffeescript/coffeescript"); require("codemirror/mode/stylus/stylus"); require("codemirror/mode/yaml/yaml"); // /added for fText plugin class fTextEditorWidget { textEditorResource: fTextSettingsResource; codeMirrorInstance: CodeMirror.EditorFromTextArea; editCallback: EditCallback; sendOperationCallback: SendOperationCallback; saveCallback: Function; clientId: number; tmpCodeMirrorDoc = new CodeMirror.Doc(""); texts: string[] = []; undoTimeout: number; undoStack: OT.TextOperation[] = []; undoQuantityByAction: number[] = [0]; redoStack: OT.TextOperation[] = []; redoQuantityByAction: number[] = [0]; sentOperation: OT.TextOperation; pendingOperation: OT.TextOperation; useSoftTab = true; // use spaces as tabs = true (indent with tabs = false) constructor(projectClient: SupClient.ProjectClient, clientId: number, textArea: HTMLTextAreaElement, options: TextEditorWidgetOptions) { let extraKeys: { [name: string]: string|Function } = { "F9": () => {}, "Tab": (cm: any) => { if (cm.getSelection() !== "") cm.execCommand("indentMore") else { if (this.useSoftTab) cm.execCommand("insertSoftTab"); else cm.execCommand("insertTab"); } }, "Cmd-X": () => { document.execCommand("cut"); }, "Cmd-C": () => { document.execCommand("copy"); }, "Cmd-V": () => { document.execCommand("paste"); }, "Ctrl-Z": () => { this.undo(); }, "Cmd-Z": () => { this.undo(); }, "Shift-Ctrl-Z": () => { this.redo(); }, "Shift-Cmd-Z": () => { this.redo(); }, "Ctrl-Y": () => { this.redo(); }, "Cmd-Y": () => { this.redo(); }, "Ctrl-S": () => { this.saveCallback(); }, "Cmd-S": () => { this.saveCallback(); } } if (options.extraKeys != null) { for (let keyName in options.extraKeys) { extraKeys[keyName] = options.extraKeys[keyName]; } } this.editCallback = options.editCallback; this.sendOperationCallback = options.sendOperationCallback; this.saveCallback = options.saveCallback; let editorConfig: any = { // theme: "monokai", lineNumbers: true, matchBrackets: true, styleActiveLine: true, autoCloseBrackets: true, gutters: ["CodeMirror-linenumbers"], indentWithTabs: false, indentUnit: 2, tabSize: 2, extraKeys: extraKeys, keyMap: "sublime", viewportMargin: Infinity, mode: options.mode, readOnly: true }; this.codeMirrorInstance = CodeMirror.fromTextArea(textArea, editorConfig); this.codeMirrorInstance.on("changes", <any>this.edit); this.codeMirrorInstance.on("beforeChange", this.beforeChange); this.clientId = clientId; projectClient.subResource("fTextSettings", this); } setText(text: string) { this.codeMirrorInstance.getDoc().setValue(text); this.codeMirrorInstance.getDoc().clearHistory(); this.codeMirrorInstance.setOption("readOnly", false); this.codeMirrorInstance.focus(); } beforeChange = (instance: CodeMirror.Editor, change: any) => { if (change.origin === "setValue" || change.origin === "network") return; let lastText = instance.getDoc().getValue(); if (lastText !== this.texts[this.texts.length - 1]) this.texts.push(lastText); } edit = (instance: CodeMirror.Editor, changes: CodeMirror.EditorChange[]) => { if (this.editCallback != null) this.editCallback(this.codeMirrorInstance.getDoc().getValue(), (<any>changes[0]).origin); let undoRedo = false; let operationToSend: OT.TextOperation; for (let changeIndex = 0; changeIndex < changes.length; changeIndex++) { let change = changes[changeIndex]; let origin: string = (<any>change).origin; // Modification from an other person if (origin === "setValue" || origin ==="network") continue; this.tmpCodeMirrorDoc.setValue(this.texts[changeIndex]); let operation = new OT.TextOperation(this.clientId); for (let line = 0; line < change.from.line; line++) operation.retain(this.tmpCodeMirrorDoc.getLine(line).length + 1); operation.retain(change.from.ch); let offset = 0; if (change.removed.length !== 1 || change.removed[0] !== "") { for (let index = 0; index < change.removed.length; index++) { let text = change.removed[index]; if (index !== 0) { operation.delete("\n"); offset += 1; } operation.delete(text); offset += text.length; } } if (change.text.length !== 1 || change.text[0] !== "") { for (let index = 0; index < change.text.length; index++) { if (index !== 0) operation.insert("\n"); operation.insert(change.text[index]); } } let beforeLength = (operation.ops[0].attributes.amount != null) ? operation.ops[0].attributes.amount : 0; operation.retain(this.tmpCodeMirrorDoc.getValue().length - beforeLength - offset); if (operationToSend == null) operationToSend = operation.clone(); else operationToSend = operationToSend.compose(operation); if (origin === "undo" || origin === "redo") undoRedo = true; } this.texts.length = 0; if (operationToSend == null) return; if (! undoRedo) { if (this.undoTimeout != null) { clearTimeout(this.undoTimeout); this.undoTimeout = null; } this.undoStack.push(operationToSend.clone().invert()); this.undoQuantityByAction[this.undoQuantityByAction.length - 1] += 1; if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] > 20) this.undoQuantityByAction.push(0); else { this.undoTimeout = window.setTimeout(() => { this.undoTimeout = null; this.undoQuantityByAction.push(0); }, 500); } this.redoStack.length = 0; this.redoQuantityByAction.length = 0; } if (this.sentOperation == null) { this.sendOperationCallback(operationToSend.serialize()); this.sentOperation = operationToSend; } else { if (this.pendingOperation != null) this.pendingOperation = this.pendingOperation.compose(operationToSend); else this.pendingOperation = operationToSend; } } receiveEditText(operationData: OperationData) { if (this.clientId === operationData.userId) { if (this.pendingOperation != null) { this.sendOperationCallback(this.pendingOperation.serialize()); this.sentOperation = this.pendingOperation; this.pendingOperation = null; } else this.sentOperation = null; return; } // Transform operation and local changes let operation = new OT.TextOperation(); operation.deserialize(operationData); if (this.sentOperation != null) { [this.sentOperation, operation] = this.sentOperation.transform(operation); if (this.pendingOperation != null) [this.pendingOperation, operation] = this.pendingOperation.transform(operation); } this.undoStack = transformStack(this.undoStack, operation); this.redoStack = transformStack(this.redoStack, operation); this.applyOperation(operation.clone(), "network", false); } applyOperation(operation: OT.TextOperation, origin: string, moveCursor: boolean) { let cursorPosition = 0; let line = 0; for (let op of operation.ops) { switch (op.type) { case "retain": { while (true) { if (op.attributes.amount <= this.codeMirrorInstance.getDoc().getLine(line).length - cursorPosition) break; op.attributes.amount -= this.codeMirrorInstance.getDoc().getLine(line).length + 1 - cursorPosition; cursorPosition = 0; line++; } cursorPosition += op.attributes.amount; break; } case "insert": { let cursor = this.codeMirrorInstance.getDoc().getCursor(); let texts = op.attributes.text.split("\n"); for (let textIndex = 0; textIndex < texts.length; textIndex++) { let text = texts[textIndex]; if (textIndex !== texts.length - 1) text += "\n"; (<any>this.codeMirrorInstance).replaceRange(text, { line, ch: cursorPosition }, null, origin); cursorPosition += text.length; if (textIndex !== texts.length - 1) { cursorPosition = 0; line++; } } if (line === cursor.line && cursorPosition === cursor.ch) { if (! operation.gotPriority(this.clientId)) { for (let i = 0; i < op.attributes.text.length; i++) (<any>this.codeMirrorInstance).execCommand("goCharLeft"); } } if (moveCursor) (<any>this.codeMirrorInstance).setCursor(line, cursorPosition); //use this way insted ? this.codeMirrorInstance.getDoc().setCursor({ line, ch: cursorPosition }); break; } case "delete": { let texts = op.attributes.text.split("\n"); for (let textIndex = 0; textIndex < texts.length; textIndex++) { let text = texts[textIndex]; if (texts[textIndex + 1] != null) (<any>this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line: line + 1, ch: 0 }, origin); else (<any>this.codeMirrorInstance).replaceRange("", { line, ch: cursorPosition }, { line, ch: cursorPosition + text.length }, origin); if (moveCursor) (<any>this.codeMirrorInstance).setCursor(line, cursorPosition); } break; } } } } undo() { if (this.undoStack.length === 0) return; if (this.undoQuantityByAction[this.undoQuantityByAction.length - 1] === 0) this.undoQuantityByAction.pop(); let undoQuantityByAction = this.undoQuantityByAction[this.undoQuantityByAction.length - 1]; for (let i = 0; i < undoQuantityByAction; i++) { let operationToUndo = this.undoStack[this.undoStack.length - 1]; this.applyOperation(operationToUndo.clone(), "undo", true); this.undoStack.pop() this.redoStack.push(operationToUndo.invert()); } if (this.undoTimeout != null) { clearTimeout(this.undoTimeout); this.undoTimeout = null; } this.redoQuantityByAction.push(this.undoQuantityByAction[this.undoQuantityByAction.length - 1]); this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = 0; } redo() { if (this.redoStack.length === 0) return; let redoQuantityByAction = this.redoQuantityByAction[this.redoQuantityByAction.length - 1] for (let i = 0; i < redoQuantityByAction; i++) { let operationToRedo = this.redoStack[this.redoStack.length - 1]; this.applyOperation(operationToRedo.clone(), "undo", true); this.redoStack.pop() this.undoStack.push(operationToRedo.invert()); } if (this.undoTimeout != null) { clearTimeout(this.undoTimeout); this.undoTimeout = null; this.undoQuantityByAction.push(this.redoQuantityByAction[this.redoQuantityByAction.length - 1]); } else this.undoQuantityByAction[this.undoQuantityByAction.length - 1] = this.redoQuantityByAction[this.redoQuantityByAction.length - 1]; this.undoQuantityByAction.push(0); this.redoQuantityByAction.pop(); } clear() { if (this.undoTimeout != null) clearTimeout(this.undoTimeout); } onResourceReceived = (resourceId: string, resource: fTextSettingsResource) => { this.textEditorResource = resource; this.codeMirrorInstance.setOption("tabSize", resource.pub.tabSize); this.codeMirrorInstance.setOption("indentUnit", resource.pub.tabSize); this.codeMirrorInstance.setOption("indentWithTabs", resource.pub.indentWithTabs); this.useSoftTab = !resource.pub.indentWithTabs; } onResourceEdited = (resourceId: string, command: string, propertyName: string) => { switch(propertyName) { case "tabSize": this.codeMirrorInstance.setOption("tabSize", this.textEditorResource.pub.tabSize); this.codeMirrorInstance.setOption("indentUnit", this.textEditorResource.pub.tabSize); break; case "indentWithTabs": this.useSoftTab = !this.textEditorResource.pub.indentWithTabs; this.codeMirrorInstance.setOption("indentWithTabs", this.textEditorResource.pub.indentWithTabs); break; } } } export = fTextEditorWidget; function transformStack(stack: OT.TextOperation[], operation: OT.TextOperation) { if (stack.length === 0) return stack; let newStack: OT.TextOperation[] = []; for (let i = stack.length - 1; i > 0; i--) { let pair = stack[i].transform(operation); newStack.push(pair[0]); operation = pair[1]; } return newStack.reverse(); }