diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/DocumentManager.ts | 41 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 303 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 42 | ||||
| -rw-r--r-- | src/client/util/Scripting.ts | 127 | ||||
| -rw-r--r-- | src/client/util/ScrollBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 33 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 2 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 240 | ||||
| -rw-r--r-- | src/client/util/Transform.ts | 56 | ||||
| -rw-r--r-- | src/client/util/TypedEvent.ts | 4 | ||||
| -rw-r--r-- | src/client/util/UndoManager.ts | 50 | ||||
| -rw-r--r-- | src/client/util/type_decls.d | 2 |
12 files changed, 523 insertions, 381 deletions
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index bf59fbb43..3b5a5b470 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,11 +1,9 @@ -import React = require('react') -import { observer } from 'mobx-react'; -import { observable, action, computed } from 'mobx'; -import { Document } from "../../fields/Document" -import { DocumentView } from '../views/nodes/DocumentView'; -import { KeyStore } from '../../fields/KeyStore'; +import { computed, observable } from 'mobx'; +import { Document } from "../../fields/Document"; import { FieldWaiting } from '../../fields/Field'; +import { KeyStore } from '../../fields/KeyStore'; import { ListField } from '../../fields/ListField'; +import { DocumentView } from '../views/nodes/DocumentView'; export class DocumentManager { @@ -27,11 +25,6 @@ export class DocumentManager { // this.DocumentViews = new Array<DocumentView>(); } - public getAllDocumentViews(collection: Document) { - return this.DocumentViews.filter(dv => - dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document == collection); - } - public getDocumentView(toFind: Document): DocumentView | null { let toReturn: DocumentView | null; @@ -40,19 +33,18 @@ export class DocumentManager { //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document; - // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { - if (Object.is(doc, toFind)) { + if (doc === toFind) { toReturn = view; return; } let docSrc = doc.GetT(KeyStore.Prototype, Document); - if (docSrc && docSrc != FieldWaiting && Object.is(docSrc, toFind)) { + if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { toReturn = view; } - }) + }); - return (toReturn); + return toReturn; } public getDocumentViews(toFind: Document): DocumentView[] { @@ -63,31 +55,30 @@ export class DocumentManager { let doc = view.props.Document; // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { - if (Object.is(doc, toFind)) { + if (doc === toFind) { toReturn.push(view); } else { let docSrc = doc.GetT(KeyStore.Prototype, Document); - if (docSrc && docSrc != FieldWaiting && Object.is(docSrc, toFind)) { + if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { toReturn.push(view); } } - }) + }); - return (toReturn); + return toReturn; } @computed public get LinkedDocumentViews() { return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField); - if (linksList && linksList != FieldWaiting && linksList.Data.length) { + if (linksList && linksList !== FieldWaiting && linksList.Data.length) { pairs.push(...linksList.Data.reduce((pairs, link) => { if (link instanceof Document) { let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document); - if (linkToDoc && linkToDoc != FieldWaiting) { - DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - pairs.push({ a: dv, b: docView1, l: link }) - }) + if (linkToDoc && linkToDoc !== FieldWaiting) { + DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => + pairs.push({ a: dv, b: docView1, l: link })); } } return pairs; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 70b1c9829..4bd654e15 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,22 +1,24 @@ -import { DocumentDecorations } from "../views/DocumentDecorations"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { Document } from "../../fields/Document" import { action } from "mobx"; -import { ImageField } from "../../fields/ImageField"; +import { Document } from "../../fields/Document"; +import { FieldWaiting } from "../../fields/Field"; import { KeyStore } from "../../fields/KeyStore"; -import { CollectionView } from "../views/collections/CollectionView"; -import { DocumentView } from "../views/nodes/DocumentView"; +import { emptyFunction } from "../../Utils"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { DocumentDecorations } from "../views/DocumentDecorations"; +import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; -export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, removeFunc: (containingCollection: CollectionView) => void = () => { }) { +export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) { let onRowMove = action((e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); - var dragData = new DragManager.DocumentDragData(docFunc()); - dragData.removeDocument = removeFunc; - DragManager.StartDocumentDrag(_reference.current!, dragData); + var dragData = new DragManager.DocumentDragData([docFunc()]); + dragData.copyOnDrop = copyOnDrop; + dragData.moveDocument = moveFunc; + DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); }); let onRowUp = action((e: PointerEvent): void => { document.removeEventListener("pointermove", onRowMove); @@ -24,20 +26,45 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: }); let onItemDown = (e: React.PointerEvent) => { // if (this.props.isSelected() || this.props.isTopMost) { - if (e.button == 0) { + if (e.button === 0) { e.stopPropagation(); if (e.shiftKey) { - CollectionDockingView.Instance.StartOtherDrag(docFunc(), e); + CollectionDockingView.Instance.StartOtherDrag([docFunc()], e); } else { document.addEventListener("pointermove", onRowMove); - document.addEventListener('pointerup', onRowUp); + document.addEventListener("pointerup", onRowUp); } } //} - } + }; return onItemDown; } +export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Document) { + let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document); + let draggedDocs = (srcTarg && srcTarg !== FieldWaiting) ? + srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => + (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; + let draggedFromDocs = (srcTarg && srcTarg !== FieldWaiting) ? + srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => + (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; + draggedDocs.push(...draggedFromDocs); + if (draggedDocs.length) { + let moddrag = [] as Document[]; + for (const draggedDoc of draggedDocs) { + let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document); + if (doc) moddrag.push(doc); + } + let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); + DragManager.StartDocumentDrag([dragEle], dragData, x, y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } +} + export namespace DragManager { export function Root() { const root = document.getElementById("root"); @@ -50,7 +77,9 @@ export namespace DragManager { let dragDiv: HTMLDivElement; export enum DragButtons { - Left = 1, Right = 2, Both = Left | Right + Left = 1, + Right = 2, + Both = Left | Right } interface DragOptions { @@ -63,8 +92,7 @@ export namespace DragManager { (): void; } - export class DragCompleteEvent { - } + export class DragCompleteEvent { } export interface DragHandlers { dragComplete: (e: DragCompleteEvent) => void; @@ -74,21 +102,27 @@ export namespace DragManager { handlers: DropHandlers; } export class DropEvent { - constructor(readonly x: number, readonly y: number, readonly data: { [id: string]: any }) { } + constructor( + readonly x: number, + readonly y: number, + readonly data: { [id: string]: any } + ) { } } - - export interface DropHandlers { drop: (e: Event, de: DropEvent) => void; } - - export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer { + export function MakeDropTarget( + element: HTMLElement, + options: DropOptions + ): DragDropDisposer { if ("canDrop" in element.dataset) { - throw new Error("Element is already droppable, can't make it droppable again"); + throw new Error( + "Element is already droppable, can't make it droppable again" + ); } - element.dataset["canDrop"] = "true"; + element.dataset.canDrop = "true"; const handler = (e: Event) => { const ce = e as CustomEvent<DropEvent>; options.handlers.drop(e, ce.detail); @@ -96,78 +130,106 @@ export namespace DragManager { element.addEventListener("dashOnDrop", handler); return () => { element.removeEventListener("dashOnDrop", handler); - delete element.dataset["canDrop"] + delete element.dataset.canDrop; }; } + export type MoveFunction = (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; export class DocumentDragData { - constructor(dragDoc: Document) { - this.draggedDocument = dragDoc; - this.droppedDocument = dragDoc; + constructor(dragDoc: Document[]) { + this.draggedDocuments = dragDoc; + this.droppedDocuments = dragDoc; + this.xOffset = 0; + this.yOffset = 0; } - draggedDocument: Document; - droppedDocument: Document; - xOffset?: number; - yOffset?: number; + draggedDocuments: Document[]; + droppedDocuments: Document[]; + xOffset: number; + yOffset: number; aliasOnDrop?: boolean; - removeDocument?: (collectionDrop: CollectionView) => void; + copyOnDrop?: boolean; + moveDocument?: MoveFunction; [id: string]: any; } - export function StartDocumentDrag(ele: HTMLElement, dragData: DocumentDragData, options?: DragOptions) { - StartDrag(ele, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocument = dragData.aliasOnDrop ? dragData.draggedDocument.CreateAlias() : dragData.draggedDocument); + + export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag(eles, dragData, downX, downY, options, + (dropData: { [id: string]: any }) => (dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => d.CreateAlias()) : dragData.copyOnDrop ? dragData.draggedDocuments.map(d => d.Copy(true) as Document) : dragData.draggedDocuments)); } export class LinkDragData { - constructor(linkSourceDoc: DocumentView) { - this.linkSourceDocumentView = linkSourceDoc; + constructor(linkSourceDoc: Document) { + this.linkSourceDocument = linkSourceDoc; } - linkSourceDocumentView: DocumentView; + droppedDocuments: Document[] = []; + linkSourceDocument: Document; [id: string]: any; } - export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, options?: DragOptions) { - StartDrag(ele, dragData, options); + + export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag([ele], dragData, downX, downY, options); } - function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { + + function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { if (!dragDiv) { dragDiv = document.createElement("div"); - dragDiv.className = "dragManager-dragDiv" + dragDiv.className = "dragManager-dragDiv"; DragManager.Root().appendChild(dragDiv); } - const w = ele.offsetWidth, h = ele.offsetHeight; - const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / w, scaleY = rect.height / h; - let x = rect.left, y = rect.top; - // const offsetX = e.x - rect.left, offsetY = e.y - rect.top; - - let dragElement = ele.cloneNode(true) as HTMLElement; - dragElement.style.opacity = "0.7"; - dragElement.style.position = "absolute"; - dragElement.style.bottom = ""; - dragElement.style.left = ""; - dragElement.style.transformOrigin = "0 0"; - dragElement.style.zIndex = "1000"; - dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; - dragElement.style.width = `${rect.width / scaleX}px`; - dragElement.style.height = `${rect.height / scaleY}px`; - - // bcz: PDFs don't show up if you clone them because they contain a canvas. - // however, PDF's have a thumbnail field that contains an image of their canvas. - // So we replace the pdf's canvas with the image thumbnail - const doc: Document = dragData["draggedDocument"]; - if (doc) { - var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; - let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField); - if (pdfBox && pdfBox.childElementCount && thumbnail) { - let img = new Image(); - img!.src = thumbnail.toString(); - img!.style.position = "absolute"; - img!.style.width = `${rect.width / scaleX}px`; - img!.style.height = `${rect.height / scaleY}px`; - pdfBox.replaceChild(img!, pdfBox.children[0]) - } - } + MainOverlayTextBox.Instance.SetTextDoc(); + + let scaleXs: number[] = []; + let scaleYs: number[] = []; + let xs: number[] = []; + let ys: number[] = []; + + const docs: Document[] = + dragData instanceof DocumentDragData ? dragData.draggedDocuments : []; + let dragElements = eles.map(ele => { + const w = ele.offsetWidth, + h = ele.offsetHeight; + const rect = ele.getBoundingClientRect(); + const scaleX = rect.width / w, + scaleY = rect.height / h; + let x = rect.left, + y = rect.top; + xs.push(x); + ys.push(y); + scaleXs.push(scaleX); + scaleYs.push(scaleY); + let dragElement = ele.cloneNode(true) as HTMLElement; + dragElement.style.opacity = "0.7"; + dragElement.style.position = "absolute"; + dragElement.style.margin = "0"; + dragElement.style.top = "0"; + dragElement.style.bottom = ""; + dragElement.style.left = "0"; + dragElement.style.transformOrigin = "0 0"; + dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; + dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; + dragElement.style.width = `${rect.width / scaleX}px`; + dragElement.style.height = `${rect.height / scaleY}px`; + + // bcz: if PDFs are rendered with svg's, then this code isn't needed + // bcz: PDFs don't show up if you clone them when rendered using a canvas. + // however, PDF's have a thumbnail field that contains an image of their canvas. + // So we replace the pdf's canvas with the image thumbnail + // if (docs.length) { + // var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; + // let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField); + // if (pdfBox && pdfBox.childElementCount && thumbnail) { + // let img = new Image(); + // img.src = thumbnail.toString(); + // img.style.position = "absolute"; + // img.style.width = `${rect.width / scaleX}px`; + // img.style.height = `${rect.height / scaleY}px`; + // pdfBox.replaceChild(img, pdfBox.children[0]) + // } + // } - dragDiv.appendChild(dragElement); + dragDiv.appendChild(dragElement); + return dragElement; + }); let hideSource = false; if (options) { @@ -177,63 +239,80 @@ export namespace DragManager { hideSource = options.hideSource(); } } - const wasHidden = ele.hidden; - if (hideSource) { - ele.hidden = true; - } + eles.map(ele => (ele.hidden = hideSource)); + + let lastX = downX; + let lastY = downY; const moveHandler = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - x += e.movementX; - y += e.movementY; - if (dragData instanceof DocumentDragData) + if (dragData instanceof DocumentDragData) { dragData.aliasOnDrop = e.ctrlKey || e.altKey; + } if (e.shiftKey) { abortDrag(); - CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); + CollectionDockingView.Instance.StartOtherDrag(docs, { + pageX: e.pageX, + pageY: e.pageY, + preventDefault: emptyFunction, + button: 0 + }); } - dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; + //TODO: Why can't we use e.movementX and e.movementY? + let moveX = e.pageX - lastX; + let moveY = e.pageY - lastY; + lastX = e.pageX; + lastY = e.pageY; + dragElements.map((dragElement, i) => (dragElement.style.transform = + `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px) + scale(${scaleXs[i]}, ${scaleYs[i]})`) + ); }; const abortDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - dragDiv.removeChild(dragElement); - ele.hidden = false; - } + dragElements.map(dragElement => dragDiv.removeChild(dragElement)); + eles.map(ele => (ele.hidden = false)); + }; const upHandler = (e: PointerEvent) => { abortDrag(); - FinishDrag(ele, e, dragData, options, finishDrag); + FinishDrag(eles, e, dragData, options, finishDrag); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { - let parent = dragEle.parentElement; - if (parent) - parent.removeChild(dragEle); + function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { + let removed = dragEles.map(dragEle => { + let parent = dragEle.parentElement; + if (parent) parent.removeChild(dragEle); + return [dragEle, parent]; + }); const target = document.elementFromPoint(e.x, e.y); - if (parent) - parent.appendChild(dragEle); - if (!target) { - return; - } - if (finishDrag) - finishDrag(dragData); - - target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", { - bubbles: true, - detail: { - x: e.x, - y: e.y, - data: dragData - } - })); + removed.map(r => { + let dragEle = r[0]; + let parent = r[1]; + if (parent && dragEle) parent.appendChild(dragEle); + }); + if (target) { + if (finishDrag) finishDrag(dragData); - if (options) { - options.handlers.dragComplete({}); + target.dispatchEvent( + new CustomEvent<DropEvent>("dashOnDrop", { + bubbles: true, + detail: { + x: e.x, + y: e.y, + data: dragData + } + }) + ); + + if (options) { + options.handlers.dragComplete({}); + } } DocumentDecorations.Instance.Hidden = false; } -}
\ No newline at end of file +} diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 2a3c1da6e..92944bec0 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,13 +1,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model" -import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands' -import { redo, undo } from 'prosemirror-history' -import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list' +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model"; +import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands'; +import { redo, undo } from 'prosemirror-history'; +import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list'; import { EditorState, Transaction, NodeSelection, } from "prosemirror-state"; import { EditorView, } from "prosemirror-view"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], - preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0] + preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; // :: Object @@ -24,7 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = { content: "inline*", group: "block", parseDOM: [{ tag: "p" }], - toDOM() { return pDOM } + toDOM() { return pDOM; } }, // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. @@ -33,14 +33,14 @@ export const nodes: { [index: string]: NodeSpec } = { group: "block", defining: true, parseDOM: [{ tag: "blockquote" }], - toDOM() { return blockquoteDOM } + toDOM() { return blockquoteDOM; } }, // :: NodeSpec A horizontal rule (`<hr>`). horizontal_rule: { group: "block", parseDOM: [{ tag: "hr" }], - toDOM() { return hrDOM } + toDOM() { return hrDOM; } }, // :: NodeSpec A heading textblock, with a `level` attribute that @@ -57,7 +57,7 @@ export const nodes: { [index: string]: NodeSpec } = { { tag: "h4", attrs: { level: 4 } }, { tag: "h5", attrs: { level: 5 } }, { tag: "h6", attrs: { level: 6 } }], - toDOM(node: any) { return ["h" + node.attrs.level, 0] } + toDOM(node: any) { return ["h" + node.attrs.level, 0]; } }, // :: NodeSpec A code listing. Disallows marks or non-text inline @@ -70,7 +70,7 @@ export const nodes: { [index: string]: NodeSpec } = { code: true, defining: true, parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], - toDOM() { return preDOM } + toDOM() { return preDOM; } }, // :: NodeSpec The text node. @@ -96,10 +96,10 @@ export const nodes: { [index: string]: NodeSpec } = { src: dom.getAttribute("src"), title: dom.getAttribute("title"), alt: dom.getAttribute("alt") - } + }; } }], - toDOM(node: any) { return ["img", node.attrs] } + toDOM(node: any) { return ["img", node.attrs]; } }, // :: NodeSpec A hard line break, represented in the DOM as `<br>`. @@ -108,7 +108,7 @@ export const nodes: { [index: string]: NodeSpec } = { group: "inline", selectable: false, parseDOM: [{ tag: "br" }], - toDOM() { return brDOM } + toDOM() { return brDOM; } }, ordered_list: { @@ -136,7 +136,7 @@ export const nodes: { [index: string]: NodeSpec } = { ...listItem, content: 'paragraph block*' } -} +}; const emDOM: DOMOutputSpecArray = ["em", 0]; const strongDOM: DOMOutputSpecArray = ["strong", 0]; @@ -156,17 +156,17 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), title: dom.getAttribute("title") } + return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; } }], - toDOM(node: any) { return ["a", node.attrs, 0] } + toDOM(node: any) { return ["a", node.attrs, 0]; } }, // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. // Has parse rules that also match `<i>` and `font-style: italic`. em: { parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], - toDOM() { return emDOM } + toDOM() { return emDOM; } }, // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules @@ -175,7 +175,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [{ tag: "strong" }, { tag: "b" }, { style: "font-weight" }], - toDOM() { return strongDOM } + toDOM() { return strongDOM; } }, underline: { @@ -221,9 +221,9 @@ export const marks: { [index: string]: MarkSpec } = { // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { parseDOM: [{ tag: "code" }], - toDOM() { return codeDOM } + toDOM() { return codeDOM; } } -} +}; // :: Schema // This schema rougly corresponds to the document schema used by @@ -233,4 +233,4 @@ export const marks: { [index: string]: MarkSpec } = { // // To reuse elements from this schema, extend or read from its // `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). -export const schema = new Schema({ nodes, marks })
\ No newline at end of file +export const schema = new Schema({ nodes, marks });
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 4e97b9401..c67cc067a 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -14,45 +14,68 @@ import { ListField } from "../../fields/ListField"; // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' // @ts-ignore -import * as typescriptlib from '!!raw-loader!./type_decls.d' +import * as typescriptlib from '!!raw-loader!./type_decls.d'; import { Documents } from "../documents/Documents"; import { Key } from "../../fields/Key"; +export interface ScriptSucccess { + success: true; + result: any; +} + +export interface ScriptError { + success: false; + error: any; +} -export interface ExecutableScript { - (): any; +export type ScriptResult = ScriptSucccess | ScriptError; + +export interface CompiledScript { + readonly compiled: true; + readonly originalScript: string; + readonly options: Readonly<ScriptOptions>; + run(args?: { [name: string]: any }): ScriptResult; +} - compiled: boolean; +export interface CompileError { + compiled: false; + errors: any[]; } -function Compile(script: string | undefined, diagnostics: Opt<any[]>, scope: { [name: string]: any }): ExecutableScript { - const compiled = !(diagnostics && diagnostics.some(diag => diag.category == ts.DiagnosticCategory.Error)); +export type CompileResult = CompiledScript | CompileError; - let func: () => Opt<Field>; - if (compiled && script) { - let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key]; - let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)]; - let params: any[] = [KeyStore, Documents, ...fieldTypes] - for (let prop in scope) { - if (prop === "this") { +function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { + const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); + if (errors || !script) { + return { compiled: false, errors: diagnostics }; + } + + let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key]; + let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)]; + let params: any[] = [KeyStore, Documents, ...fieldTypes]; + let compiledFunction = new Function(...paramNames, `return ${script}`); + let { capturedVariables = {} } = options; + let run = (args: { [name: string]: any } = {}): ScriptResult => { + let argsArray: any[] = []; + for (let name of customParams) { + if (name === "this") { continue; } - paramNames.push(prop); - params.push(scope[prop]); + if (name in args) { + argsArray.push(args[name]); + } else { + argsArray.push(capturedVariables[name]); + } } - let thisParam = scope["this"]; - let compiledFunction = new Function(...paramNames, script); - func = function (): Opt<Field> { - return compiledFunction.apply(thisParam, params) - }; - } else { - func = () => undefined; - } - - return Object.assign(func, - { - compiled - }); + let thisParam = args.this || capturedVariables.this; + try { + const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); + return { success: true, result }; + } catch (error) { + return { success: false, error }; + } + }; + return { compiled: true, run, originalScript, options }; } interface File { @@ -74,14 +97,14 @@ class ScriptingCompilerHost { } // getDefaultLibFileName(options: ts.CompilerOptions): string { getDefaultLibFileName(options: any): string { - return 'node_modules/typescript/lib/lib.d.ts' // No idea what this means... + return 'node_modules/typescript/lib/lib.d.ts'; // No idea what this means... } writeFile(fileName: string, content: string) { const file = this.files.find(file => file.fileName === fileName); if (file) { file.content = content; } else { - this.files.push({ fileName, content }) + this.files.push({ fileName, content }); } } getCurrentDirectory(): string { @@ -108,26 +131,56 @@ class ScriptingCompilerHost { } } -export function CompileScript(script: string, scope?: { [name: string]: any }, addReturn: boolean = false): ExecutableScript { +export interface ScriptOptions { + requiredType?: string; + addReturn?: boolean; + params?: { [name: string]: string }; + capturedVariables?: { [name: string]: Field }; +} + +export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { + const { requiredType = "", addReturn = false, params = {}, capturedVariables = {} } = options; let host = new ScriptingCompilerHost; - let funcScript = `(function() { + let paramNames: string[] = []; + if ("this" in params || "this" in capturedVariables) { + paramNames.push("this"); + } + for (const key in params) { + if (key === "this") continue; + paramNames.push(key); + } + let paramList = paramNames.map(key => { + const val = params[key]; + return `${key}: ${val}`; + }); + for (const key in capturedVariables) { + if (key === "this") continue; + paramNames.push(key); + paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); + } + let paramString = paramList.join(", "); + let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} { ${addReturn ? `return ${script};` : script} - }).apply(this)` + })`; host.writeFile("file.ts", funcScript); host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); let program = ts.createProgram(["file.ts"], {}, host); let testResult = program.emit(); - let outputText = "return " + host.readFile("file.js"); + let outputText = host.readFile("file.js"); let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return Compile(outputText, diagnostics, scope || {}); + return Run(outputText, paramNames, diagnostics, script, options); +} + +export function OrLiteralType(returnType: string): string { + return `${returnType} | string | number`; } export function ToField(data: any): Opt<Field> { - if (typeof data == "string") { + if (typeof data === "string") { return new TextField(data); - } else if (typeof data == "number") { + } else if (typeof data === "number") { return new NumberField(data); } return undefined; diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx index b6b088170..a209874a3 100644 --- a/src/client/util/ScrollBox.tsx +++ b/src/client/util/ScrollBox.tsx @@ -1,4 +1,4 @@ -import React = require("react") +import React = require("react"); export class ScrollBox extends React.Component { onWheel = (e: React.WheelEvent) => { @@ -16,6 +16,6 @@ export class ScrollBox extends React.Component { }} onWheel={this.onWheel}> {this.props.children} </div> - ) + ); } }
\ No newline at end of file diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 1a711ae64..c56f6a4ff 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,5 +1,8 @@ import { observable, action } from "mobx"; import { DocumentView } from "../views/nodes/DocumentView"; +import { Document } from "../../fields/Document"; +import { Main } from "../views/Main"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; export namespace SelectionManager { class Manager { @@ -10,30 +13,46 @@ export namespace SelectionManager { SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it if (!ctrlPressed) { - manager.SelectedDocuments = []; + this.DeselectAll(); } if (manager.SelectedDocuments.indexOf(doc) === -1) { - manager.SelectedDocuments.push(doc) + manager.SelectedDocuments.push(doc); + doc.props.onActiveChanged(true); } } + + @action + DeselectAll(): void { + manager.SelectedDocuments.map(dv => dv.props.onActiveChanged(false)); + manager.SelectedDocuments = []; + MainOverlayTextBox.Instance.SetTextDoc(); + } } - const manager = new Manager; + const manager = new Manager(); export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { - manager.SelectDoc(doc, ctrlPressed) + manager.SelectDoc(doc, ctrlPressed); } export function IsSelected(doc: DocumentView): boolean { return manager.SelectedDocuments.indexOf(doc) !== -1; } - export function DeselectAll(): void { - manager.SelectedDocuments = [] + export function DeselectAll(except?: Document): void { + let found: DocumentView | undefined = undefined; + if (except) { + for (const view of manager.SelectedDocuments) { + if (view.props.Document === except) found = view; + } + } + + manager.DeselectAll(); + if (found) manager.SelectDoc(found, false); } export function SelectedDocuments(): Array<DocumentView> { return manager.SelectedDocuments; } -}
\ No newline at end of file +} diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index ea580d104..7deea3be6 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -1,4 +1,4 @@ -@import "../views/global_variables"; +@import "../views/globalCssVariables"; .tooltipMenu { position: absolute; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 2a613ba8b..7951e5686 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -5,135 +5,137 @@ import { keymap } from "prosemirror-keymap"; import { EditorState, Transaction, NodeSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType } from "prosemirror-model" -import React = require("react") +import { Schema, NodeType } from "prosemirror-model"; +import React = require("react"); import "./TooltipTextMenu.scss"; const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); -import { library } from '@fortawesome/fontawesome-svg-core' -import { wrapInList, bulletList } from 'prosemirror-schema-list' -import { - faListUl, -} from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { wrapInList, bulletList } from 'prosemirror-schema-list'; +import { faListUl } from '@fortawesome/free-solid-svg-icons'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { - private tooltip: HTMLElement; - - constructor(view: EditorView) { - this.tooltip = document.createElement("div"); - this.tooltip.className = "tooltipMenu"; - - //add the div which is the tooltip - view.dom.parentNode!.appendChild(this.tooltip); - - //add additional icons - library.add(faListUl); - - //add the buttons to the tooltip - let items = [ - { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, - { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, - { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, - { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, - { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, - { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, - //this doesn't work currently - look into notion of active block - { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, - ] - items.forEach(({ dom }) => this.tooltip.appendChild(dom)); - - //pointer down handler to activate button effects - this.tooltip.addEventListener("pointerdown", e => { - e.preventDefault(); - view.focus(); - items.forEach(({ command, dom }) => { - if (dom.contains(e.srcElement)) { - let active = command(view.state, view.dispatch, view); - //uncomment this if we want the bullet button to disappear if current selection is bulleted - // dom.style.display = active ? "" : "none" + private tooltip: HTMLElement; + private editorProps: FieldViewProps; + + constructor(view: EditorView, editorProps: FieldViewProps) { + this.editorProps = editorProps; + this.tooltip = document.createElement("div"); + this.tooltip.className = "tooltipMenu"; + + //add the div which is the tooltip + view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + + //add additional icons + library.add(faListUl); + + //add the buttons to the tooltip + let items = [ + { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, + { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, + { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, + { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, + { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, + { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, + //this doesn't work currently - look into notion of active block + { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, + ]; + items.forEach(({ dom }) => this.tooltip.appendChild(dom)); + + //pointer down handler to activate button effects + this.tooltip.addEventListener("pointerdown", e => { + e.preventDefault(); + view.focus(); + items.forEach(({ command, dom }) => { + if (dom.contains(e.target as Node)) { + let active = command(view.state, view.dispatch, view); + //uncomment this if we want the bullet button to disappear if current selection is bulleted + // dom.style.display = active ? "" : "none" + } + }); + }); + + this.update(view, undefined); + } + + // Helper function to create menu icons + icon(text: string, name: string) { + let span = document.createElement("span"); + span.className = "menuicon " + name; + span.title = name; + span.textContent = text; + return span; + } + + //adapted this method - use it to check if block has a tag (ie bulleting) + blockActive(type: NodeType<Schema<string, string>>, state: EditorState) { + let attrs = {}; + + if (state.selection instanceof NodeSelection) { + const sel: NodeSelection = state.selection; + let $from = sel.$from; + let to = sel.to; + let node = sel.node; + + if (node) { + return node.hasMarkup(type, attrs); + } + + return to <= $from.end() && $from.parent.hasMarkup(type, attrs); } - }) - }) - - this.update(view, undefined); - } - - // Helper function to create menu icons - icon(text: string, name: string) { - let span = document.createElement("span"); - span.className = "menuicon " + name; - span.title = name; - span.textContent = text; - return span; - } - - //adapted this method - use it to check if block has a tag (ie bulleting) - blockActive(type: NodeType<Schema<string, string>>, state: EditorState) { - let attrs = {}; - - if (state.selection instanceof NodeSelection) { - const sel: NodeSelection = state.selection; - let $from = sel.$from; - let to = sel.to; - let node = sel.node; - - if (node) { - return node.hasMarkup(type, attrs); - } - - return to <= $from.end() && $from.parent.hasMarkup(type, attrs); } - } - - //this doesn't currently work but could be used to use icons for buttons - unorderedListIcon(): HTMLSpanElement { - let span = document.createElement("span"); - let icon = document.createElement("FontAwesomeIcon"); - icon.className = "menuicon fa fa-smile-o"; - span.appendChild(icon); - return span; - } - - // Create an icon for a heading at the given level - heading(level: number) { - return { - command: setBlockType(schema.nodes.heading, { level }), - dom: this.icon("H" + level, "heading") + + //this doesn't currently work but could be used to use icons for buttons + unorderedListIcon(): HTMLSpanElement { + let span = document.createElement("span"); + let icon = document.createElement("FontAwesomeIcon"); + icon.className = "menuicon fa fa-smile-o"; + span.appendChild(icon); + return span; + } + + // Create an icon for a heading at the given level + heading(level: number) { + return { + command: setBlockType(schema.nodes.heading, { level }), + dom: this.icon("H" + level, "heading") + }; } - } - - //updates the tooltip menu when the selection changes - update(view: EditorView, lastState: EditorState | undefined) { - let state = view.state - // Don't do anything if the document/selection didn't change - if (lastState && lastState.doc.eq(state.doc) && - lastState.selection.eq(state.selection)) return - - // Hide the tooltip if the selection is empty - if (state.selection.empty) { - this.tooltip.style.display = "none" - return + + //updates the tooltip menu when the selection changes + update(view: EditorView, lastState: EditorState | undefined) { + let state = view.state; + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return; + + // Hide the tooltip if the selection is empty + if (state.selection.empty) { + this.tooltip.style.display = "none"; + return; + } + + // Otherwise, reposition it and update its content + this.tooltip.style.display = ""; + let { from, to } = state.selection; + let start = view.coordsAtPos(from), end = view.coordsAtPos(to); + // The box in which the tooltip is positioned, to use as base + let box = this.tooltip.offsetParent!.getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; + let mid = Math.min(start.left, end.left) + width; + + //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 + this.tooltip.style.width = 122 + "px"; + this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; } - // Otherwise, reposition it and update its content - this.tooltip.style.display = "" - let { from, to } = state.selection - let start = view.coordsAtPos(from), end = view.coordsAtPos(to) - // The box in which the tooltip is positioned, to use as base - let box = this.tooltip.offsetParent!.getBoundingClientRect() - // Find a center-ish x position from the selection endpoints (when - // crossing lines, end may be more to the left) - let left = Math.max((start.left + end.left) / 2, start.left + 3) - this.tooltip.style.left = (left - box.left) + "px" - let width = Math.abs(start.left - end.left) / 2; - let mid = Math.min(start.left, end.left) + width; - - //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 - this.tooltip.style.width = 122 + "px"; - this.tooltip.style.bottom = (box.bottom - start.top) + "px"; - } - - destroy() { this.tooltip.remove() } + destroy() { this.tooltip.remove(); } }
\ No newline at end of file diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts index 3e1039166..e9170ec36 100644 --- a/src/client/util/Transform.ts +++ b/src/client/util/Transform.ts @@ -3,7 +3,7 @@ export class Transform { private _translateY: number = 0; private _scale: number = 1; - static get Identity(): Transform { + static Identity(): Transform { return new Transform(0, 0, 1); } @@ -17,78 +17,64 @@ export class Transform { this._scale = scale; } - translate = (x: number, y: number): Transform => { + translate = (x: number, y: number): this => { this._translateX += x; this._translateY += y; return this; } - scale = (scale: number): Transform => { + scale = (scale: number): this => { this._scale *= scale; this._translateX *= scale; this._translateY *= scale; return this; } - scaleAbout = (scale: number, x: number, y: number): Transform => { + scaleAbout = (scale: number, x: number, y: number): this => { this._translateX += x * this._scale - x * this._scale * scale; this._translateY += y * this._scale - y * this._scale * scale; this._scale *= scale; return this; } - transform = (transform: Transform): Transform => { + transform = (transform: Transform): this => { this._translateX = transform._translateX + transform._scale * this._translateX; this._translateY = transform._translateY + transform._scale * this._translateY; this._scale *= transform._scale; return this; } - preTranslate = (x: number, y: number): Transform => { + preTranslate = (x: number, y: number): this => { this._translateX += this._scale * x; this._translateY += this._scale * y; return this; } - preScale = (scale: number): Transform => { + preScale = (scale: number): this => { this._scale *= scale; return this; } - preTransform = (transform: Transform): Transform => { + preTransform = (transform: Transform): this => { this._translateX += transform._translateX * this._scale; this._translateY += transform._translateY * this._scale; this._scale *= transform._scale; return this; } - translated = (x: number, y: number): Transform => { - return this.copy().translate(x, y); - } + translated = (x: number, y: number): Transform => this.copy().translate(x, y); - preTranslated = (x: number, y: number): Transform => { - return this.copy().preTranslate(x, y); - } + preTranslated = (x: number, y: number): Transform => this.copy().preTranslate(x, y); - scaled = (scale: number): Transform => { - return this.copy().scale(scale); - } + scaled = (scale: number): Transform => this.copy().scale(scale); - scaledAbout = (scale: number, x: number, y: number): Transform => { - return this.copy().scaleAbout(scale, x, y); - } + scaledAbout = (scale: number, x: number, y: number): Transform => this.copy().scaleAbout(scale, x, y); - preScaled = (scale: number): Transform => { - return this.copy().preScale(scale); - } + preScaled = (scale: number): Transform => this.copy().preScale(scale); - transformed = (transform: Transform): Transform => { - return this.copy().transform(transform); - } + transformed = (transform: Transform): Transform => this.copy().transform(transform); - preTransformed = (transform: Transform): Transform => { - return this.copy().preTransform(transform); - } + preTransformed = (transform: Transform): Transform => this.copy().preTransform(transform); transformPoint = (x: number, y: number): [number, number] => { x *= this._scale; @@ -98,9 +84,7 @@ export class Transform { return [x, y]; } - transformDirection = (x: number, y: number): [number, number] => { - return [x * this._scale, y * this._scale]; - } + transformDirection = (x: number, y: number): [number, number] => [x * this._scale, y * this._scale]; transformBounds(x: number, y: number, width: number, height: number): { x: number, y: number, width: number, height: number } { [x, y] = this.transformPoint(x, y); @@ -108,12 +92,8 @@ export class Transform { return { x, y, width, height }; } - inverse = () => { - return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale) - } + inverse = () => new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale); - copy = () => { - return new Transform(this._translateX, this._translateY, this._scale); - } + copy = () => new Transform(this._translateX, this._translateY, this._scale); }
\ No newline at end of file diff --git a/src/client/util/TypedEvent.ts b/src/client/util/TypedEvent.ts index 0714a7f5c..532ba78eb 100644 --- a/src/client/util/TypedEvent.ts +++ b/src/client/util/TypedEvent.ts @@ -36,7 +36,5 @@ export class TypedEvent<T> { this.listenersOncer = []; } - pipe = (te: TypedEvent<T>): Disposable => { - return this.on((e) => te.emit(e)); - } + pipe = (te: TypedEvent<T>): Disposable => this.on((e) => te.emit(e)); }
\ No newline at end of file diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 6d1b2f1b8..27aed4bac 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,6 +1,7 @@ import { observable, action } from "mobx"; -import 'source-map-support/register' +import 'source-map-support/register'; import { Without } from "../../Utils"; +import { string } from "prop-types"; function getBatchName(target: any, key: string | symbol): string { let keyName = key.toString(); @@ -30,11 +31,24 @@ function propertyDecorator(target: any, key: string | symbol) { batch.end(); } } - }) + }); } - }) + }); } -export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { + +export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any; +export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; +export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { + if (!key) { + return function () { + let batch = UndoManager.StartBatch(""); + try { + return target.apply(undefined, arguments); + } finally { + batch.end(); + } + }; + } if (!descriptor) { propertyDecorator(target, key); return; @@ -44,11 +58,11 @@ export function undoBatch(target: any, key: string | symbol, descriptor?: TypedP descriptor.value = function (...args: any[]) { let batch = UndoManager.StartBatch(getBatchName(target, key)); try { - return oldFunction.apply(this, args) + return oldFunction.apply(this, args); } finally { batch.end(); } - } + }; return descriptor; } @@ -84,6 +98,9 @@ export namespace UndoManager { export function GetOpenBatches(): Without<Batch, 'end'>[] { return openBatches; } + export function TraceOpenBatches() { + console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join("\n\t")}\n`); + } export class Batch { private disposed: boolean = false; @@ -100,8 +117,8 @@ export namespace UndoManager { EndBatch(cancel); } - end = () => { this.dispose(false); } - cancel = () => { this.dispose(true); } + end = () => { this.dispose(false); }; + cancel = () => { this.dispose(true); }; } export function StartBatch(batchName: string): Batch { @@ -121,12 +138,15 @@ export namespace UndoManager { redoStack.length = 0; currentBatch = undefined; } - }) + }); export function RunInBatch(fn: () => void, batchName: string) { let batch = StartBatch(batchName); - fn(); - batch.end(); + try { + fn(); + } finally { + batch.end(); + } } export const Undo = action(() => { @@ -146,7 +166,7 @@ export namespace UndoManager { undoing = false; redoStack.push(commands); - }) + }); export const Redo = action(() => { if (redoStack.length === 0) { @@ -159,12 +179,12 @@ export namespace UndoManager { } undoing = true; - for (let i = 0; i < commands.length; i++) { - commands[i].redo(); + for (const command of commands) { + command.redo(); } undoing = false; undoStack.push(commands); - }) + }); }
\ No newline at end of file diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 4f69053b1..47c3481b2 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -181,7 +181,7 @@ declare class Key extends Field { Copy(): Field; ToScriptString(): string; } -declare type FIELD_WAITING = "<Waiting>"; +declare type FIELD_WAITING = null; declare type Opt<T> = T | undefined; declare type FieldValue<T> = Opt<T> | FIELD_WAITING; // @ts-ignore |
