aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DocumentManager.ts128
-rw-r--r--src/client/util/DragManager.ts344
-rw-r--r--src/client/util/History.ts122
-rw-r--r--src/client/util/ProsemirrorKeymap.ts100
-rw-r--r--src/client/util/RichTextRules.ts43
-rw-r--r--src/client/util/RichTextSchema.tsx589
-rw-r--r--src/client/util/Scripting.ts141
-rw-r--r--src/client/util/ScrollBox.tsx4
-rw-r--r--src/client/util/SearchUtil.ts25
-rw-r--r--src/client/util/SelectionManager.ts63
-rw-r--r--src/client/util/SerializationHelper.ts130
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx86
-rw-r--r--src/client/util/TooltipTextMenu.scss258
-rw-r--r--src/client/util/TooltipTextMenu.tsx575
-rw-r--r--src/client/util/Transform.ts56
-rw-r--r--src/client/util/TypedEvent.ts4
-rw-r--r--src/client/util/UndoManager.ts105
-rw-r--r--src/client/util/jsx-decl.d.ts1
-rw-r--r--src/client/util/type_decls.d12
19 files changed, 2228 insertions, 558 deletions
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5b99b4ef8..d06af6dd5 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,14 @@
-import React = require('react')
-import { observer } from 'mobx-react';
-import { observable, action } from 'mobx';
-import { Document } from "../../fields/Document"
+import { computed, observable } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
+import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
+import { undoBatch } from './UndoManager';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { Id } from '../../new_fields/RefField';
+import { CollectionView } from '../views/collections/CollectionView';
+import { CollectionPDFView } from '../views/collections/CollectionPDFView';
+import { CollectionVideoView } from '../views/collections/CollectionVideoView';
export class DocumentManager {
@@ -24,28 +30,114 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentView(toFind: Document): DocumentView | null {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
- let toReturn: DocumentView | null;
- toReturn = null;
+ let toReturn: DocumentView | null = null;
+ let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
+
+ for (let i = 0; i < passes.length; i++) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ toReturn = view;
+ return;
+ }
+ });
+ if (!toReturn) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ toReturn = view;
+ }
+ });
+ }
+ }
+
+ return toReturn;
+ }
+
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ return this.getDocumentViewById(toFind[Id], preferredCollection);
+ }
+
+ public getDocumentViews(toFind: Doc): DocumentView[] {
+
+ let toReturn: DocumentView[] = [];
//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)) {
- // toReturn = view;
- // return;
- // }
- // }
-
- if (Object.is(doc, toFind)) {
- toReturn = view;
- return;
+
+ if (doc === toFind) {
+ toReturn.push(view);
+ } else {
+ let docSrc = FieldValue(doc.proto);
+ if (docSrc && Object.is(docSrc, toFind)) {
+ toReturn.push(view);
+ }
}
+ });
- })
+ return toReturn;
+ }
- return (toReturn);
+ @computed
+ public get LinkedDocumentViews() {
+ return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let linksList = DocListCast(dv.props.Document.linkedToDocs);
+ 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: Doc }[]));
+ }
+ linksList = DocListCast(dv.props.Document.linkedFromDocs);
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
+ if (linkFromDoc) {
+ DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
+ pairs.push({ a: dv, b: docView1, l: link }));
+ }
+ }
+ return pairs;
+ }, pairs));
+ }
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+ }
+
+ @undoBatch
+ public jumpToDocument = async (docDelegate: Doc, makeCopy: boolean = true): Promise<void> => {
+ let doc = docDelegate.proto ? docDelegate.proto : docDelegate;
+ const page = NumCast(doc.page, undefined);
+ const contextDoc = await Cast(doc.annotationOn, Doc);
+ if (contextDoc) {
+ const curPage = NumCast(contextDoc.curPage, page);
+ if (page !== curPage) contextDoc.curPage = page;
+ }
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ } else {
+ if (!contextDoc) {
+ CollectionDockingView.Instance.AddRightSplit(docDelegate ? (makeCopy ? Doc.MakeCopy(docDelegate) : docDelegate) : Doc.MakeDelegate(doc));
+ } else {
+ let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
+ if (contextView) {
+ contextDoc.panTransformType = "Ease";
+ contextView.props.focus(contextDoc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(contextDoc);
+ }
+ }
+ }
}
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4a61220a5..7f75a95f0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,40 +1,70 @@
-import { DocumentDecorations } from "../views/DocumentDecorations";
+import { action, runInAction } from "mobx";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
+import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { Document } from "../../fields/Document"
-import { action } from "mobx";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { ImageField } from "../../fields/ImageField";
-import { KeyStore } from "../../fields/KeyStore";
-
-export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document) {
- let onRowMove = action((e: PointerEvent): void => {
+import * as globalCssVariables from "../views/globalCssVariables.scss";
+
+export type dropActionType = "alias" | "copy" | undefined;
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
+ let onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- DragManager.StartDrag(_reference.current!, { document: docFunc() });
- });
- let onRowUp = action((e: PointerEvent): void => {
+ var dragData = new DragManager.DocumentDragData([await docFunc()]);
+ dragData.dropAction = dropAction;
+ dragData.moveDocument = moveFunc;
+ DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+ };
+ let onRowUp = (): void => {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
- });
- let onItemDown = (e: React.PointerEvent) => {
+ };
+ let onItemDown = async (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([await 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: Doc) {
+ let srcTarg = sourceDoc.proto;
+ let draggedDocs: Doc[] = [];
+ let draggedFromDocs: Doc[] = [];
+ if (srcTarg) {
+ let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
+ let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
+ if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+ if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ }
+ draggedDocs.push(...draggedFromDocs);
+ if (draggedDocs.length) {
+ let moddrag: Doc[] = [];
+ for (const draggedDoc of draggedDocs) {
+ let doc = await Cast(draggedDoc.annotationOn, Doc);
+ 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");
@@ -47,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 {
@@ -60,8 +92,7 @@ export namespace DragManager {
(): void;
}
- export class DragCompleteEvent {
- }
+ export class DragCompleteEvent { }
export interface DragHandlers {
dragComplete: (e: DragCompleteEvent) => void;
@@ -70,21 +101,29 @@ export namespace DragManager {
export interface DropOptions {
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 },
+ readonly mods: string
+ ) { }
}
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);
@@ -92,54 +131,120 @@ export namespace DragManager {
element.addEventListener("dashOnDrop", handler);
return () => {
element.removeEventListener("dashOnDrop", handler);
- delete element.dataset["canDrop"]
+ delete element.dataset.canDrop;
};
}
- export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions) {
+ export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ export class DocumentDragData {
+ constructor(dragDoc: Doc[]) {
+ this.draggedDocuments = dragDoc;
+ this.droppedDocuments = dragDoc;
+ this.xOffset = 0;
+ this.yOffset = 0;
+ }
+ draggedDocuments: Doc[];
+ droppedDocuments: Doc[];
+ xOffset: number;
+ yOffset: number;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
+ moveDocument?: MoveFunction;
+ [id: string]: any;
+ }
+
+ export let StartDragFunctions: (() => void)[] = [];
+
+ export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ runInAction(() => StartDragFunctions.map(func => func()));
+ StartDrag(eles, dragData, downX, downY, options,
+ (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: Doc, blacklist: Doc[] = []) {
+ this.linkSourceDocument = linkSourceDoc;
+ this.blacklist = blacklist;
+ }
+ droppedDocuments: Doc[] = [];
+ linkSourceDocument: Doc;
+ blacklist: Doc[];
+ dontClearTextBox?: boolean;
+ [id: string]: any;
+ }
+
+ export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
+ 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.className = "dragManager-dragDiv";
+ dragDiv.style.pointerEvents = "none";
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 docView: DocumentView = dragData["documentView"];
- const doc: Document = docView ? docView.props.Document : dragData["document"];
-
- 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])
- }
- }
+ let scaleXs: number[] = [];
+ let scaleYs: number[] = [];
+ let xs: number[] = [];
+ let ys: number[] = [];
+
+ const docs: Doc[] =
+ 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.color = "black";
+ 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) {
@@ -149,59 +254,92 @@ 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) {
+ dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+ }
if (e.shiftKey) {
- abortDrag();
- CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
+ AbortDrag();
+ 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 = () => {
+ 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);
- dragDiv.removeChild(dragElement);
- if (hideSource && !wasHidden) {
- ele.hidden = false;
+ if (options) {
+ options.handlers.dragComplete({});
}
- }
+ };
+
+ AbortDrag = () => {
+ hideDragElements();
+ endDrag();
+ };
const upHandler = (e: PointerEvent) => {
- abortDrag();
- FinishDrag(ele, e, dragData, options);
+ hideDragElements();
+ dispatchDrag(eles, e, dragData, options, finishDrag);
+ endDrag();
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
}
- function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions) {
- let parent = dragEle.parentElement;
- if (parent)
- parent.removeChild(dragEle);
+ 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);
+ 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);
- if (parent)
- parent.appendChild(dragEle);
- if (!target) {
- return;
- }
- target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
- bubbles: true,
- detail: {
- x: e.x,
- y: e.y,
- data: dragData
- }
- }));
- if (options) {
- options.handlers.dragComplete({});
+ removed.map(r => {
+ 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);
+
+ target.dispatchEvent(
+ new CustomEvent<DropEvent>("dashOnDrop", {
+ bubbles: true,
+ detail: {
+ x: e.x,
+ y: e.y,
+ data: dragData,
+ mods: e.altKey ? "AltKey" : ""
+ }
+ })
+ );
}
- DocumentDecorations.Instance.Hidden = false;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
new file mode 100644
index 000000000..92d2b2b44
--- /dev/null
+++ b/src/client/util/History.ts
@@ -0,0 +1,122 @@
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { DocServer } from "../DocServer";
+import { Main } from "../views/Main";
+import { RouteStore } from "../../server/RouteStore";
+
+export namespace HistoryUtil {
+ export interface DocInitializerList {
+ [key: string]: string | number;
+ }
+
+ export interface DocUrl {
+ type: "doc";
+ docId: string;
+ initializers: {
+ [docId: string]: DocInitializerList;
+ };
+ }
+
+ export type ParsedUrl = DocUrl;
+
+ // const handlers: ((state: ParsedUrl | null) => void)[] = [];
+ function onHistory(e: PopStateEvent) {
+ if (window.location.pathname !== RouteStore.home) {
+ const url = e.state as ParsedUrl || parseUrl(window.location.pathname);
+ if (url) {
+ switch (url.type) {
+ case "doc":
+ onDocUrl(url);
+ break;
+ }
+ }
+ }
+ // for (const handler of handlers) {
+ // handler(e.state);
+ // }
+ }
+
+ export function pushState(state: ParsedUrl) {
+ history.pushState(state, "", createUrl(state));
+ }
+
+ export function replaceState(state: ParsedUrl) {
+ history.replaceState(state, "", createUrl(state));
+ }
+
+ function copyState(state: ParsedUrl): ParsedUrl {
+ return JSON.parse(JSON.stringify(state));
+ }
+
+ export function getState(): ParsedUrl {
+ return copyState(history.state);
+ }
+
+ // export function addHandler(handler: (state: ParsedUrl | null) => void) {
+ // handlers.push(handler);
+ // }
+
+ // export function removeHandler(handler: (state: ParsedUrl | null) => void) {
+ // const index = handlers.indexOf(handler);
+ // if (index !== -1) {
+ // handlers.splice(index, 1);
+ // }
+ // }
+
+ export function parseUrl(pathname: string): ParsedUrl | undefined {
+ let pathnameSplit = pathname.split("/");
+ if (pathnameSplit.length !== 2) {
+ return undefined;
+ }
+ const type = pathnameSplit[0];
+ const data = pathnameSplit[1];
+
+ if (type === "doc") {
+ const s = data.split("?");
+ if (s.length < 1 || s.length > 2) {
+ return undefined;
+ }
+ const docId = s[0];
+ const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {};
+ return {
+ type: "doc",
+ docId,
+ initializers
+ };
+ }
+
+ return undefined;
+ }
+
+ export function createUrl(params: ParsedUrl): string {
+ let baseUrl = DocServer.prepend(`/${params.type}`);
+ switch (params.type) {
+ case "doc":
+ const initializers = encodeURIComponent(JSON.stringify(params.initializers));
+ const id = params.docId;
+ let url = baseUrl + `/${id}`;
+ if (Object.keys(params.initializers).length) {
+ url += `?${initializers}`;
+ }
+ return url;
+ }
+ return "";
+ }
+
+ export async function initDoc(id: string, initializer: DocInitializerList) {
+ const doc = await DocServer.GetRefField(id);
+ if (!(doc instanceof Doc)) {
+ return;
+ }
+ Doc.assign(doc, initializer);
+ }
+
+ async function onDocUrl(url: DocUrl) {
+ const field = await DocServer.GetRefField(url.docId);
+ await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
+ if (field instanceof Doc) {
+ Main.Instance.openWorkspace(field, true);
+ }
+ }
+
+ window.onpopstate = onHistory;
+}
diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorKeymap.ts
new file mode 100644
index 000000000..00d086b97
--- /dev/null
+++ b/src/client/util/ProsemirrorKeymap.ts
@@ -0,0 +1,100 @@
+import { Schema } from "prosemirror-model";
+import {
+ wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
+ joinUp, joinDown, lift, selectParentNode
+} from "prosemirror-commands";
+import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list";
+import { undo, redo } from "prosemirror-history";
+import { undoInputRule } from "prosemirror-inputrules";
+import { Transaction, EditorState } from "prosemirror-state";
+
+const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
+
+export type KeyMap = { [key: string]: any };
+
+export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
+ let keys: { [key: string]: any } = {}, type;
+
+ function bind(key: string, cmd: any) {
+ if (mapKeys) {
+ let mapped = mapKeys[key];
+ if (mapped === false) return;
+ if (mapped) key = mapped;
+ }
+ keys[key] = cmd;
+ }
+
+ bind("Mod-z", undo);
+ bind("Shift-Mod-z", redo);
+ bind("Backspace", undoInputRule);
+
+ if (!mac) {
+ bind("Mod-y", redo);
+ }
+
+ bind("Alt-ArrowUp", joinUp);
+ bind("Alt-ArrowDown", joinDown);
+ bind("Mod-BracketLeft", lift);
+ bind("Escape", selectParentNode);
+
+ if (type = schema.marks.strong) {
+ bind("Mod-b", toggleMark(type));
+ bind("Mod-B", toggleMark(type));
+ }
+ if (type = schema.marks.em) {
+ bind("Mod-i", toggleMark(type));
+ bind("Mod-I", toggleMark(type));
+ }
+ if (type = schema.marks.code) {
+ bind("Mod-`", toggleMark(type));
+ }
+
+ if (type = schema.nodes.bullet_list) {
+ bind("Ctrl-b", wrapInList(type));
+ }
+ if (type = schema.nodes.ordered_list) {
+ bind("Ctrl-n", wrapInList(type));
+ }
+ if (type = schema.nodes.blockquote) {
+ bind("Ctrl->", wrapIn(type));
+ }
+ if (type = schema.nodes.hard_break) {
+ let br = type, cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
+ return true;
+ }
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ if (mac) {
+ bind("Ctrl-Enter", cmd);
+ }
+ }
+ if (type = schema.nodes.list_item) {
+ bind("Enter", splitListItem(type));
+ bind("Shift-Tab", liftListItem(type));
+ bind("Tab", sinkListItem(type));
+ }
+ if (type = schema.nodes.paragraph) {
+ bind("Shift-Ctrl-0", setBlockType(type));
+ }
+ if (type = schema.nodes.code_block) {
+ bind("Shift-Ctrl-\\", setBlockType(type));
+ }
+ if (type = schema.nodes.heading) {
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(type, { level: i }));
+ }
+ }
+ if (type = schema.nodes.horizontal_rule) {
+ let hr = type;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+ }
+
+ return keys;
+} \ No newline at end of file
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
new file mode 100644
index 000000000..3b8396510
--- /dev/null
+++ b/src/client/util/RichTextRules.ts
@@ -0,0 +1,43 @@
+import {
+ inputRules,
+ wrappingInputRule,
+ textblockTypeInputRule,
+ smartQuotes,
+ emDash,
+ ellipsis
+} from "prosemirror-inputrules";
+import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model";
+
+import { schema } from "./RichTextSchema";
+
+export const inpRules = {
+ rules: [
+ ...smartQuotes,
+ ellipsis,
+ emDash,
+
+ // > blockquote
+ wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+
+ // 1. ordered list
+ wrappingInputRule(
+ /^(\d+)\.\s$/,
+ schema.nodes.ordered_list,
+ match => ({ order: +match[1] }),
+ (match, node) => node.childCount + node.attrs.order === +match[1]
+ ),
+
+ // * bullet list
+ wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
+
+ // ``` code block
+ textblockTypeInputRule(/^```$/, schema.nodes.code_block),
+
+ // # heading
+ textblockTypeInputRule(
+ new RegExp("^(#{1,6})\\s$"),
+ schema.nodes.heading,
+ match => ({ level: match[1].length })
+ )
+ ]
+};
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index abf448c9f..3e3e98206 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,129 +1,147 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray } 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
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes: { [index: string]: NodeSpec } = {
- // :: NodeSpec The top level document node.
- doc: {
- content: "block+"
- },
-
- // :: NodeSpec A plain paragraph textblock. Represented in the DOM
- // as a `<p>` element.
- paragraph: {
- content: "inline*",
- group: "block",
- parseDOM: [{ tag: "p" }],
- toDOM() { return pDOM }
- },
-
- // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
- blockquote: {
- content: "block+",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "blockquote" }],
- toDOM() { return blockquoteDOM }
- },
-
- // :: NodeSpec A horizontal rule (`<hr>`).
- horizontal_rule: {
- group: "block",
- parseDOM: [{ tag: "hr" }],
- toDOM() { return hrDOM }
- },
-
- // :: NodeSpec A heading textblock, with a `level` attribute that
- // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
- // `<h6>` elements.
- heading: {
- attrs: { level: { default: 1 } },
- content: "inline*",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "h1", attrs: { level: 1 } },
- { tag: "h2", attrs: { level: 2 } },
- { tag: "h3", attrs: { level: 3 } },
- { tag: "h4", attrs: { level: 4 } },
- { tag: "h5", attrs: { level: 5 } },
- { tag: "h6", attrs: { level: 6 } }],
- toDOM(node: any) { return ["h" + node.attrs.level, 0] }
- },
-
- // :: NodeSpec A code listing. Disallows marks or non-text inline
- // nodes by default. Represented as a `<pre>` element with a
- // `<code>` element inside of it.
- code_block: {
- content: "text*",
- marks: "",
- group: "block",
- code: true,
- defining: true,
- parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
- toDOM() { return preDOM }
- },
-
- // :: NodeSpec The text node.
- text: {
- group: "inline"
- },
-
- // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
- // `alt`, and `href` attributes. The latter two default to the empty
- // string.
- image: {
- inline: true,
- attrs: {
- src: {},
- alt: { default: null },
- title: { default: null }
- },
- group: "inline",
- draggable: true,
- parseDOM: [{
- tag: "img[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
+ // :: NodeSpec The top level document node.
+ doc: {
+ content: "block+"
+ },
+
+ // :: NodeSpec A plain paragraph textblock. Represented in the DOM
+ // as a `<p>` element.
+ paragraph: {
+ content: "inline*",
+ group: "block",
+ parseDOM: [{ tag: "p" }],
+ toDOM() { return pDOM; }
+ },
+
+ // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`<hr>`).
+ horizontal_rule: {
+ group: "block",
+ parseDOM: [{ tag: "hr" }],
+ toDOM() { return hrDOM; }
+ },
+
+ // :: NodeSpec A heading textblock, with a `level` attribute that
+ // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
+ // `<h6>` elements.
+ heading: {
+ attrs: { level: { default: 1 } },
+ content: "inline*",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "h1", attrs: { level: 1 } },
+ { tag: "h2", attrs: { level: 2 } },
+ { tag: "h3", attrs: { level: 3 } },
+ { tag: "h4", attrs: { level: 4 } },
+ { tag: "h5", attrs: { level: 5 } },
+ { tag: "h6", attrs: { level: 6 } }],
+ toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ },
+
+ // :: NodeSpec A code listing. Disallows marks or non-text inline
+ // nodes by default. Represented as a `<pre>` element with a
+ // `<code>` element inside of it.
+ code_block: {
+ content: "text*",
+ marks: "",
+ group: "block",
+ code: true,
+ defining: true,
+ parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
+ toDOM() { return preDOM; }
+ },
+
+ // :: NodeSpec The text node.
+ text: {
+ group: "inline"
+ },
+
+ // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
+ // `alt`, and `href` attributes. The latter two default to the empty
+ // string.
+ image: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "img[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
}
- }
- }],
- toDOM(node: any) { return ["img", node.attrs] }
- },
-
- // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
- hard_break: {
- inline: true,
- group: "inline",
- selectable: false,
- parseDOM: [{ tag: "br" }],
- toDOM() { return brDOM }
- },
-
- ordered_list: {
- ...orderedList,
- content: 'list_item+',
- group: 'block'
- },
- bullet_list: {
- content: 'list_item+',
- group: 'block',
- parseDOM: [{ tag: "ul" }, { style: "list-style-type=disc;" }],
- toDOM() { return ulDOM }
- },
- list_item: {
- ...listItem,
- content: 'paragraph block*'
- }
-}
+ },
+
+ // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
+ hard_break: {
+ inline: true,
+ group: "inline",
+ selectable: false,
+ parseDOM: [{ tag: "br" }],
+ toDOM() { return brDOM; }
+ },
+
+ ordered_list: {
+ ...orderedList,
+ content: 'list_item+',
+ group: 'block'
+ },
+ //this doesn't currently work for some reason
+ bullet_list: {
+ ...bulletList,
+ content: 'list_item+',
+ group: 'block',
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ // toDOM() { return ulDOM }
+ },
+ //bullet_list: {
+ // content: 'list_item+',
+ // group: 'block',
+ //active: blockActive(schema.nodes.bullet_list),
+ //enable: wrapInList(schema.nodes.bullet_list),
+ //run: wrapInList(schema.nodes.bullet_list),
+ //select: state => true,
+ // },
+ list_item: {
+ ...listItem,
+ content: 'paragraph block*'
+ }
+};
const emDOM: DOMOutputSpecArray = ["em", 0];
const strongDOM: DOMOutputSpecArray = ["strong", 0];
@@ -132,86 +150,263 @@ const underlineDOM: DOMOutputSpecArray = ["underline", 0];
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks: { [index: string]: MarkSpec } = {
- // :: MarkSpec A link. Has `href` and `title` attributes. `title`
- // defaults to the empty string. Rendered and parsed as an `<a>`
- // element.
- link: {
- attrs: {
- href: {},
- title: { default: null }
- },
- inclusive: false,
- parseDOM: [{
- tag: "a[href]", getAttrs(dom: any) {
- return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }
- }
- }],
- 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 }
- },
-
- // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
- // also match `<b>` and `font-weight: bold`.
- strong: {
- parseDOM: [{ tag: "strong" },
- { tag: "b" },
- { style: "font-weight" }],
- toDOM() { return strongDOM }
- },
-
- underline: {
- parseDOM: [
- { tag: 'u' },
- { style: 'text-decoration=underline' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration:underline'
- }]
- },
-
- strikethrough: {
- parseDOM: [
- { tag: 'strike' },
- { style: 'text-decoration=line-through' },
- { style: 'text-decoration-line=line-through' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration-line:line-through'
- }]
- },
-
- subscript: {
- excludes: 'superscript',
- parseDOM: [
- { tag: 'sub' },
- { style: 'vertical-align=sub' }
- ],
- toDOM: () => ['sub']
- },
-
- superscript: {
- excludes: 'subscript',
- parseDOM: [
- { tag: 'sup' },
- { style: 'vertical-align=super' }
- ],
- toDOM: () => ['sup']
- },
-
-
- // :: MarkSpec Code font mark. Represented as a `<code>` element.
- code: {
- parseDOM: [{ tag: "code" }],
- toDOM() { return codeDOM }
- }
+ // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // defaults to the empty string. Rendered and parsed as an `<a>`
+ // element.
+ link: {
+ attrs: {
+ href: {},
+ title: { default: null }
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return { href: dom.getAttribute("href"), title: dom.getAttribute("title") };
+ }
+ }],
+ 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; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
+ // also match `<b>` and `font-weight: bold`.
+ strong: {
+ parseDOM: [{ tag: "strong" },
+ { tag: "b" },
+ { style: "font-weight" }],
+ toDOM() { return strongDOM; }
+ },
+
+ underline: {
+ parseDOM: [
+ { tag: 'u' },
+ { style: 'text-decoration=underline' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline'
+ }]
+ },
+
+ strikethrough: {
+ parseDOM: [
+ { tag: 'strike' },
+ { style: 'text-decoration=line-through' },
+ { style: 'text-decoration-line=line-through' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration-line:line-through'
+ }]
+ },
+
+ subscript: {
+ excludes: 'superscript',
+ parseDOM: [
+ { tag: 'sub' },
+ { style: 'vertical-align=sub' }
+ ],
+ toDOM: () => ['sub']
+ },
+
+ superscript: {
+ excludes: 'subscript',
+ parseDOM: [
+ { tag: 'sup' },
+ { style: 'vertical-align=super' }
+ ],
+ toDOM: () => ['sup']
+ },
+
+
+ // :: MarkSpec Code font mark. Represented as a `<code>` element.
+ code: {
+ parseDOM: [{ tag: "code" }],
+ toDOM() { return codeDOM; }
+ },
+
+
+ /* FONTS */
+ timesNewRoman: {
+ parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: "Times New Roman", Times, serif;'
+ }]
+ },
+
+ arial: {
+ parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Arial, Helvetica, sans-serif;'
+ }]
+ },
+
+ georgia: {
+ parseDOM: [{ style: 'font-family: Georgia, serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Georgia, serif;'
+ }]
+ },
+
+ comicSans: {
+ parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: "Comic Sans MS", cursive, sans-serif;'
+ }]
+ },
+
+ tahoma: {
+ parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Tahoma, Geneva, sans-serif;'
+ }]
+ },
+
+ impact: {
+ parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: Impact, Charcoal, sans-serif;'
+ }]
+ },
+
+ crimson: {
+ parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }],
+ toDOM: () => ['span', {
+ style: 'font-family: "Crimson Text", sans-serif;'
+ }]
+ },
+
+ /** FONT SIZES */
+
+ p10: {
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 10px;'
+ }]
+ },
+
+ p12: {
+ parseDOM: [{ style: 'font-size: 12px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 12px;'
+ }]
+ },
+
+ p14: {
+ parseDOM: [{ style: 'font-size: 14px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 14px;'
+ }]
+ },
+
+ p16: {
+ parseDOM: [{ style: 'font-size: 16px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 16px;'
+ }]
+ },
+
+ p24: {
+ parseDOM: [{ style: 'font-size: 24px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 24px;'
+ }]
+ },
+
+ p32: {
+ parseDOM: [{ style: 'font-size: 32px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 32px;'
+ }]
+ },
+
+ p48: {
+ parseDOM: [{ style: 'font-size: 48px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 48px;'
+ }]
+ },
+
+ p72: {
+ parseDOM: [{ style: 'font-size: 72px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 72px;'
+ }]
+ },
+};
+function getFontSize(element: any) {
+ return parseFloat((getComputedStyle(element) as any).fontSize);
}
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any) {
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ let self = this;
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ view.dispatch(
+ view.state.tr.setNodeMarkup(getPos(), null,
+ { src: node.attrs.src, width: self._outer.style.width })
+ .setSelection(view.state.selection));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
+
+ this._outer.appendChild(this._handle);
+ this._outer.appendChild(this._img);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
@@ -220,4 +415,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 46bd1a206..e45f61c11 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,56 +1,76 @@
// 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
// 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 { 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;
+ 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];
- let paramNames = ["KeyStore", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [KeyStore, ...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 = [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 => {
+ 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 {
@@ -72,14 +92,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 {
@@ -106,27 +126,44 @@ 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}
- })()`
+ })`;
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 || {});
-}
-
-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;
+ return Run(outputText, paramNames, diagnostics, script, options);
} \ No newline at end of file
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/SearchUtil.ts b/src/client/util/SearchUtil.ts
new file mode 100644
index 000000000..4ccff0d1b
--- /dev/null
+++ b/src/client/util/SearchUtil.ts
@@ -0,0 +1,25 @@
+import * as rp from 'request-promise';
+import { DocServer } from '../DocServer';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/RefField';
+
+export namespace SearchUtil {
+ export function Search(query: string, returnDocs: true): Promise<Doc[]>;
+ export function Search(query: string, returnDocs: false): Promise<string[]>;
+ export async function Search(query: string, returnDocs: boolean) {
+ const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+ qs: { query }
+ }));
+ if (!returnDocs) {
+ return ids;
+ }
+ const docMap = await DocServer.GetRefFields(ids);
+ return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
+ }
+
+ export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> {
+ const proto = await Doc.GetT(doc, "proto", Doc, true);
+ const protoId = (proto || doc)[Id];
+ return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1a711ae64..8c92c2023 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,5 +1,8 @@
import { observable, action } from "mobx";
+import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { NumCast } from "../../new_fields/Types";
export namespace SelectionManager {
class Manager {
@@ -10,30 +13,76 @@ 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.whenActiveChanged(true);
}
}
+
+ @action
+ DeselectAll(): void {
+ manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
+ manager.SelectedDocuments = [];
+ FormattedTextBox.InputBoxOverlay = undefined;
+ }
+ @action
+ ReselectAll() {
+ let sdocs = manager.SelectedDocuments.map(d => d);
+ manager.SelectedDocuments = [];
+ return sdocs;
+ }
+ @action
+ ReselectAll2(sdocs: DocumentView[]) {
+ sdocs.map(s => SelectionManager.SelectDoc(s, true));
+ }
}
- 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?: Doc): 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 ReselectAll() {
+ let sdocs = manager.ReselectAll();
+ setTimeout(() => manager.ReselectAll2(sdocs), 0);
+ }
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
-} \ No newline at end of file
+ export function ViewsSortedVertically(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
+ export function ViewsSortedHorizontally(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
+ if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
+}
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/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
new file mode 100644
index 000000000..55e0eb909
--- /dev/null
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -0,0 +1,86 @@
+import { action, IReactionDisposer, reaction } from "mobx";
+import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
+import { baseKeymap, lift } from "prosemirror-commands";
+import { history, redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorView } from "prosemirror-view";
+import { schema } from "./RichTextSchema";
+import { Schema, NodeType, MarkType } 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, liftListItem, listItem } from 'prosemirror-schema-list';
+import {
+ faListUl,
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { throwStatement } from "babel-types";
+
+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 TooltipLinkingMenu {
+
+ private tooltip: HTMLElement;
+ private view: EditorView;
+ private editorProps: FieldViewProps;
+
+ constructor(view: EditorView, editorProps: FieldViewProps) {
+ this.view = view;
+ this.editorProps = editorProps;
+ this.tooltip = document.createElement("div");
+ this.tooltip.className = "tooltipMenu linking";
+
+ //add the div which is the tooltip
+ view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
+
+ let target = "https://www.google.com";
+
+ let link = document.createElement("a");
+ link.href = target;
+ link.textContent = target;
+ link.target = "_blank";
+ link.style.color = "white";
+ this.tooltip.appendChild(link);
+
+ this.update(view, undefined);
+ }
+
+ //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;
+ }
+
+ console.log("STORED:");
+ console.log(state.doc.content.firstChild!.content);
+
+ // 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.tooltip.style.width = "auto";
+ this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
+ }
+
+ destroy() { this.tooltip.remove(); }
+}
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index fa43f5326..437da0d63 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -1,14 +1,260 @@
+@import "../views/globalCssVariables";
+
+.ProseMirror-textblock-dropdown {
+ min-width: 3em;
+ }
+
+ .ProseMirror-menu {
+ margin: 0 -4px;
+ line-height: 1;
+ }
+
+ .ProseMirror-tooltip .ProseMirror-menu {
+ width: -webkit-fit-content;
+ width: fit-content;
+ white-space: pre;
+ }
+
+ .ProseMirror-menuitem {
+ margin-right: 3px;
+ display: inline-block;
+ }
+
+ .ProseMirror-menuseparator {
+ // border-right: 1px solid #ddd;
+ margin-right: 3px;
+ }
+
+ .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
+ font-size: 90%;
+ white-space: nowrap;
+ }
+
+ .ProseMirror-menu-dropdown {
+ vertical-align: 1px;
+ cursor: pointer;
+ position: relative;
+ padding-right: 15px;
+ margin: 3px;
+ background: #333333;
+ border-radius: 3px;
+ text-align: center;
+ }
+
+ .ProseMirror-menu-dropdown-wrap {
+ padding: 1px 0 1px 4px;
+ display: inline-block;
+ position: relative;
+ }
+
+ .ProseMirror-menu-dropdown:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+
+ .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
+ position: absolute;
+ background: $dark-color;
+ color:white;
+ border: 1px solid rgb(223, 223, 223);
+ padding: 2px;
+ }
+
+ .ProseMirror-menu-dropdown-menu {
+ z-index: 15;
+ min-width: 6em;
+ }
+
+ .linking {
+ text-align: center;
+ }
+
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ padding: 2px 8px 2px 4px;
+ width: auto;
+ }
+
+ .ProseMirror-menu-dropdown-item:hover {
+ background: #2e2b2b;
+ }
+
+ .ProseMirror-menu-submenu-wrap {
+ position: relative;
+ margin-right: -4px;
+ }
+
+ .ProseMirror-menu-submenu-label:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 4px);
+ }
+
+ .ProseMirror-menu-submenu {
+ display: none;
+ min-width: 4em;
+ left: 100%;
+ top: -3px;
+ }
+
+ .ProseMirror-menu-active {
+ background: #eee;
+ border-radius: 4px;
+ }
+
+ .ProseMirror-menu-active {
+ background: #eee;
+ border-radius: 4px;
+ }
+
+ .ProseMirror-menu-disabled {
+ opacity: .3;
+ }
+
+ .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
+ display: block;
+ }
+
+ .ProseMirror-menubar {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+ position: relative;
+ min-height: 1em;
+ color: white;
+ padding: 1px 6px;
+ top: 0; left: 0; right: 0;
+ border-bottom: 1px solid silver;
+ background:$dark-color;
+ z-index: 10;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ overflow: visible;
+ }
+
+ .ProseMirror-icon {
+ display: inline-block;
+ line-height: .8;
+ vertical-align: -2px; /* Compensate for padding */
+ padding: 2px 8px;
+ cursor: pointer;
+ }
+
+ .ProseMirror-menu-disabled.ProseMirror-icon {
+ cursor: default;
+ }
+
+ .ProseMirror-icon svg {
+ fill: currentColor;
+ height: 1em;
+ }
+
+ .ProseMirror-icon span {
+ vertical-align: text-top;
+ }
+
+ .ProseMirror ul, .ProseMirror ol {
+ padding-left: 30px;
+ }
+
+ .ProseMirror blockquote {
+ padding-left: 1em;
+ border-left: 3px solid #eee;
+ margin-left: 0; margin-right: 0;
+ }
+
+ .ProseMirror-example-setup-style img {
+ cursor: default;
+ }
+
+ .ProseMirror-prompt {
+ background: white;
+ padding: 5px 10px 5px 15px;
+ border: 1px solid silver;
+ position: fixed;
+ border-radius: 3px;
+ z-index: 11;
+ box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
+ }
+
+ .ProseMirror-prompt h5 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 100%;
+ color: #444;
+ }
+
+ .ProseMirror-prompt input[type="text"],
+ .ProseMirror-prompt textarea {
+ background: #eee;
+ border: none;
+ outline: none;
+ }
+
+ .ProseMirror-prompt input[type="text"] {
+ padding: 0 4px;
+ }
+
+ .ProseMirror-prompt-close {
+ position: absolute;
+ left: 2px; top: 1px;
+ color: #666;
+ border: none; background: transparent; padding: 0;
+ }
+
+ .ProseMirror-prompt-close:after {
+ content: "✕";
+ font-size: 12px;
+ }
+
+ .ProseMirror-invalid {
+ background: #ffc;
+ border: 1px solid #cc7;
+ border-radius: 4px;
+ padding: 5px 10px;
+ position: absolute;
+ min-width: 10em;
+ }
+
+ .ProseMirror-prompt-buttons {
+ margin-top: 5px;
+ display: none;
+ }
.tooltipMenu {
position: absolute;
- z-index: 20;
- background: rgb(19, 18, 18);
+ z-index: 200;
+ background: $dark-color;
border: 1px solid silver;
border-radius: 4px;
padding: 2px 10px;
margin-bottom: 7px;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
+ pointer-events: all;
+ .ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
}
.tooltipMenu:before {
@@ -31,7 +277,7 @@
bottom: -4.5px;
border: 5px solid transparent;
border-bottom-width: 0;
- border-top-color: black;
+ border-top-color: $dark-color;
}
.menuicon {
@@ -51,4 +297,8 @@
.underline {text-decoration: underline}
.superscript {vertical-align:super}
.subscript { vertical-align:sub }
- .strikethrough {text-decoration-line:line-through} \ No newline at end of file
+ .strikethrough {text-decoration-line:line-through}
+ .font-size-indicator {
+ font-size: 12px;
+ padding-right: 0px;
+ }
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 3b87fe9de..4d40d09b2 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,125 +1,478 @@
import { action, IReactionDisposer, reaction } from "mobx";
-import { baseKeymap } from "prosemirror-commands";
+import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
+import { baseKeymap, lift } from "prosemirror-commands";
import { history, redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-const { exampleSetup } = require("prosemirror-example-setup")
-import { EditorState, Transaction, } from "prosemirror-state";
+import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
-import React = require("react")
+import { Schema, NodeType, MarkType, Mark } 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 { library } from '@fortawesome/fontawesome-svg-core';
+import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
+import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
import {
- faListUl,
+ faListUl,
} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { throwStatement } from "babel-types";
+import { View } from "@react-pdf/renderer";
+import { DragManager } from "./DragManager";
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/RefField";
+import { Utils } from "../northstar/utils/Utils";
+import { DocServer } from "../DocServer";
+import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocumentManager } from "./DocumentManager";
+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;
-
- 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") },
- { 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)) {
- command(view.state, view.dispatch, view)
+ 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");
+ private linkEditor?: HTMLDivElement;
+ private linkText?: HTMLDivElement;
+ private linkDrag?: HTMLImageElement;
+ //dropdown doms
+ private fontSizeDom?: Node;
+ private fontStyleDom?: Node;
+ private listTypeBtnDom?: Node;
+
+ constructor(view: EditorView, editorProps: FieldViewProps) {
+ this.view = view;
+ this.state = view.state;
+ 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") },
+ // { 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 }) => {
+ this.tooltip.appendChild(dom);
+
+ //pointer down handler to activate button effects
+ dom.addEventListener("pointerdown", e => {
+ e.preventDefault();
+ view.focus();
+ command(view.state, view.dispatch, view);
+ });
+
+ });
+
+ //list of font styles
+ this.fontStylesToName = new Map();
+ this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman");
+ this.fontStylesToName.set(schema.marks.arial, "Arial");
+ this.fontStylesToName.set(schema.marks.georgia, "Georgia");
+ this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS");
+ this.fontStylesToName.set(schema.marks.tahoma, "Tahoma");
+ this.fontStylesToName.set(schema.marks.impact, "Impact");
+ this.fontStylesToName.set(schema.marks.crimson, "Crimson Text");
+ this.fontStyles = Array.from(this.fontStylesToName.keys());
+
+ //font sizes
+ this.fontSizeToNum = new Map();
+ this.fontSizeToNum.set(schema.marks.p10, 10);
+ this.fontSizeToNum.set(schema.marks.p12, 12);
+ this.fontSizeToNum.set(schema.marks.p14, 14);
+ this.fontSizeToNum.set(schema.marks.p16, 16);
+ this.fontSizeToNum.set(schema.marks.p24, 24);
+ this.fontSizeToNum.set(schema.marks.p32, 32);
+ this.fontSizeToNum.set(schema.marks.p48, 48);
+ this.fontSizeToNum.set(schema.marks.p72, 72);
+ this.fontSizes = Array.from(this.fontSizeToNum.keys());
+
+ //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);
+ }
+
+ //label of dropdown will change to given label
+ updateFontSizeDropdown(label: string) {
+ //filtering function - might be unecessary
+ let cut = (arr: MenuItem[]) => arr.filter(x => x);
+
+ //font SIZES
+ let fontSizeBtns: MenuItem[] = [];
+ this.fontSizeToNum.forEach((number, mark) => {
+ fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ });
+
+ if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); }
+ this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), {
+ label: label,
+ css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;"
+ }) as MenuItem).render(this.view).dom;
+ this.tooltip.appendChild(this.fontSizeDom);
+ }
+
+ //label of dropdown will change to given label
+ updateFontStyleDropdown(label: string) {
+ //filtering function - might be unecessary
+ let cut = (arr: MenuItem[]) => arr.filter(x => x);
+
+ //font STYLES
+ let fontBtns: MenuItem[] = [];
+ this.fontStylesToName.forEach((name, mark) => {
+ 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); }
+ this.fontStyleDom = (new Dropdown(cut(fontBtns), {
+ label: label,
+ css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;"
+ }) as MenuItem).render(this.view).dom;
+
+ this.tooltip.appendChild(this.fontStyleDom);
+ }
+
+ updateLinkMenu() {
+ if (!this.linkEditor || !this.linkText) {
+ this.linkEditor = document.createElement("div");
+ this.linkEditor.style.color = "white";
+ this.linkText = document.createElement("div");
+ this.linkText.style.cssFloat = "left";
+ this.linkText.style.marginRight = "5px";
+ this.linkText.style.marginLeft = "5px";
+ this.linkText.setAttribute("contenteditable", "true");
+ this.linkText.style.whiteSpace = "nowrap";
+ this.linkText.style.width = "150px";
+ this.linkText.style.overflow = "hidden";
+ this.linkText.style.color = "white";
+ this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
+ let linkBtn = document.createElement("div");
+ linkBtn.textContent = ">>";
+ linkBtn.style.width = "20px";
+ linkBtn.style.height = "20px";
+ linkBtn.style.color = "white";
+ linkBtn.style.cssFloat = "left";
+ linkBtn.onpointerdown = (e: PointerEvent) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ let href: string = link.attrs.href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f)) {
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ }
+ else CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+ this.linkDrag = document.createElement("img");
+ this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
+ this.linkDrag.style.width = "20px";
+ this.linkDrag.style.height = "20px";
+ this.linkDrag.style.color = "white";
+ this.linkDrag.style.background = "black";
+ this.linkDrag.style.cssFloat = "left";
+ this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ let dragData = new DragManager.LinkDragData(this.editorProps.Document);
+ dragData.dontClearTextBox = true;
+ DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
+ {
+ handlers: {
+ dragComplete: action(() => {
+ let m = dragData.droppedDocuments;
+ this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+ }),
+ },
+ hideSource: false
+ });
+ };
+ this.linkEditor.appendChild(this.linkDrag);
+ this.linkEditor.appendChild(this.linkText);
+ this.linkEditor.appendChild(linkBtn);
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ this.linkText.textContent = link ? link.attrs.href : "-empty-";
+
+ this.linkText.onkeydown = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ this.makeLink(this.linkText!.textContent!);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ makeLink = (target: string) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ //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;
+ let state = view.state;
+ let dispatch = view.dispatch;
+
+ //remove all other active font marks
+ fontMarks.forEach((type) => {
+ if (dispatch) {
+ if ($cursor) {
+ if (type.isInSet(state.storedMarks || $cursor.marks())) {
+ dispatch(state.tr.removeStoredMark(type));
+ }
+ } else {
+ let has = false, tr = state.tr;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ has = state.doc.rangeHasMark($from.pos, $to.pos, type);
+ }
+ for (let i of ranges) {
+ let { $from, $to } = i;
+ if (has) {
+ toggleMark(type)(view.state, view.dispatch, view);
+ }
+ }
+ }
+ }
+ }); //actually apply font
+ return toggleMark(markType)(view.state, view.dispatch, view);
+ }
+
+ //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);
}
- })
- })
-
- 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;
- }
-
- blockActive(view: EditorView) {
- const { $from, to } = view.state.selection
-
- return to <= $from.end() && $from.parent.hasMarkup(schema.nodes.bulletList);
- }
-
- //this doesn't currently work but hopefully will soon
- 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
- }
-
- // Otherwise, reposition it and update its content
- this.tooltip.style.display = ""
- let { from, to } = state.selection
- // These are in screen coordinates
- //check this - tranform
- 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 = 120 + "px";
- this.tooltip.style.bottom = (box.bottom - start.top) + "px";
- }
-
- destroy() { this.tooltip.remove() }
-} \ No newline at end of file
+ }
+
+ //makes a button for the drop down FOR MARKS
+ //css is the style you want applied to the button
+ 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,
+ execEvent: "",
+ class: "menuicon",
+ css: css,
+ enable(state) { return true; },
+ run() {
+ changeToMarkInGroup(markType, view, groupMarks);
+ }
+ });
+ }
+
+ //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");
+ span.className = name + " menuicon";
+ span.title = name;
+ span.textContent = text;
+ span.style.color = "white";
+ return span;
+ }
+
+ //method for checking whether node can be inserted
+ canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) {
+ let $from = state.selection.$from;
+ for (let d = $from.depth; d >= 0; d--) {
+ let index = $from.index(d);
+ if ($from.node(d).canReplaceWith(index, index, nodeType)) return true;
+ }
+ return false;
+ }
+
+
+ //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);
+ }
+ }
+
+ // 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;
+ }
+
+ // 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.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) {
+ // if we want to update something somewhere with active font name
+ let fontName = this.fontStylesToName.get(activeStyles[0]);
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ } else if (activeStyles.length === 0) {
+ //crimson on default
+ this.updateFontStyleDropdown("Crimson Text");
+ } else {
+ this.updateFontStyleDropdown("Various");
+ }
+
+ //UPDATE FONT SIZE DROPDOWN
+ let activeSizes = this.activeMarksOnSelection(this.fontSizes);
+ if (activeSizes.length === 1) { //if there's only one active font size
+ let size = this.fontSizeToNum.get(activeSizes[0]);
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ } else if (activeSizes.length === 0) {
+ //should be 14 on default
+ this.updateFontSizeDropdown("14 pt");
+ } else { //multiple font sizes selected
+ this.updateFontSizeDropdown("Various");
+ }
+
+ this.updateLinkMenu();
+ }
+
+ //finds all active marks on selection in given group
+ activeMarksOnSelection(markGroup: MarkType[]) {
+ //current selection
+ let { empty, $cursor, ranges } = this.view.state.selection as TextSelection;
+ let state = this.view.state;
+ let dispatch = this.view.dispatch;
+
+ let activeMarks = markGroup.filter(mark => {
+ if (dispatch) {
+ let has = false, tr = state.tr;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ return state.doc.rangeHasMark($from.pos, $to.pos, mark);
+ }
+ }
+ return false;
+ });
+ return activeMarks;
+ }
+
+ destroy() { this.tooltip.remove(); }
+}
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 46ad558f3..c0ed015bd 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,4 +1,14 @@
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
+import 'source-map-support/register';
+import { Without } from "../../Utils";
+
+function getBatchName(target: any, key: string | symbol): string {
+ let keyName = key.toString();
+ if (target && target.constructor && target.constructor.name) {
+ return `${target.constructor.name}.${keyName}`;
+ }
+ return keyName;
+}
function propertyDecorator(target: any, key: string | symbol) {
Object.defineProperty(target, key, {
@@ -13,18 +23,31 @@ function propertyDecorator(target: any, key: string | symbol) {
writable: true,
configurable: true,
value: function (...args: any[]) {
+ let batch = UndoManager.StartBatch(getBatchName(target, key));
try {
- UndoManager.StartBatch();
return value.apply(this, args);
} finally {
- UndoManager.EndBatch();
+ 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;
@@ -32,13 +55,13 @@ export function undoBatch(target: any, key: string | symbol, descriptor?: TypedP
const oldFunction = descriptor.value;
descriptor.value = function (...args: any[]) {
+ let batch = UndoManager.StartBatch(getBatchName(target, key));
try {
- UndoManager.StartBatch()
- return oldFunction.apply(this, args)
+ return oldFunction.apply(this, args);
} finally {
- UndoManager.EndBatch()
+ batch.end();
}
- }
+ };
return descriptor;
}
@@ -70,26 +93,64 @@ export namespace UndoManager {
return redoStack.length > 0;
}
- export function StartBatch(): void {
+ export function PrintBatches(): void {
+ GetOpenBatches().forEach(batch => console.log(batch.batchName));
+ }
+
+ let openBatches: Batch[] = [];
+ 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;
+
+ constructor(readonly batchName: string) {
+ openBatches.push(this);
+ }
+
+ private dispose = (cancel: boolean) => {
+ if (this.disposed) {
+ throw new Error("Cannot dispose an already disposed batch");
+ }
+ this.disposed = true;
+ openBatches.splice(openBatches.indexOf(this));
+ EndBatch(cancel);
+ }
+
+ end = () => { this.dispose(false); };
+ cancel = () => { this.dispose(true); };
+ }
+
+ export function StartBatch(batchName: string): Batch {
batchCounter++;
if (batchCounter > 0) {
currentBatch = [];
}
+ return new Batch(batchName);
}
- export const EndBatch = action(() => {
+ const EndBatch = action((cancel: boolean = false) => {
batchCounter--;
if (batchCounter === 0 && currentBatch && currentBatch.length) {
- undoStack.push(currentBatch);
+ if (!cancel) {
+ undoStack.push(currentBatch);
+ }
redoStack.length = 0;
currentBatch = undefined;
}
- })
+ });
- export function RunInBatch(fn: () => void) {
- StartBatch();
- fn();
- EndBatch();
+ //TODO Make this return the return value
+ export function RunInBatch<T>(fn: () => T, batchName: string) {
+ let batch = StartBatch(batchName);
+ try {
+ return runInAction(fn);
+ } finally {
+ batch.end();
+ }
}
export const Undo = action(() => {
@@ -109,7 +170,7 @@ export namespace UndoManager {
undoing = false;
redoStack.push(commands);
- })
+ });
export const Redo = action(() => {
if (redoStack.length === 0) {
@@ -122,12 +183,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/jsx-decl.d.ts b/src/client/util/jsx-decl.d.ts
new file mode 100644
index 000000000..532f06178
--- /dev/null
+++ b/src/client/util/jsx-decl.d.ts
@@ -0,0 +1 @@
+declare module 'react-jsx-parser';
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 679f73f42..47c3481b2 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -174,13 +174,14 @@ declare class ListField<T> extends BasicField<T[]>{
Copy(): Field;
}
declare class Key extends Field {
+ constructor(name:string);
Name: string;
TrySetValue(value: any): boolean;
GetValue(): any;
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
@@ -213,3 +214,12 @@ declare class Document extends Field {
GetAllPrototypes(): Document[];
MakeDelegate(): Document;
}
+
+declare const KeyStore: {
+ [name: string]: Key;
+}
+
+// @ts-ignore
+declare const console: any;
+
+declare const Documents: any;