aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
authorEleanor Eng <eleanor_eng@brown.edu>2019-05-04 16:44:22 -0400
committerEleanor Eng <eleanor_eng@brown.edu>2019-05-04 16:44:40 -0400
commitcecd09917f23fc83c87cffc4fddf5fe1f8331bac (patch)
treeab7144d36046aaada287c2ce14d2fe7722585dce /src/client/util
parent0bf0b028b1af8b9481e369c754277af5fb8b3fcd (diff)
parent1ccabe155cb4f23c0aa7e37f91cd4a303008b8c7 (diff)
merge with master
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DocumentManager.ts44
-rw-r--r--src/client/util/DragManager.ts113
-rw-r--r--src/client/util/Scripting.ts34
-rw-r--r--src/client/util/SelectionManager.ts10
-rw-r--r--src/client/util/SerializationHelper.ts130
-rw-r--r--src/client/util/TooltipTextMenu.tsx85
-rw-r--r--src/client/util/UndoManager.ts7
7 files changed, 304 insertions, 119 deletions
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 56669fb79..69964e2c9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,9 +1,8 @@
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';
+import { Doc } from '../../new_fields/Doc';
+import { FieldValue, Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
export class DocumentManager {
@@ -25,26 +24,21 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentView(toFind: Document): DocumentView | null {
+ public getDocumentView(toFind: Doc): DocumentView | null {
- let toReturn: DocumentView | null;
- toReturn = null;
+ let toReturn: DocumentView | null = null;
//gets document view that is in a freeform canvas collection
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- if (doc === toFind) {
+ if (view.props.Document === toFind) {
toReturn = view;
return;
}
});
if (!toReturn) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- let docSrc = doc.GetT(KeyStore.Prototype, Document);
- if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) {
+ let doc = view.props.Document.proto;
+ if (doc && Object.is(doc, toFind)) {
toReturn = view;
}
});
@@ -52,7 +46,7 @@ export class DocumentManager {
return toReturn;
}
- public getDocumentViews(toFind: Document): DocumentView[] {
+ public getDocumentViews(toFind: Doc): DocumentView[] {
let toReturn: DocumentView[] = [];
@@ -64,8 +58,8 @@ export class DocumentManager {
if (doc === toFind) {
toReturn.push(view);
} else {
- let docSrc = doc.GetT(KeyStore.Prototype, Document);
- if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) {
+ let docSrc = FieldValue(doc.proto);
+ if (docSrc && Object.is(docSrc, toFind)) {
toReturn.push(view);
}
}
@@ -77,20 +71,20 @@ export class DocumentManager {
@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) {
- pairs.push(...linksList.Data.reduce((pairs, link) => {
- if (link instanceof Document) {
- let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
- if (linkToDoc && linkToDoc !== FieldWaiting) {
+ let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc));
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
+ if (linkToDoc) {
DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
pairs.push({ a: dv, b: docView1, l: link }));
}
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]));
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]);
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
}
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 46658867b..a3dbe6e43 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,14 +1,14 @@
import { action } from "mobx";
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from "../../fields/Field";
-import { KeyStore } from "../../fields/KeyStore";
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";
+import { Doc } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
+import { listSpec } from "../../new_fields/Schema";
-export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) {
+export type dropActionType = "alias" | "copy" | undefined;
+export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
let onRowMove = action((e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
@@ -16,7 +16,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
var dragData = new DragManager.DocumentDragData([docFunc()]);
- dragData.copyOnDrop = copyOnDrop;
+ dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
});
@@ -40,19 +40,19 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
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) : [];
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+ let srcTarg = sourceDoc.proto;
+ let draggedDocs = srcTarg ?
+ Cast(srcTarg.linkedToDocs, listSpec(Doc), []).map(linkDoc =>
+ Cast(linkDoc.linkedTo, Doc) as Doc) : [];
+ let draggedFromDocs = srcTarg ?
+ Cast(srcTarg.linkedFromDocs, listSpec(Doc), []).map(linkDoc =>
+ Cast(linkDoc.linkedFrom, Doc) as Doc) : [];
draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
- let moddrag = [] as Document[];
+ let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
- let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document);
+ let doc = await Cast(draggedDoc.annotationOn, Doc);
if (doc) moddrag.push(doc);
}
let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
@@ -134,35 +134,42 @@ export namespace DragManager {
};
}
- export type MoveFunction = (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Document[]) {
+ constructor(dragDoc: Doc[]) {
this.draggedDocuments = dragDoc;
this.droppedDocuments = dragDoc;
this.xOffset = 0;
this.yOffset = 0;
}
- draggedDocuments: Document[];
- droppedDocuments: Document[];
+ draggedDocuments: Doc[];
+ droppedDocuments: Doc[];
xOffset: number;
yOffset: number;
- aliasOnDrop?: boolean;
- copyOnDrop?: boolean;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
moveDocument?: MoveFunction;
[id: string]: any;
}
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));
+ (dropData: { [id: string]: any }) =>
+ (dropData.droppedDocuments = dragData.userDropAction == "alias" || (!dragData.userDropAction && dragData.dropAction == "alias") ?
+ dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
+ dragData.userDropAction == "copy" || (!dragData.userDropAction && dragData.dropAction == "copy") ?
+ dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
+ dragData.draggedDocuments));
}
export class LinkDragData {
- constructor(linkSourceDoc: Document) {
+ constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) {
this.linkSourceDocument = linkSourceDoc;
+ this.blacklist = blacklist;
}
- droppedDocuments: Document[] = [];
- linkSourceDocument: Document;
+ droppedDocuments: Doc[] = [];
+ linkSourceDocument: Doc;
+ blacklist: Doc[];
[id: string]: any;
}
@@ -170,10 +177,13 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export let AbortDrag: () => void = emptyFunction;
+
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.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
MainOverlayTextBox.Instance.SetTextDoc();
@@ -183,7 +193,7 @@ export namespace DragManager {
let xs: number[] = [];
let ys: number[] = [];
- const docs: Document[] =
+ const docs: Doc[] =
dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
@@ -247,7 +257,7 @@ export namespace DragManager {
e.stopPropagation();
e.preventDefault();
if (dragData instanceof DocumentDragData) {
- dragData.aliasOnDrop = e.ctrlKey || e.altKey;
+ dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
if (e.shiftKey) {
AbortDrag();
@@ -269,33 +279,47 @@ export namespace DragManager {
);
};
- AbortDrag = () => {
+ let hideDragElements = () => {
+ dragElements.map(dragElement => dragElement.parentNode == dragDiv && dragDiv.removeChild(dragElement));
+ eles.map(ele => (ele.hidden = false));
+ };
+ let endDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
- dragElements.map(dragElement => { if (dragElement.parentNode == dragDiv) dragDiv.removeChild(dragElement); });
- eles.map(ele => (ele.hidden = false));
+ if (options) {
+ options.handlers.dragComplete({});
+ }
+ }
+
+ AbortDrag = () => {
+ hideDragElements();
+ endDrag();
};
const upHandler = (e: PointerEvent) => {
- AbortDrag();
- FinishDrag(eles, e, dragData, options, finishDrag);
+ hideDragElements();
+ dispatchDrag(eles, e, dragData, options, finishDrag);
+ endDrag();
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
}
- export let AbortDrag: () => void = emptyFunction;
-
- function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
+ function dispatchDrag(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];
+ // let parent = dragEle.parentElement;
+ // if (parent) parent.removeChild(dragEle);
+ let ret = [dragEle, dragEle.style.width, dragEle.style.height];
+ dragEle.style.width = "0";
+ dragEle.style.height = "0";
+ return ret;
});
const target = document.elementFromPoint(e.x, e.y);
removed.map(r => {
- let dragEle = r[0];
- let parent = r[1];
- if (parent && dragEle) parent.appendChild(dragEle);
+ let dragEle = r[0] as HTMLElement;
+ dragEle.style.width = r[1] as string;
+ dragEle.style.height = r[2] as string;
+ // let parent = r[1];
+ // if (parent && dragEle) parent.appendChild(dragEle);
});
if (target) {
if (finishDrag) finishDrag(dragData);
@@ -310,11 +334,6 @@ export namespace DragManager {
}
})
);
-
- if (options) {
- options.handlers.dragComplete({});
- }
}
- DocumentDecorations.Instance.Hidden = false;
}
}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c67cc067a..e45f61c11 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,13 +1,5 @@
// import * as ts from "typescript"
let ts = (window as any).ts;
-import { Opt, Field } from "../../fields/Field";
-import { Document } from "../../fields/Document";
-import { NumberField } from "../../fields/NumberField";
-import { ImageField } from "../../fields/ImageField";
-import { TextField } from "../../fields/TextField";
-import { RichTextField } from "../../fields/RichTextField";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
// // @ts-ignore
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// // @ts-ignore
@@ -15,8 +7,11 @@ import { ListField } from "../../fields/ListField";
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import { Documents } from "../documents/Documents";
-import { Key } from "../../fields/Key";
+import { Docs } from "../documents/Documents";
+import { Doc, Field } from '../../new_fields/Doc';
+import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
+import { List } from '../../new_fields/List';
+import { RichTextField } from '../../new_fields/RichTextField';
export interface ScriptSucccess {
success: true;
@@ -50,9 +45,9 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
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 fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField];
+ let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
+ let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
let run = (args: { [name: string]: any } = {}): ScriptResult => {
@@ -171,17 +166,4 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
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") {
- return new TextField(data);
- } else if (typeof data === "number") {
- return new NumberField(data);
- }
- return undefined;
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 92d78696e..fe5acf4b4 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,9 +1,7 @@
import { observable, action } from "mobx";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { Document } from "../../fields/Document";
-import { Main } from "../views/Main";
+import { Doc } from "../../new_fields/Doc";
import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
-import { DragManager } from "./DragManager";
+import { DocumentView } from "../views/nodes/DocumentView";
export namespace SelectionManager {
class Manager {
@@ -51,7 +49,7 @@ export namespace SelectionManager {
return manager.SelectedDocuments.indexOf(doc) !== -1;
}
- export function DeselectAll(except?: Document): void {
+ export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
for (const view of manager.SelectedDocuments) {
@@ -65,7 +63,7 @@ export namespace SelectionManager {
export function ReselectAll() {
let sdocs = manager.ReselectAll();
- manager.ReselectAll2(sdocs);
+ setTimeout(() => manager.ReselectAll2(sdocs), 0);
}
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
new file mode 100644
index 000000000..7ded85e43
--- /dev/null
+++ b/src/client/util/SerializationHelper.ts
@@ -0,0 +1,130 @@
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
+import { Field } from "../../new_fields/Doc";
+
+export namespace SerializationHelper {
+ let serializing: number = 0;
+ export function IsSerializing() {
+ return serializing > 0;
+ }
+
+ export function Serialize(obj: Field): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!(obj.constructor.name in reverseMap)) {
+ throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const json = serialize(obj);
+ json.__type = reverseMap[obj.constructor.name];
+ serializing -= 1;
+ return json;
+ }
+
+ export function Deserialize(obj: any): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!obj.__type) {
+ throw Error("No property 'type' found in JSON.");
+ }
+
+ if (!(obj.__type in serializationTypes)) {
+ throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const value = deserialize(serializationTypes[obj.__type], obj);
+ serializing -= 1;
+ return value;
+ }
+}
+
+let serializationTypes: { [name: string]: any } = {};
+let reverseMap: { [ctor: string]: string } = {};
+
+export interface DeserializableOpts {
+ (constructor: { new(...args: any[]): any }): void;
+ withFields(fields: string[]): Function;
+}
+
+export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(constructor: { new(...args: any[]): any }): void;
+export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+ function addToMap(name: string, ctor: { new(...args: any[]): any }) {
+ const schema = getDefaultModelSchema(ctor) as any;
+ if (schema.targetClass !== ctor) {
+ const newSchema = { ...schema, factory: () => new ctor() };
+ setDefaultModelSchema(ctor, newSchema);
+ }
+ if (!(name in serializationTypes)) {
+ serializationTypes[name] = ctor;
+ reverseMap[ctor.name] = name;
+ } else {
+ throw new Error(`Name ${name} has already been registered as deserializable`);
+ }
+ }
+ if (typeof constructor === "string") {
+ return Object.assign((ctor: { new(...args: any[]): any }) => {
+ addToMap(constructor, ctor);
+ }, { withFields: Deserializable.withFields });
+ }
+ addToMap(constructor.name, constructor);
+}
+
+export namespace Deserializable {
+ export function withFields(fields: string[]) {
+ return function (constructor: { new(...fields: any[]): any }) {
+ Deserializable(constructor);
+ let schema = getDefaultModelSchema(constructor);
+ if (schema) {
+ schema.factory = context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ };
+ // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
+ // fields.forEach(field => {
+ // if (field in schema.props) {
+ // let propSchema = schema.props[field];
+ // if (propSchema === false) {
+ // return;
+ // } else if (propSchema === true) {
+ // propSchema = primitive();
+ // }
+ // schema.props[field] = custom(propSchema.serializer,
+ // () => {
+ // return SKIP;
+ // });
+ // }
+ // });
+ } else {
+ schema = {
+ props: {},
+ factory: context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ }
+ };
+ setDefaultModelSchema(constructor, schema);
+ }
+ };
+ }
+}
+
+export function autoObject(): PropSchema {
+ return custom(
+ (s) => SerializationHelper.Serialize(s),
+ (s) => SerializationHelper.Deserialize(s)
+ );
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 4f0eb7d63..68a73375e 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -11,7 +11,8 @@ import React = require("react");
import "./TooltipTextMenu.scss";
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
+import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
+import { liftTarget } from 'prosemirror-transform';
import {
faListUl,
} from '@fortawesome/free-solid-svg-icons';
@@ -24,19 +25,22 @@ const SVG = "http://www.w3.org/2000/svg";
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
- private tooltip: HTMLElement;
+ public tooltip: HTMLElement;
private num_icons = 0;
private view: EditorView;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
+ private listTypes: NodeType[];
private editorProps: FieldViewProps;
private state: EditorState;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
+ private listTypeToIcon: Map<NodeType, string>;
private fontSizeIndicator: HTMLSpanElement = document.createElement("span");
//dropdown doms
- private fontSizeDom: Node;
- private fontStyleDom: Node;
+ private fontSizeDom?: Node;
+ private fontStyleDom?: Node;
+ private listTypeBtnDom?: Node;
constructor(view: EditorView, editorProps: FieldViewProps) {
this.view = view;
@@ -58,8 +62,9 @@ export class TooltipTextMenu {
{ 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") },
- { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
- { command: lift, dom: this.icon("<", "lift") },
+ // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
+ // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") },
+ // { command: lift, dom: this.icon("<", "lift") },
];
//add menu items
items.forEach(({ dom, command }) => {
@@ -96,7 +101,11 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p72, 72);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
- //this.addFontDropdowns();
+ //list types
+ this.listTypeToIcon = new Map();
+ this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
+ this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
+ this.listTypes = Array.from(this.listTypeToIcon.keys());
this.update(view, undefined);
}
@@ -109,7 +118,7 @@ export class TooltipTextMenu {
//font SIZES
let fontSizeBtns: MenuItem[] = [];
this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
});
if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); }
@@ -128,7 +137,7 @@ export class TooltipTextMenu {
//font STYLES
let fontBtns: MenuItem[] = [];
this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
});
if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); }
@@ -140,6 +149,29 @@ export class TooltipTextMenu {
this.tooltip.appendChild(this.fontStyleDom);
}
+ //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown
+ updateListItemDropdown(label: string, listTypeBtn: Node) {
+ //remove old btn
+ if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
+
+ //Make a dropdown of all list types
+ let toAdd: MenuItem[] = [];
+ this.listTypeToIcon.forEach((icon, type) => {
+ toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
+ });
+ //option to remove the list formatting
+ toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType));
+
+ listTypeBtn = (new Dropdown(toAdd, {
+ label: label,
+ css: "color:white; width: 40px;"
+ }) as MenuItem).render(this.view).dom;
+
+ //add this new button and return it
+ this.tooltip.appendChild(listTypeBtn);
+ return listTypeBtn;
+ }
+
//for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) {
let { empty, $cursor, ranges } = view.state.selection as TextSelection;
@@ -171,9 +203,18 @@ export class TooltipTextMenu {
return toggleMark(markType)(view.state, view.dispatch, view);
}
- //makes a button for the drop down
+ //remove all node typeand apply the passed-in one to the selected text
+ changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) {
+ //remove old
+ liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
+ if (nodeType) { //add new
+ wrapInList(nodeType)(view.state, view.dispatch);
+ }
+ }
+
+ //makes a button for the drop down FOR MARKS
//css is the style you want applied to the button
- dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+ dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
return new MenuItem({
title: "",
label: label,
@@ -186,6 +227,23 @@ export class TooltipTextMenu {
}
});
}
+
+ //makes a button for the drop down FOR NODE TYPES
+ //css is the style you want applied to the button
+ dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) {
+ return new MenuItem({
+ title: "",
+ label: label,
+ execEvent: "",
+ class: "menuicon",
+ css: css,
+ enable(state) { return true; },
+ run() {
+ changeToNodeInGroup(nodeType, view, groupNodes);
+ }
+ });
+ }
+
// Helper function to create menu icons
icon(text: string, name: string) {
let span = document.createElement("span");
@@ -262,6 +320,9 @@ export class TooltipTextMenu {
this.tooltip.style.width = 225 + "px";
this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
+ //UPDATE LIST ITEM DROPDOWN
+ this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!);
+
//UPDATE FONT STYLE DROPDOWN
let activeStyles = this.activeMarksOnSelection(this.fontStyles);
if (activeStyles.length === 1) {
@@ -288,7 +349,7 @@ export class TooltipTextMenu {
}
}
- //finds all active marks on selection
+ //finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
let { empty, $cursor, ranges } = this.view.state.selection as TextSelection;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 27aed4bac..0b5280c4a 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,4 +1,4 @@
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import 'source-map-support/register';
import { Without } from "../../Utils";
import { string } from "prop-types";
@@ -140,10 +140,11 @@ export namespace UndoManager {
}
});
- export function RunInBatch(fn: () => void, batchName: string) {
+ //TODO Make this return the return value
+ export function RunInBatch<T>(fn: () => T, batchName: string) {
let batch = StartBatch(batchName);
try {
- fn();
+ return runInAction(fn);
} finally {
batch.end();
}