diff options
| author | kimdahey <claire_kim1@brown.edu> | 2020-01-16 11:31:41 -0500 |
|---|---|---|
| committer | kimdahey <claire_kim1@brown.edu> | 2020-01-16 11:31:41 -0500 |
| commit | 6be0e19ed0bd13f3796f542affa5a2e52674650c (patch) | |
| tree | 1be222ea9341ecd8020fad3149035fa650a8a07f /src/client/util | |
| parent | 5cde81d8c6b4dcd8d0796f8669b668763957f395 (diff) | |
| parent | e410cde0e430553002d4e1a2f64364b57b65fdbc (diff) | |
merged w master
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/ClientDiagnostics.ts | 34 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 56 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 414 | ||||
| -rw-r--r-- | src/client/util/DropConverter.ts | 12 | ||||
| -rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 7 | ||||
| -rw-r--r-- | src/client/util/InteractionUtils.ts | 40 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 6 | ||||
| -rw-r--r-- | src/client/util/ParagraphNodeSpec.ts | 10 | ||||
| -rw-r--r-- | src/client/util/ProseMirrorEditorView.tsx | 74 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts | 10 | ||||
| -rw-r--r-- | src/client/util/RichTextMenu.scss | 121 | ||||
| -rw-r--r-- | src/client/util/RichTextMenu.tsx | 855 | ||||
| -rw-r--r-- | src/client/util/RichTextRules.ts | 182 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 184 | ||||
| -rw-r--r-- | src/client/util/SearchUtil.ts | 10 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 7 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.scss | 4 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 1216 |
18 files changed, 1911 insertions, 1331 deletions
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts deleted file mode 100644 index 0a213aa1c..000000000 --- a/src/client/util/ClientDiagnostics.ts +++ /dev/null @@ -1,34 +0,0 @@ -export namespace ClientDiagnostics { - - export async function start() { - - let serverPolls = 0; - const serverHandle = setInterval(async () => { - if (++serverPolls === 20) { - alert("Your connection to the server has been terminated."); - clearInterval(serverHandle); - } - await fetch("/serverHeartbeat"); - serverPolls--; - }, 1000 * 15); - - let executed = false; - let solrHandle: NodeJS.Timeout | undefined; - const handler = async () => { - const response = await fetch("/solrHeartbeat"); - if (!(await response.json()).running) { - if (!executed) { - alert("Looks like SOLR is not running on your machine."); - executed = true; - solrHandle && clearInterval(solrHandle); - } - } - }; - await handler(); - if (!executed) { - solrHandle = setInterval(handler, 1000 * 15); - } - - } - -}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index d491cd1b1..fb4c2155a 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -93,33 +93,36 @@ export class DocumentManager { const toReturn: DocumentView[] = []; DocumentManager.Instance.DocumentViews.map(view => - Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); + view.props.Document === toFind && toReturn.push(view)); + DocumentManager.Instance.DocumentViews.map(view => + view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); return toReturn; } @computed public get LinkedDocumentViews() { - const pairs = DocumentManager.Instance.DocumentViews.filter(dv => - (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR - || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which - const rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors - const init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed - return init && rest; - }) - ).reduce((pairs, dv) => { - const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); - pairs.push(...linksList.reduce((pairs, link) => { - const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); - linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) { - pairs.push({ a: dv, b: docView1, l: link }); - } - }); + const pairs = DocumentManager.Instance.DocumentViews + //.filter(dv => (dv.isSelected() || Doc.IsBrushed(dv.props.Document))) // draw links from DocumentViews that are selected or brushed OR + // || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which + // const rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors + // const init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed + // return init && rest; + // } + // ) + .reduce((pairs, dv) => { + const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); + pairs.push(...linksList.reduce((pairs, link) => { + const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); + linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { + if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) { + pairs.push({ a: dv, b: docView1, l: link }); + } + }); + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); return pairs; } @@ -131,10 +134,13 @@ export class DocumentManager { finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document); }; const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc); - const annotatedDoc = await Cast(targetDoc.annotationOn, Doc); + let annotatedDoc = await Cast(docView?.props.Document.annotationOn, Doc); + if (annotatedDoc) { + const first = DocumentManager.Instance.getFirstDocumentView(annotatedDoc); + if (first) annotatedDoc = first.props.Document; + } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? - annotatedDoc && docView.props.focus(annotatedDoc, false); - docView.props.focus(docView.props.Document, willZoom); + docView.props.focus(docView.props.Document, false); highlight(); } else { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; @@ -176,7 +182,7 @@ export class DocumentManager { } public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) { - const linkDocs = link ? [link] : LinkManager.Instance.getAllRelatedLinks(doc); + const linkDocs = link ? [link] : DocListCast(doc.links); SelectionManager.DeselectAll(); const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc)); const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc)); @@ -194,6 +200,8 @@ export class DocumentManager { const target = linkFollowDocs[reverse ? 1 : 0]; target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]); DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id]); + } else if (link) { + DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined); } } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index b681387d1..df2f5fe3c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,5 +1,4 @@ -import { action, runInAction } from "mobx"; -import { Doc, Field } from "../../new_fields/Doc"; +import { Doc, Field, DocListCast } from "../../new_fields/Doc"; import { Cast, ScriptCast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -19,10 +18,10 @@ import { convertDropDataToButtons } from "./DropConverter"; export type dropActionType = "alias" | "copy" | undefined; export function SetupDrag( _reference: React.RefObject<HTMLElement>, - docFunc: () => Doc | Promise<Doc>, + docFunc: () => Doc | Promise<Doc> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, - options?: any, + treeViewId?: string, dontHideOnDrop?: boolean, dragStarted?: () => void ) { @@ -33,13 +32,15 @@ export function SetupDrag( document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); const doc = await docFunc(); - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = dropAction; - dragData.moveDocument = moveFunc; - dragData.options = options; - dragData.dontHideOnDrop = dontHideOnDrop; - DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); - dragStarted && dragStarted(); + if (doc) { + const dragData = new DragManager.DocumentDragData([doc]); + dragData.dropAction = dropAction; + dragData.moveDocument = moveFunc; + dragData.treeViewId = treeViewId; + dragData.dontHideOnDrop = dontHideOnDrop; + DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); + dragStarted && dragStarted(); + } }; const onRowUp = (): void => { document.removeEventListener("pointermove", onRowMove); @@ -50,12 +51,13 @@ export function SetupDrag( e.stopPropagation(); if (e.shiftKey && CollectionDockingView.Instance) { e.persist(); - CollectionDockingView.Instance.StartOtherDrag({ + const dragDoc = await docFunc(); + dragDoc && CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }, [await docFunc()]); + }, [dragDoc]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); @@ -65,62 +67,9 @@ export function SetupDrag( return onItemDown; } -function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - const document = SelectionManager.SelectedDocuments()[0]; - document && document.props.removeDocument && document.props.removeDocument(doc); - addDocument(doc); - return true; -} - -export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) { - const draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc); - if (draggeddoc) { - const moddrag = await Cast(draggeddoc.annotationOn, Doc); - const dragdocs = moddrag ? [moddrag] : [draggeddoc]; - const dragData = new DragManager.DocumentDragData(dragdocs); - dragData.moveDocument = moveLinkedDocument; - DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } -} - -export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) { - const srcTarg = sourceDoc.proto; - let draggedDocs: Doc[] = []; - - if (srcTarg) { - const linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg); - if (linkDocs) { - draggedDocs = linkDocs.map(link => { - const opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc); - if (opp) return opp; - }) as Doc[]; - } - } - if (draggedDocs.length) { - const moddrag: Doc[] = []; - for (const draggedDoc of draggedDocs) { - const doc = await Cast(draggedDoc.annotationOn, Doc); - if (doc) moddrag.push(doc); - } - const dragdocs = moddrag.length ? moddrag : draggedDocs; - const dragData = new DragManager.DocumentDragData(dragdocs); - dragData.moveDocument = moveLinkedDocument; - DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } -} - - export namespace DragManager { + let dragDiv: HTMLDivElement; + export function Root() { const root = document.getElementById("root"); if (!root) { @@ -128,79 +77,45 @@ export namespace DragManager { } return root; } + export let AbortDrag: () => void = emptyFunction; + export type MoveFunction = (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; - let dragDiv: HTMLDivElement; - - export enum DragButtons { - Left = 1, - Right = 2, - Both = Left | Right - } - - interface DragOptions { - handlers: DragHandlers; - - hideSource: boolean | (() => boolean); - - dragHasStarted?: () => void; - - withoutShiftDrag?: boolean; - - finishDrag?: (dropData: { [id: string]: any }) => void; - - offsetX?: number; - + export interface DragDropDisposer { (): void; } + export interface DragOptions { + dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed + hideSource?: boolean; // hide source document during drag + offsetX?: number; // offset of top left of source drag visual from cursor offsetY?: number; } - export interface DragDropDisposer { - (): void; - } - - export class DragCompleteEvent { } - - export interface DragHandlers { - dragComplete: (e: DragCompleteEvent) => void; - } - - export interface DropOptions { - handlers: DropHandlers; - } + // event called when the drag operation results in a drop action export class DropEvent { constructor( readonly x: number, readonly y: number, - readonly data: { [id: string]: any }, - readonly mods: string + readonly complete: DragCompleteEvent, + readonly altKey: boolean, + readonly metaKey: boolean, + readonly ctrlKey: boolean ) { } } - export interface DropHandlers { - drop: (e: Event, de: DropEvent) => void; - } - - 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" - ); + // event called when the drag operation has completed (aborted or completed a drop) -- this will be after any drop event has been generated + export class DragCompleteEvent { + constructor(aborted: boolean, dragData: { [id: string]: any }) { + this.aborted = aborted; + this.docDragData = dragData instanceof DocumentDragData ? dragData : undefined; + this.annoDragData = dragData instanceof PdfAnnoDragData ? dragData : undefined; + this.linkDragData = dragData instanceof LinkDragData ? dragData : undefined; + this.columnDragData = dragData instanceof ColumnDragData ? dragData : undefined; } - element.dataset.canDrop = "true"; - const handler = (e: Event) => { - const ce = e as CustomEvent<DropEvent>; - options.handlers.drop(e, ce.detail); - }; - element.addEventListener("dashOnDrop", handler); - return () => { - element.removeEventListener("dashOnDrop", handler); - delete element.dataset.canDrop; - }; + aborted: boolean; + docDragData?: DocumentDragData; + annoDragData?: PdfAnnoDragData; + linkDragData?: LinkDragData; + columnDragData?: ColumnDragData; } - export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; export class DocumentDragData { constructor(dragDoc: Doc[]) { this.draggedDocuments = dragDoc; @@ -209,6 +124,9 @@ export namespace DragManager { } draggedDocuments: Doc[]; droppedDocuments: Doc[]; + dragDivName?: string; + treeViewId?: string; + dontHideOnDrop?: boolean; offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; @@ -216,16 +134,32 @@ export namespace DragManager { moveDocument?: MoveFunction; isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts applyAsTemplate?: boolean; - [id: string]: any; } - - export class AnnotationDragData { + export class LinkDragData { + constructor(linkSourceDoc: Doc) { + this.linkSourceDocument = linkSourceDoc; + } + droppedDocuments: Doc[] = []; + linkSourceDocument: Doc; + dontClearTextBox?: boolean; + linkDocument?: Doc; + } + export class ColumnDragData { + constructor(colKey: SchemaHeaderField) { + this.colKey = colKey; + } + colKey: SchemaHeaderField; + } + // used by PDFs to conditionally (if the drop completes) create a text annotation when dragging from the PDF toolbar when a text region has been selected. + // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag + export class PdfAnnoDragData { constructor(dragDoc: Doc, annotationDoc: Doc, dropDoc: Doc) { this.dragDocument = dragDoc; this.dropDocument = dropDoc; this.annotationDocument = annotationDoc; this.offset = [0, 0]; } + linkedToDoc?: boolean; targetContext: Doc | undefined; dragDocument: Doc; annotationDocument: Doc; @@ -235,98 +169,103 @@ export namespace DragManager { userDropAction: dropActionType; } - export let StartDragFunctions: (() => void)[] = []; + export function MakeDropTarget( + element: HTMLElement, + dropFunc: (e: Event, de: DropEvent) => void + ): DragDropDisposer { + if ("canDrop" in element.dataset) { + throw new Error( + "Element is already droppable, can't make it droppable again" + ); + } + element.dataset.canDrop = "true"; + const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); + element.addEventListener("dashOnDrop", handler); + return () => { + element.removeEventListener("dashOnDrop", handler); + delete element.dataset.canDrop; + }; + } + // drag a document and drop it (or make an alias/copy on drop) export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { - runInAction(() => StartDragFunctions.map(func => func())); + const finishDrag = (e: DragCompleteEvent) => { + e.docDragData && (e.docDragData.droppedDocuments = + dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result : + dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : + dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d) + ); + e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) => + Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined)); + }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded - StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : - (dropData: { [id: string]: any }) => { - (dropData.droppedDocuments = - dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result : - dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : - dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d) - ); - dropData.droppedDocuments.forEach((drop: Doc, i: number) => - Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined)); - }); + StartDrag(eles, dragData, downX, downY, options, finishDrag); } + // drag a button template and drop a new button export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { - const dragData = new DragManager.DocumentDragData([]); - runInAction(() => StartDragFunctions.map(func => func())); - StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag : - (dropData: { [id: string]: any }) => { - const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title }); - bd.onClick = ScriptField.MakeScript(script); - params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); - initialize && initialize(bd); - bd.buttonParams = new List<string>(params); - dropData.droppedDocuments = [bd]; - }); + const finishDrag = (e: DragCompleteEvent) => { + const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title }); + bd.onClick = ScriptField.MakeScript(script); + params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); + initialize && initialize(bd); + bd.buttonParams = new List<string>(params); + e.docDragData && (e.docDragData.droppedDocuments = [bd]); + }; + StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } - export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { - dragData.moveDocument = moveLinkedDocument; + // drag links and drop link targets (aliasing them if needed) + export async function StartLinkTargetsDrag(dragEle: HTMLElement, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { + const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; + + if (draggedDocs.length) { + const moddrag: Doc[] = []; + for (const draggedDoc of draggedDocs) { + const doc = await Cast(draggedDoc.annotationOn, Doc); + if (doc) moddrag.push(doc); + } - runInAction(() => StartDragFunctions.map(func => func())); - StartDrag(eles, dragData, downX, downY, options, - (dropData: { [id: string]: any }) => { - const droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => { - const dvs = DocumentManager.Instance.getDocumentViews(d); - if (dvs.length) { - const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined; - const inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView); - if (inContext.length) { - inContext.forEach(dv => droppedDocs.push(dv.props.Document)); + const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); + dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => { + const document = SelectionManager.SelectedDocuments()[0]; + document && document.props.removeDocument && document.props.removeDocument(doc); + addDocument(doc); + return true; + }; + const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined; + const finishDrag = (e: DragCompleteEvent) => + e.docDragData && (e.docDragData.droppedDocuments = + dragData.draggedDocuments.reduce((droppedDocs, d) => { + const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView); + if (dvs.length) { + dvs.forEach(dv => droppedDocs.push(dv.props.Document)); } else { droppedDocs.push(Doc.MakeAlias(d)); } - } else { - droppedDocs.push(Doc.MakeAlias(d)); - } - return droppedDocs; - }, []); - dropData.droppedDocuments = droppedDocuments; - }); - } + return droppedDocs; + }, [] as Doc[])); - export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) { - StartDrag(eles, dragData, downX, downY, options); - } - - export class LinkDragData { - constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) { - this.linkSourceDocument = linkSourceDoc; - this.blacklist = blacklist; + StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag); } - droppedDocuments: Doc[] = []; - linkSourceDocument: Doc; - blacklist: Doc[]; - dontClearTextBox?: boolean; - [id: string]: any; } - // for column dragging in schema view - export class ColumnDragData { - constructor(colKey: SchemaHeaderField) { - this.colKey = colKey; - } - colKey: SchemaHeaderField; - [id: string]: any; + // drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption + export function StartPdfAnnoDrag(eles: HTMLElement[], dragData: PdfAnnoDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag(eles, dragData, downX, downY, options); } - export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) { - StartDrag([ele], dragData, downX, downY, options); + // drags a linker button and creates a link on drop + export function StartLinkDrag(ele: HTMLElement, sourceDoc: Doc, downX: number, downY: number, options?: DragOptions) { + StartDrag([ele], new DragManager.LinkDragData(sourceDoc), downX, downY, options); } + // drags a column from a schema view export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, 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) { + function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { eles = eles.filter(e => e); if (!dragDiv) { dragDiv = document.createElement("div"); @@ -340,33 +279,29 @@ export namespace DragManager { const xs: number[] = []; const ys: number[] = []; - const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments : - dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; + const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : []; const dragElements = eles.map(ele => { - const w = ele.offsetWidth, - h = ele.offsetHeight; + if (!ele.parentNode) dragDiv.appendChild(ele); + const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement; const rect = ele.getBoundingClientRect(); - const scaleX = rect.width / w, - scaleY = rect.height / h; - const x = rect.left, - y = rect.top; - xs.push(x); - ys.push(y); + const scaleX = rect.width / ele.offsetWidth, + scaleY = rect.height / ele.offsetHeight; + xs.push(rect.left); + ys.push(rect.top); scaleXs.push(scaleX); scaleYs.push(scaleY); - const dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; - dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; dragElement.style.position = "absolute"; dragElement.style.margin = "0"; dragElement.style.top = "0"; dragElement.style.bottom = ""; dragElement.style.left = "0"; - dragElement.style.transition = "none"; dragElement.style.color = "black"; + dragElement.style.transition = "none"; dragElement.style.transformOrigin = "0 0"; + dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; - dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; + dragElement.style.transform = `translate(${rect.left + (options?.offsetX || 0)}px, ${rect.top + (options?.offsetY || 0)}px) scale(${scaleX}, ${scaleY})`; dragElement.style.width = `${rect.width / scaleX}px`; dragElement.style.height = `${rect.height / scaleY}px`; @@ -384,31 +319,19 @@ export namespace DragManager { Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] })); }, 0); } - const set = dragElement.getElementsByTagName('*'); if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none"; + const set = dragElement.getElementsByTagName('*'); // tslint:disable-next-line: prefer-for-of for (let i = 0; i < set.length; i++) { - if (set[i].hasAttribute("style")) { - const s = set[i]; - (s as any).style.pointerEvents = "none"; - } + set[i].hasAttribute("style") && ((set[i] as any).style.pointerEvents = "none"); } - dragDiv.appendChild(dragElement); return dragElement; }); - let hideSource = false; - if (options) { - if (typeof options.hideSource === "boolean") { - hideSource = options.hideSource; - } else { - hideSource = options.hideSource(); - } - } - - eles.map(ele => ele.hidden = hideSource); + const hideSource = options?.hideSource ? true : false; + eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (ele.hidden = hideSource)); let lastX = downX; let lastY = downY; @@ -417,9 +340,9 @@ export namespace DragManager { if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey ? "alias" : undefined; } - if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) { + if (e.shiftKey && CollectionDockingView.Instance) { AbortDrag(); - finishDrag && finishDrag(dragData); + finishDrag?.(new DragCompleteEvent(true, dragData)); CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, @@ -433,56 +356,51 @@ export namespace DragManager { lastX = e.pageX; lastY = e.pageY; dragElements.map((dragElement, i) => (dragElement.style.transform = - `translate(${(xs[i] += moveX) + (options ? (options.offsetX || 0) : 0)}px, ${(ys[i] += moveY) + (options ? (options.offsetY || 0) : 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) + `translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) ); }; const hideDragShowOriginalElements = () => { dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.map(ele => ele.hidden = false); + eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false)); }; const endDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - if (options) { - options.handlers.dragComplete({}); - } }; AbortDrag = () => { hideDragShowOriginalElements(); SelectionManager.SetIsDragging(false); + options?.dragComplete?.(new DragCompleteEvent(true, dragData)); endDrag(); }; const upHandler = (e: PointerEvent) => { hideDragShowOriginalElements(); dispatchDrag(eles, e, dragData, options, finishDrag); SelectionManager.SetIsDragging(false); + options?.dragComplete?.(new DragCompleteEvent(false, dragData)); endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { + function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) { const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => { - // let parent = dragEle.parentElement; - // if (parent) parent.removeChild(dragEle); - const ret = [dragEle, dragEle.style.width, dragEle.style.height]; + const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height }; dragEle.style.width = "0"; dragEle.style.height = "0"; return ret; }); const target = document.elementFromPoint(e.x, e.y); removed.map(r => { - const 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); + r.ele.style.width = r.w; + r.ele.style.height = r.h; }); if (target) { - finishDrag && finishDrag(dragData); + const complete = new DragCompleteEvent(false, dragData); + finishDrag?.(complete); target.dispatchEvent( new CustomEvent<DropEvent>("dashOnDrop", { @@ -490,8 +408,10 @@ export namespace DragManager { detail: { x: e.x, y: e.y, - data: dragData, - mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : e.metaKey ? "MetaKey" : "" + complete: complete, + altKey: e.altKey, + metaKey: e.metaKey, + ctrlKey: e.ctrlKey } }) ); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index b2c720d5d..da0ad7efe 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -10,17 +10,17 @@ import { ScriptField } from "../../new_fields/ScriptField"; function makeTemplate(doc: Doc): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc; - const layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0]; - const fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, ""); + const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0]; + const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, ""); const docs = DocListCast(layoutDoc[fieldKey]); let any = false; - docs.map(d => { + docs.forEach(d => { if (!StrCast(d.title).startsWith("-")) { any = true; - return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)); + Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)); + } else if (d.type === DocumentType.COL) { + any = makeTemplate(d) || any; } - if (d.type === DocumentType.COL) return makeTemplate(d); - return false; }); return any; } diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index e6a215b2c..5b5bffd8c 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -122,12 +122,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => { const path = Utils.prepend(clientAccessPath); - const options = { - nativeWidth: 300, - width: 300, - title: name - }; - const document = await Docs.Get.DocumentFromType(type, path, options); + const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name }); const { data, error } = exifData; if (document) { Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data); diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts index 0c3de66ed..2e4e8c7ca 100644 --- a/src/client/util/InteractionUtils.ts +++ b/src/client/util/InteractionUtils.ts @@ -8,6 +8,17 @@ export namespace InteractionUtils { const REACT_POINTER_PEN_BUTTON = 0; const ERASER_BUTTON = 5; + export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] { + const myTouches = new Array<React.Touch>(); + for (let i = 0; i < e.targetTouches.length; i++) { + const pt = e.targetTouches.item(i); + if (pt && prevPoints.has(pt.identifier)) { + myTouches.push(pt); + } + } + return myTouches; + } + export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { switch (type) { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 @@ -79,6 +90,20 @@ export namespace InteractionUtils { return 0; } + export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: React.Touch[], leniency: number): boolean { + for (const touch of newTouches) { + if (touch) { + const oldTouch = oldTouches.get(touch.identifier); + if (oldTouch) { + if (TwoPointEuclidist(touch, oldTouch) >= leniency) { + return true; + } + } + } + } + return false; + } + // These might not be very useful anymore, but I'll leave them here for now -syip2 { @@ -134,20 +159,5 @@ export namespace InteractionUtils { // return { type: undefined }; // } // } - - // export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: TouchList, leniency: number): boolean { - // for (let i = 0; i < newTouches.length; i++) { - // let touch = newTouches.item(i); - // if (touch) { - // let oldTouch = oldTouches.get(touch.identifier); - // if (oldTouch) { - // if (TwoPointEuclidist(touch, oldTouch) >= leniency) { - // return true; - // } - // } - // } - // } - // return false; - // } } }
\ No newline at end of file diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index fb6f27478..5f3667acc 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -70,7 +70,7 @@ export class LinkManager { } // finds all links that contain the given anchor - public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> { + public getAllRelatedLinks(anchor: Doc): Doc[] { const related = LinkManager.Instance.getAllLinks().filter(link => { const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null)); const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); @@ -244,7 +244,5 @@ export class LinkManager { if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } } -Scripting.addGlobal(function links(doc: any) { - return new List(LinkManager.Instance.getAllRelatedLinks(doc)); -}); +Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); });
\ No newline at end of file diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts index 3a993e1ff..0a3b68217 100644 --- a/src/client/util/ParagraphNodeSpec.ts +++ b/src/client/util/ParagraphNodeSpec.ts @@ -34,6 +34,7 @@ const ParagraphNodeSpec: NodeSpec = { color: { default: null }, id: { default: null }, indent: { default: null }, + inset: { default: null }, lineSpacing: { default: null }, // TODO: Add UI to let user edit / clear padding. paddingBottom: { default: null }, @@ -76,6 +77,7 @@ function toDOM(node: Node): DOMOutputSpec { const { align, indent, + inset, lineSpacing, paddingTop, paddingBottom, @@ -105,6 +107,14 @@ function toDOM(node: Node): DOMOutputSpec { style += `padding-bottom: ${paddingBottom};`; } + if (indent) { + style += `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};`; + } + + if (inset) { + style += `margin-left: ${inset}; margin-right: ${inset};`; + } + style && (attrs.style = style); if (indent) { diff --git a/src/client/util/ProseMirrorEditorView.tsx b/src/client/util/ProseMirrorEditorView.tsx new file mode 100644 index 000000000..b42adfbb4 --- /dev/null +++ b/src/client/util/ProseMirrorEditorView.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { EditorView } from "prosemirror-view"; +import { EditorState } from "prosemirror-state"; + +export interface ProseMirrorEditorViewProps { + /* EditorState instance to use. */ + editorState: EditorState; + /* Called when EditorView produces new EditorState. */ + onEditorState: (editorState: EditorState) => any; +} + +/** + * This wraps ProseMirror's EditorView into React component. + * This code was found on https://discuss.prosemirror.net/t/using-with-react/904 + */ +export class ProseMirrorEditorView extends React.Component<ProseMirrorEditorViewProps> { + + private _editorView?: EditorView; + + _createEditorView = (element: HTMLDivElement | null) => { + if (element !== null) { + this._editorView = new EditorView(element, { + state: this.props.editorState, + dispatchTransaction: this.dispatchTransaction, + }); + } + } + + dispatchTransaction = (tx: any) => { + // In case EditorView makes any modification to a state we funnel those + // modifications up to the parent and apply to the EditorView itself. + const editorState = this.props.editorState.apply(tx); + if (this._editorView) { + this._editorView.updateState(editorState); + } + this.props.onEditorState(editorState); + } + + focus() { + if (this._editorView) { + this._editorView.focus(); + } + } + + componentWillReceiveProps(nextProps: { editorState: EditorState<any>; }) { + // In case we receive new EditorState through props — we apply it to the + // EditorView instance. + if (this._editorView) { + if (nextProps.editorState !== this.props.editorState) { + this._editorView.updateState(nextProps.editorState); + } + } + } + + componentWillUnmount() { + if (this._editorView) { + this._editorView.destroy(); + } + } + + shouldComponentUpdate() { + // Note that EditorView manages its DOM itself so we'd ratrher don't mess + // with it. + return false; + } + + render() { + // Render just an empty div which is then used as a container for an + // EditorView instance. + return ( + <div ref={this._createEditorView} /> + ); + } +}
\ No newline at end of file diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index f1fa6f11d..c028dbf8b 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -6,6 +6,8 @@ import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { TooltipTextMenu } from "./TooltipTextMenu"; +import { SelectionManager } from "./SelectionManager"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -46,7 +48,11 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Alt-ArrowUp", joinUp); bind("Alt-ArrowDown", joinDown); bind("Mod-BracketLeft", lift); - bind("Escape", selectParentNode); + bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as any).blur?.(); + SelectionManager.DeselectAll(); + }); bind("Mod-b", toggleMark(schema.marks.strong)); bind("Mod-B", toggleMark(schema.marks.strong)); @@ -105,8 +111,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: return true; }); - bind("Mod-s", TooltipTextMenu.insertStar); - bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const ref = state.selection; const range = ref.$from.blockRange(ref.$to); diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss new file mode 100644 index 000000000..43cc23ecd --- /dev/null +++ b/src/client/util/RichTextMenu.scss @@ -0,0 +1,121 @@ +@import "../views/globalCssVariables"; + +.button-dropdown-wrapper { + position: relative; + + .dropdown-button { + width: 15px; + padding-left: 5px; + padding-right: 5px; + } + + .dropdown-button-combined { + width: 50px; + display: flex; + justify-content: space-between; + + svg:nth-child(2) { + margin-top: 2px; + } + } + + .dropdown { + position: absolute; + top: 35px; + left: 0; + background-color: #323232; + color: $light-color-secondary; + border: 1px solid #4d4d4d; + border-radius: 0 6px 6px 6px; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + min-width: 150px; + padding: 5px; + font-size: 12px; + z-index: 10001; + + button { + background-color: #323232; + border: 1px solid black; + border-radius: 1px; + padding: 6px; + margin: 5px 0; + font-size: 10px; + + &:hover { + background-color: black; + } + + &:last-child { + margin-bottom: 0; + } + } + } + + input { + color: black; + } +} + +.link-menu { + .divider { + background-color: white; + height: 1px; + width: 100%; + } +} + +.color-preview-button { + .color-preview { + width: 100%; + height: 3px; + margin-top: 3px; + } +} + +.color-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + button.color-button { + width: 20px; + height: 20px; + border-radius: 15px !important; + margin: 3px; + border: 2px solid transparent !important; + padding: 3px; + + &.active { + border: 2px solid white !important; + } + } +} + +select { + background-color: #323232; + color: white; + border: 1px solid black; + // border-top: none; + // border-bottom: none; + font-size: 12px; + height: 100%; + margin-right: 3px; + + &:focus, + &:hover { + background-color: black; + } + + &::-ms-expand { + color: white; + } +} + +.row-2 { + display: flex; + justify-content: space-between; + + >div { + display: flex; + } +}
\ No newline at end of file diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx new file mode 100644 index 000000000..419d7caf9 --- /dev/null +++ b/src/client/util/RichTextMenu.tsx @@ -0,0 +1,855 @@ +import React = require("react"); +import AntimodeMenu from "../views/AntimodeMenu"; +import { observable, action, } from "mobx"; +import { observer } from "mobx-react"; +import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model"; +import { schema } from "./RichTextSchema"; +import { EditorView } from "prosemirror-view"; +import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons"; +import { MenuItem, Dropdown } from "prosemirror-menu"; +import { updateBullets } from "./ProsemirrorExampleTransfer"; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { NumCast, Cast, StrCast } from "../../new_fields/Types"; +import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; +import { unimplementedFunction, Utils } from "../../Utils"; +import { wrapInList } from "prosemirror-schema-list"; +import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField'; +import "./RichTextMenu.scss"; +import { DocServer } from "../DocServer"; +import { Doc } from "../../new_fields/Doc"; +import { SelectionManager } from "./SelectionManager"; +import { LinkManager } from "./LinkManager"; +const { toggleMark, setBlockType } = require("prosemirror-commands"); + +library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); + +@observer +export default class RichTextMenu extends AntimodeMenu { + static Instance: RichTextMenu; + public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable + + private view?: EditorView; + private editorProps: FieldViewProps & FormattedTextBoxProps | undefined; + + private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[]; + private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[]; + private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[]; + private fontColors: (string | undefined)[]; + private highlightColors: (string | undefined)[]; + + @observable private boldActive: boolean = false; + @observable private italicsActive: boolean = false; + @observable private underlineActive: boolean = false; + @observable private strikethroughActive: boolean = false; + @observable private subscriptActive: boolean = false; + @observable private superscriptActive: boolean = false; + + @observable private activeFontSize: string = ""; + @observable private activeFontFamily: string = ""; + @observable private activeListType: string = ""; + + @observable private brushIsEmpty: boolean = true; + @observable private brushMarks: Set<Mark> = new Set(); + @observable private showBrushDropdown: boolean = false; + + @observable private activeFontColor: string = "black"; + @observable private showColorDropdown: boolean = false; + + @observable private activeHighlightColor: string = "transparent"; + @observable private showHighlightDropdown: boolean = false; + + @observable private currentLink: string | undefined = ""; + @observable private showLinkDropdown: boolean = false; + + constructor(props: Readonly<{}>) { + super(props); + RichTextMenu.Instance = this; + this._canFade = false; + + this.fontSizeOptions = [ + { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "8pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize }, + { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true }, + { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option + ]; + + this.fontFamilyOptions = [ + { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } }, + { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } }, + { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } }, + { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } }, + { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } }, + { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } }, + { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } }, + { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true }, + // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true }, + ]; + + this.listTypeOptions = [ + { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, + { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, + { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType }, + { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, + ]; + + this.fontColors = [ + DarkPastelSchemaPalette.get("pink2"), + DarkPastelSchemaPalette.get("purple4"), + DarkPastelSchemaPalette.get("bluegreen1"), + DarkPastelSchemaPalette.get("yellow4"), + DarkPastelSchemaPalette.get("red2"), + DarkPastelSchemaPalette.get("bluegreen7"), + DarkPastelSchemaPalette.get("bluegreen5"), + DarkPastelSchemaPalette.get("orange1"), + "#757472", + "#000" + ]; + + this.highlightColors = [ + PastelSchemaPalette.get("pink2"), + PastelSchemaPalette.get("purple4"), + PastelSchemaPalette.get("bluegreen1"), + PastelSchemaPalette.get("yellow4"), + PastelSchemaPalette.get("red2"), + PastelSchemaPalette.get("bluegreen7"), + PastelSchemaPalette.get("bluegreen5"), + PastelSchemaPalette.get("orange1"), + "white", + "transparent" + ]; + } + + @action + changeView(view: EditorView) { + this.view = view; + } + + update(view: EditorView, lastState: EditorState | undefined) { + this.updateFromDash(view, lastState, this.editorProps); + } + + @action + public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { + if (!view) { + console.log("no editor? why?"); + return; + } + this.view = view; + const state = view.state; + props && (this.editorProps = props); + + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; + + // update active marks + const activeMarks = this.getActiveMarksOnSelection(); + this.setActiveMarkButtons(activeMarks); + + // update active font family and size + const active = this.getActiveFontStylesOnSelection(); + const activeFamilies = active && active.get("families"); + const activeSizes = active && active.get("sizes"); + + this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; + this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various"; + + // update link in current selection + const targetTitle = await this.getTextLinkTargetTitle(); + this.setCurrentLink(targetTitle); + } + + setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => { + if (mark) { + const node = (state.selection as NodeSelection).node; + if (node?.type === schema.nodes.ordered_list) { + let attrs = node.attrs; + if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize }; + if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color }; + const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); + dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); + } else { + toggleMark(mark.type, mark.attrs)(state, (tx: any) => { + const { from, $from, to, empty } = tx.selection; + if (!tx.doc.rangeHasMark(from, to, mark.type)) { + toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); + } else dispatch(tx); + }); + } + } + } + + // finds font sizes and families in selection + getActiveFontStylesOnSelection() { + if (!this.view) return; + + const activeFamilies: string[] = []; + const activeSizes: string[] = []; + const state = this.view.state; + const pos = this.view.state.selection.$from; + const ref_node = this.reference_node(pos); + if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { + ref_node.marks.forEach(m => { + m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); + m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); + }); + } + + const styles = new Map<String, String[]>(); + styles.set("families", activeFamilies); + styles.set("sizes", activeSizes); + return styles; + } + + getMarksInSelection(state: EditorState<any>) { + const found = new Set<Mark>(); + const { from, to } = state.selection as TextSelection; + state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m))); + return found; + } + + //finds all active marks on selection in given group + getActiveMarksOnSelection() { + if (!this.view) return; + + const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; + if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); + //current selection + const { empty, ranges, $to } = this.view.state.selection as TextSelection; + const state = this.view.state; + let activeMarks: MarkType[] = []; + if (!empty) { + activeMarks = markGroup.filter(mark => { + const has = false; + for (let i = 0; !has && i < ranges.length; i++) { + return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark); + } + return false; + }); + } + else { + const pos = this.view.state.selection.$from; + const ref_node: ProsNode | null = this.reference_node(pos); + if (ref_node !== null && ref_node !== this.view.state.doc) { + if (ref_node.isText) { + } + else { + return []; + } + activeMarks = markGroup.filter(mark_type => { + if (mark_type === state.schema.marks.pFontSize) { + return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); + } + const mark = state.schema.mark(mark_type); + return ref_node.marks.includes(mark); + }); + } + } + return activeMarks; + } + + destroy() { + } + + @action + setActiveMarkButtons(activeMarks: MarkType[] | undefined) { + if (!activeMarks) return; + + this.boldActive = false; + this.italicsActive = false; + this.underlineActive = false; + this.strikethroughActive = false; + this.subscriptActive = false; + this.superscriptActive = false; + + activeMarks.forEach(mark => { + switch (mark.name) { + case "strong": this.boldActive = true; break; + case "em": this.italicsActive = true; break; + case "underline": this.underlineActive = true; break; + case "strikethrough": this.strikethroughActive = true; break; + case "subscript": this.subscriptActive = true; break; + case "superscript": this.superscriptActive = true; break; + } + }); + } + + createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) { + const self = this; + function onClick(e: React.PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.view && command && command(self.view.state, self.view.dispatch, self.view); + self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); + self.setActiveMarkButtons(self.getActiveMarksOnSelection()); + } + + return ( + <button className={"antimodeMenu-button" + (isActive ? " active" : "")} title={title} onPointerDown={onClick}> + <FontAwesomeIcon icon={faIcon as IconProp} size="lg" /> + </button> + ); + } + + createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[]): JSX.Element { + const items = options.map(({ title, label, hidden, style }) => { + if (hidden) { + return label === activeOption ? + <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> : + <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>; + } + return label === activeOption ? + <option value={label} title={title} style={style ? style : {}} selected>{label}</option> : + <option value={label} title={title} style={style ? style : {}}>{label}</option>; + }); + + const self = this; + function onChange(e: React.ChangeEvent<HTMLSelectElement>) { + e.stopPropagation(); + e.preventDefault(); + options.forEach(({ label, mark, command }) => { + if (e.target.value === label) { + self.view && mark && command(mark, self.view); + } + }); + } + return <select onChange={onChange}>{items}</select>; + } + + createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[]): JSX.Element { + const items = options.map(({ title, label, hidden, style }) => { + if (hidden) { + return label === activeOption ? + <option value={label} title={title} style={style ? style : {}} selected hidden>{label}</option> : + <option value={label} title={title} style={style ? style : {}} hidden>{label}</option>; + } + return label === activeOption ? + <option value={label} title={title} style={style ? style : {}} selected>{label}</option> : + <option value={label} title={title} style={style ? style : {}}>{label}</option>; + }); + + const self = this; + function onChange(val: string) { + options.forEach(({ label, node, command }) => { + if (val === label) { + self.view && node && command(node); + } + }); + } + return <select onChange={e => onChange(e.target.value)}>{items}</select>; + } + + changeFontSize = (mark: Mark, view: EditorView) => { + const size = mark.attrs.fontSize; + if (this.editorProps) { + const ruleProvider = this.editorProps.ruleProvider; + const heading = NumCast(this.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleSize_" + heading] = size; + } + } + this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: size }), view.state, view.dispatch); + } + + changeFontFamily = (mark: Mark, view: EditorView) => { + const fontName = mark.attrs.family; + if (this.editorProps) { + const ruleProvider = this.editorProps.ruleProvider; + const heading = NumCast(this.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleFont_" + heading] = fontName; + } + } + this.setMark(view.state.schema.marks.pFontFamily.create({ family: fontName }), view.state, view.dispatch); + } + + // TODO: remove doesn't work + //remove all node type and apply the passed-in one to the selected text + changeListType = (nodeType: NodeType | undefined) => { + if (!this.view) return; + + if (nodeType === schema.nodes.bullet_list) { + wrapInList(nodeType)(this.view.state, this.view.dispatch); + } else { + const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); + if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + this.view!.dispatch(tx2); + })) { + const tx2 = this.view.state.tr; + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + this.view.dispatch(tx3); + } + } + } + + insertSummarizer(state: EditorState<any>, dispatch: any) { + if (state.selection.empty) return false; + const mark = state.schema.marks.summarize.create(); + const tr = state.tr; + tr.addMark(state.selection.from, state.selection.to, mark); + const content = tr.selection.content(); + const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); + dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + return true; + } + + @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; } + + createBrushButton() { + const self = this; + function onBrushClick(e: React.PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.view && self.fillBrush(self.view.state, self.view.dispatch); + } + + let label = "Stored marks: "; + if (this.brushMarks && this.brushMarks.size > 0) { + this.brushMarks.forEach((mark: Mark) => { + const markType = mark.type; + label += markType.name; + label += ", "; + }); + label = label.substring(0, label.length - 2); + } else { + label = "No marks are currently stored"; + } + + const button = + <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}> + <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} /> + </button>; + + const dropdownContent = + <div className="dropdown"> + <p>{label}</p> + <button onPointerDown={this.clearBrush}>Clear brush</button> + {/* <input placeholder="Enter URL"></input> */} + </div>; + + return ( + <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} /> + ); + } + + @action + clearBrush() { + RichTextMenu.Instance.brushIsEmpty = true; + RichTextMenu.Instance.brushMarks = new Set(); + } + + @action + fillBrush(state: EditorState<any>, dispatch: any) { + if (!this.view) return; + + if (this.brushIsEmpty) { + const selected_marks = this.getMarksInSelection(this.view.state); + if (selected_marks.size >= 0) { + this.brushMarks = selected_marks; + this.brushIsEmpty = !this.brushIsEmpty; + } + } + else { + const { from, to, $from } = this.view.state.selection; + if (!this.view.state.selection.empty && $from && $from.nodeAfter) { + if (this.brushMarks && to - from > 0) { + this.view.dispatch(this.view.state.tr.removeMark(from, to)); + Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => { + this.setMark(mark, this.view!.state, this.view!.dispatch); + }); + } + } + else { + this.brushIsEmpty = !this.brushIsEmpty; + } + } + } + + @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } + @action setActiveColor(color: string) { this.activeFontColor = color; } + + createColorButton() { + const self = this; + function onColorClick(e: React.PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + } + function changeColor(e: React.PointerEvent, color: string) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.setActiveColor(color); + self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + } + + const button = + <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}> + <FontAwesomeIcon icon="palette" size="lg" /> + <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div> + </button>; + + const dropdownContent = + <div className="dropdown"> + <p>Change font color:</p> + <div className="color-wrapper"> + {this.fontColors.map(color => { + if (color) { + return this.activeFontColor === color ? + <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> : + <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>; + } + })} + </div> + </div>; + + return ( + <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} /> + ); + } + + public insertColor(color: String, state: EditorState<any>, dispatch: any) { + const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color }); + if (state.selection.empty) { + dispatch(state.tr.addStoredMark(colorMark)); + return false; + } + this.setMark(colorMark, state, dispatch); + } + + @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; } + @action setActiveHighlight(color: string) { this.activeHighlightColor = color; } + + createHighlighterButton() { + const self = this; + function onHighlightClick(e: React.PointerEvent) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + } + function changeHighlight(e: React.PointerEvent, color: string) { + e.preventDefault(); + e.stopPropagation(); + self.view && self.view.focus(); + self.setActiveHighlight(color); + self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + } + + const button = + <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onHighlightClick}> + <FontAwesomeIcon icon="highlighter" size="lg" /> + <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div> + </button>; + + const dropdownContent = + <div className="dropdown"> + <p>Change highlight color:</p> + <div className="color-wrapper"> + {this.highlightColors.map(color => { + if (color) { + return this.activeHighlightColor === color ? + <button className="color-button active" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> : + <button className="color-button" style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>; + } + })} + </div> + </div>; + + return ( + <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} /> + ); + } + + insertHighlight(color: String, state: EditorState<any>, dispatch: any) { + if (state.selection.empty) return false; + toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch); + } + + @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; } + @action setCurrentLink(link: string) { this.currentLink = link; } + + createLinkButton() { + const self = this; + + function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) { + self.setCurrentLink(e.target.value); + } + + const link = this.currentLink ? this.currentLink : ""; + + const button = <FontAwesomeIcon icon="link" size="lg" />; + + const dropdownContent = + <div className="dropdown link-menu"> + <p>Linked to:</p> + <input value={link} placeholder="Enter URL" onChange={onLinkChange} /> + <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button> + <div className="divider"></div> + <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> + </div>; + + return ( + <ButtonDropdown view={this.view} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> + ); + } + + async getTextLinkTargetTitle() { + if (!this.view) return; + + const node = this.view.state.selection.$from.nodeAfter; + const link = node && node.marks.find(m => m.type.name === "link"); + if (link) { + const href = link.attrs.href; + if (href) { + if (href.indexOf(Utils.prepend("/doc/")) === 0) { + const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (linkclicked) { + const linkDoc = await DocServer.GetRefField(linkclicked); + if (linkDoc instanceof Doc) { + const anchor1 = await Cast(linkDoc.anchor1, Doc); + const anchor2 = await Cast(linkDoc.anchor2, Doc); + const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document; + if (currentDoc && anchor1 && anchor2) { + if (Doc.AreProtosEqual(currentDoc, anchor1)) { + return StrCast(anchor2.title); + } + if (Doc.AreProtosEqual(currentDoc, anchor2)) { + return StrCast(anchor1.title); + } + } + } + } + } else { + return href; + } + } else { + return link.attrs.title; + } + } + } + + // TODO: should check for valid URL + makeLinkToURL = (target: String, lcoation: string) => { + if (!this.view) return; + + let node = this.view.state.selection.$from.nodeAfter; + let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); + 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"); + } + + deleteLink = () => { + if (!this.view) return; + + const node = this.view.state.selection.$from.nodeAfter; + const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link); + const href = link!.attrs.href; + if (href) { + if (href.indexOf(Utils.prepend("/doc/")) === 0) { + const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (linkclicked) { + DocServer.GetRefField(linkclicked).then(async linkDoc => { + if (linkDoc instanceof Doc) { + LinkManager.Instance.deleteLink(linkDoc); + this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link)); + } + }); + } + } else { + if (node) { + const { tr, schema, selection } = this.view.state; + const extension = this.linkExtend(selection.$anchor, href); + this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link)); + } + } + } + } + + linkExtend($start: ResolvedPos, href: string) { + const mark = this.view!.state.schema.marks.link; + + let startIndex = $start.index(); + let endIndex = $start.indexAfter(); + + while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--; + while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++; + + let startPos = $start.start(); + let endPos = startPos; + for (let i = 0; i < endIndex; i++) { + const size = $start.parent.child(i).nodeSize; + if (i < startIndex) startPos += size; + endPos += size; + } + return { from: startPos, to: endPos }; + } + + reference_node(pos: ResolvedPos<any>): ProsNode | null { + if (!this.view) return null; + + let ref_node: ProsNode = this.view.state.doc; + if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { + ref_node = pos.nodeBefore; + } + else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { + ref_node = pos.nodeAfter; + } + else if (pos.pos > 0) { + let skip = false; + for (let i: number = pos.pos - 1; i > 0; i--) { + this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { + if (node.isLeaf && !skip) { + ref_node = node; + skip = true; + } + + }); + } + } + if (!ref_node.isLeaf && ref_node.childCount > 0) { + ref_node = ref_node.child(0); + } + return ref_node; + } + + @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; } + @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } + + @action + toggleMenuPin = (e: React.MouseEvent) => { + this.Pinned = !this.Pinned; + if (!this.Pinned) { + this.fadeOut(true); + } + } + + render() { + + const row1 = <div className="antimodeMenu-row">{[ + this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + this.createColorButton(), + this.createHighlighterButton(), + this.createLinkButton(), + this.createBrushButton(), + this.createButton("indent", "Summarize", undefined, this.insertSummarizer), + ]}</div>; + + const row2 = <div className="antimodeMenu-row row-2"> + <div> + {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions), + this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions), + this.createNodesDropdown(this.activeListType, this.listTypeOptions)]} + </div> + <div> + <button className="antimodeMenu-button" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> + </button> + {this.getDragger()} + </div> + </div>; + + return ( + <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.getElementWithRows([row1, row2], 2, false)} + </div> + ); + } +} + +interface ButtonDropdownProps { + view?: EditorView; + button: JSX.Element; + dropdownContent: JSX.Element; + openDropdownOnButton?: boolean; +} + +@observer +class ButtonDropdown extends React.Component<ButtonDropdownProps> { + + @observable private showDropdown: boolean = false; + private ref: HTMLDivElement | null = null; + + componentDidMount() { + document.addEventListener("pointerdown", this.onBlur); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onBlur); + } + + @action + setShowDropdown(show: boolean) { + this.showDropdown = show; + } + @action + toggleDropdown() { + this.showDropdown = !this.showDropdown; + } + + onDropdownClick = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.props.view && this.props.view.focus(); + this.toggleDropdown(); + } + + onBlur = (e: PointerEvent) => { + setTimeout(() => { + if (this.ref !== null && !this.ref.contains(e.target as Node)) { + this.setShowDropdown(false); + } + }, 0); + } + + render() { + return ( + <div className="button-dropdown-wrapper" ref={node => this.ref = node}> + {this.props.openDropdownOnButton ? + <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> + {this.props.button} + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> : + <> + {this.props.button} + <button className="dropdown-button antimodeMenu-button" onPointerDown={this.onDropdownClick}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> + </>} + + {this.showDropdown ? this.props.dropdownContent : <></>} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index bf365579a..29b378299 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -5,8 +5,11 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { NumCast, Cast } from "../../new_fields/Types"; import { Doc } from "../../new_fields/Doc"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { Docs } from "../documents/Documents"; +import { TooltipTextMenuManager } from "../util/TooltipTextMenu"; +import { Docs, DocUtils } from "../documents/Documents"; import { Id } from "../../new_fields/FieldSymbols"; +import { DocServer } from "../DocServer"; +import { returnFalse, Utils } from "../../Utils"; export const inpRules = { rules: [ @@ -59,53 +62,121 @@ export const inpRules = { } ), + // set the font size using #<font-size> new InputRule( - new RegExp(/^#([0-9]+)\s$/), + new RegExp(/^%([0-9]+)\s$/), (state, match, start, end) => { const size = Number(match[1]); - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { - (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = size; + (Cast(FormattedTextBox.FocusedBox!.props.Document, Doc) as Doc).heading = size; return state.tr.deleteRange(start, end); } return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); }), + + // make current selection a hyperlink portal (assumes % was used to initiate an EnteringStyle mode) new InputRule( - new RegExp(/t/), + new RegExp(/@$/), (state, match, start, end) => { - if (state.selection.to === state.selection.from) return null; - const node = (state.doc.resolve(start) as any).nodeAfter; - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null; + + const value = state.doc.textBetween(start, end); + if (value) { + DocServer.GetRefField(value).then(docx => { + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500, }, value); + DocUtils.Publish(target, value, returnFalse, returnFalse); + DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", ""); + }); + const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + value), location: "onRight", title: value, targetId: value }); + return state.tr.addMark(start, end, link); + } + return state.tr; }), + // stop using active style new InputRule( - new RegExp(/i/), + new RegExp(/%%$/), (state, match, start, end) => { - if (state.selection.to === state.selection.from) return null; - const node = (state.doc.resolve(start) as any).nodeAfter; - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + const tr = state.tr.deleteRange(start, end); + const marks = state.tr.selection.$anchor.nodeBefore?.marks; + return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr; }), + + // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) new InputRule( - new RegExp(/\!/), + new RegExp(/[ti!x]$/), (state, match, start, end) => { - if (state.selection.to === state.selection.from) return null; + if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null; + const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??"; const node = (state.doc.resolve(start) as any).nodeAfter; - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), + + // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/(%d|d)$/), + (state, match, start, end) => { + if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null; + const pos = (state.doc.resolve(start) as any); + for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + const node = pos.node(depth); + if (node.type === schema.nodes.paragraph) { + const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 }); + const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start))); + return match[0].startsWith("%") ? result.deleteRange(start, end) : result; + } + } + return null; }), + + // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( - new RegExp(/\x/), + new RegExp(/(%h|h)$/), (state, match, start, end) => { - if (state.selection.to === state.selection.from) return null; - const node = (state.doc.resolve(start) as any).nodeAfter; - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null; + const pos = (state.doc.resolve(start) as any); + for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + const node = pos.node(depth); + if (node.type === schema.nodes.paragraph) { + const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 }); + const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start))); + return match[0].startsWith("%") ? result.deleteRange(start, end) : result; + } + } + return null; }), + // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( - new RegExp(/^\^\^\s$/), + new RegExp(/(%q|q)$/), + (state, match, start, end) => { + if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null; + const pos = (state.doc.resolve(start) as any); + if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) { + const node = state.selection.node; + return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 }); + } + for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + const node = pos.node(depth); + if (node.type === schema.nodes.paragraph) { + const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 }); + const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start))); + return match[0].startsWith("%") ? result.deleteRange(start, end) : result; + } + } + return null; + }), + + + // center justify text + new InputRule( + new RegExp(/%\^$/), (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "center"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; @@ -114,13 +185,14 @@ export const inpRules = { state.tr; return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), + // left justify text new InputRule( - new RegExp(/^\[\[\s$/), + new RegExp(/%\[$/), (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "left"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; @@ -129,13 +201,14 @@ export const inpRules = { state.tr; return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), + // right justify text new InputRule( - new RegExp(/^\]\]\s$/), + new RegExp(/%\]$/), (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; const sm = state.storedMarks || undefined; - const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider; - const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading); + const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider; + const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading); if (ruleProvider && heading) { ruleProvider["ruleAlign_" + heading] = "right"; return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; @@ -145,38 +218,38 @@ export const inpRules = { return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); }), new InputRule( - new RegExp(/##\s$/), + new RegExp(/%#$/), (state, match, start, end) => { - const target = Docs.Create.TextDocument({ width: 75, height: 35, backgroundColor: "yellow", autoHeight: true, fontSize: 9, title: "inline comment" }); + const target = Docs.Create.TextDocument({ width: 75, height: 35, backgroundColor: "yellow", annotationOn: FormattedTextBox.FocusedBox!.dataDoc, autoHeight: true, fontSize: 9, title: "inline comment" }); const node = (state.doc.resolve(start) as any).nodeAfter; const newNode = schema.nodes.dashComment.create({ docid: target[Id] }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" }); const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; return replaced;//.setSelection(new NodeSelection(replaced.doc.resolve(end))); }), new InputRule( - new RegExp(/\(\(/), + new RegExp(/%\(/), (state, match, start, end) => { const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const mark = state.schema.marks.highlight.create(); + const sm = state.storedMarks || []; + const mark = state.schema.marks.summarizeInclusive.create(); + sm.push(mark); const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark); const content = selected.selection.content(); - const replaced = node ? selected.replaceRangeWith(start, start, - schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + const replaced = node ? selected.replaceRangeWith(start, end, + schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))); + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); }), new InputRule( - new RegExp(/\)\)/), + new RegExp(/%\)/), (state, match, start, end) => { - const mark = state.schema.marks.highlight.create(); - return state.tr.removeStoredMark(mark); + return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); }), new InputRule( - new RegExp(/\^f\s$/), + new RegExp(/%f$/), (state, match, start, end) => { const newNode = schema.nodes.footnote.create({}); const tr = state.tr; @@ -185,9 +258,26 @@ export const inpRules = { tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))); }), - // let newNode = schema.nodes.footnote.create({}); - // if (dispatch && state.selection.from === state.selection.to) { - // return true; - // } + + // activate a style by name using prefix '%' + new InputRule( + new RegExp(/%[a-z]+$/), + (state, match, start, end) => { + const color = match[0].substring(1, match[0].length); + const marks = TooltipTextMenuManager.Instance._brushMap.get(color); + if (marks) { + const tr = state.tr.deleteRange(start, end); + return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr; + } + const isValidColor = (strColor: string) => { + const s = new Option().style; + s.color = strColor; + return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned + }; + if (isValidColor(color)) { + return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color })); + } + return null; + }), ] }; diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 3b786e61d..ef90a7294 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -16,11 +16,10 @@ import { DocumentManager } from "./DocumentManager"; import ParagraphNodeSpec from "./ParagraphNodeSpec"; import { Transform } from "./Transform"; import React = require("react"); -import { BoolCast, NumCast } from "../../new_fields/Types"; +import { BoolCast, NumCast, Cast } from "../../new_fields/Types"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { any } from "bluebird"; -const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], +const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; // :: Object @@ -31,7 +30,6 @@ export const nodes: { [index: string]: NodeSpec } = { content: "block+" }, - footnote: { group: "inline", content: "inline*", @@ -46,15 +44,6 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [{ tag: "footnote" }] }, - // // :: NodeSpec A plain paragraph textblock. Represented in the DOM - // // as a `<p>` element. - // paragraph: { - // content: "inline*", - // group: "block", - // parseDOM: [{ tag: "p" }], - // toDOM() { return pDOM; } - // }, - paragraph: ParagraphNodeSpec, // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. @@ -120,7 +109,7 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, - star: { + summary: { inline: true, attrs: { visibility: { default: false }, @@ -132,15 +121,6 @@ export const nodes: { [index: string]: NodeSpec } = { const attrs = { style: `width: 40px` }; return ["span", { ...node.attrs, ...attrs }]; }, - // parseDOM: [{ - // tag: "star", getAttrs(dom: any) { - // return { - // visibility: dom.getAttribute("visibility"), - // oldtext: dom.getAttribute("oldtext"), - // oldtextlen: dom.getAttribute("oldtextlen"), - // } - // } - // }] }, // :: NodeSpec An inline image (`<img>`) node. Supports `src`, @@ -188,18 +168,7 @@ export const nodes: { [index: string]: NodeSpec } = { docid: { default: "" }, }, 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, dragging the image crashes. Why? + draggable: false, toDOM(node) { const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` }; return ["div", { ...node.attrs, ...attrs }]; @@ -249,20 +218,21 @@ export const nodes: { [index: string]: NodeSpec } = { bulletStyle: { default: 0 }, mapStyle: { default: "decimal" }, setFontSize: { default: undefined }, - setFontFamily: { default: undefined }, + setFontFamily: { default: "inherit" }, + setFontColor: { default: "inherit" }, inheritedFontSize: { default: undefined }, - visibility: { default: true } + visibility: { default: true }, + indent: { default: undefined } }, toDOM(node: Node<any>) { - const bs = node.attrs.bulletStyle; if (node.attrs.mapStyle === "bullet") return ['ul', 0]; - const decMap = bs ? "decimal" + bs : ""; - const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; - const map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; + const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize; const ffam = node.attrs.setFontFamily; - return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] : - ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }]; + const color = node.attrs.setFontColor; + return node.attrs.visibility ? + ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] : + ['ol', { class: `${map}-ol`, style: `list-style: none;` }]; } }, @@ -285,10 +255,7 @@ export const nodes: { [index: string]: NodeSpec } = { ...listItem, content: 'paragraph block*', toDOM(node: any) { - const bs = node.attrs.bulletStyle; - const decMap = bs ? "decimal" + bs : ""; - const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; - const map = node.attrs.mapStyle === "decimal" ? decMap : node.attrs.mapStyle === "multi" ? multiMap : ""; + const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."]; //return ["li", { class: `${map}` }, 0]; } @@ -307,6 +274,8 @@ export const marks: { [index: string]: MarkSpec } = { link: { attrs: { href: {}, + targetId: { default: "" }, + showPreview: { default: true }, location: { default: null }, title: { default: null }, docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text @@ -314,22 +283,23 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") }; + return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") }; } }], toDOM(node: any) { return node.attrs.docref && node.attrs.title ? ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] : - ["a", { ...node.attrs, title: `${node.attrs.title}` }, 0]; + ["a", { ...node.attrs, id: node.attrs.targetId, title: `${node.attrs.title}` }, 0]; } }, + // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text. - color: { + pFontColor: { attrs: { color: { default: "#000" } }, - inclusive: false, + inclusive: true, parseDOM: [{ tag: "span", getAttrs(dom: any) { return { color: dom.getAttribute("color") }; @@ -344,7 +314,7 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { highlight: { default: "transparent" } }, - inclusive: false, + inclusive: true, parseDOM: [{ tag: "span", getAttrs(dom: any) { return { highlight: dom.getAttribute("backgroundColor") }; @@ -427,7 +397,7 @@ export const marks: { [index: string]: MarkSpec } = { } }, - highlight: { + summarizeInclusive: { parseDOM: [ { tag: "span", @@ -436,7 +406,7 @@ export const marks: { [index: string]: MarkSpec } = { const style = getComputedStyle(p); if (style.textDecoration === "underline") return null; if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 && - p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) { + p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) { return null; } } @@ -447,6 +417,31 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: true, toDOM() { return ['span', { + style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)' + }]; + } + }, + + summarize: { + inclusive: false, + parseDOM: [ + { + tag: "span", + getAttrs: (p: any) => { + if (typeof (p) !== "string") { + const style = getComputedStyle(p); + if (style.textDecoration === "underline") return null; + if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 && + p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) { + return null; + } + } + return false; + } + }, + ], + toDOM() { + return ['span', { style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)' }]; } @@ -489,7 +484,6 @@ export const marks: { [index: string]: MarkSpec } = { user_mark: { attrs: { userid: { default: "" }, - opened: { default: true }, modified: { default: "when?" }, // 5 second intervals since 1970 }, group: "inline", @@ -499,25 +493,21 @@ export const marks: { [index: string]: MarkSpec } = { const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : ""; - return node.attrs.opened ? - ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] : - ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]]; + return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0]; } }, // the id of the user who entered the text user_tag: { attrs: { userid: { default: "" }, - opened: { default: true }, modified: { default: "when?" }, // 5 second intervals since 1970 tag: { default: "" } }, group: "inline", + inclusive: false, toDOM(node: any) { const uid = node.attrs.userid.replace(".", "").replace("@", ""); - return node.attrs.opened ? - ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] : - ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]]; + return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0]; } }, @@ -551,18 +541,6 @@ export const marks: { [index: string]: MarkSpec } = { }] }, - pFontColor: { - attrs: { - color: { default: "yellow" } - }, - parseDOM: [{ style: 'background: #d9dbdd' }], - toDOM: (node) => { - return ['span', { - style: `color: ${node.attrs.color}` - }]; - } - }, - /** FONT SIZES */ pFontSize: { attrs: { @@ -671,22 +649,33 @@ export class DashDocCommentView { this._collapsed.className = "formattedTextBox-inlineComment"; this._collapsed.id = "DashDocCommentView-" + node.attrs.docid; this._view = view; - let targetNode = () => { - for (let i = getPos() + 1; i < view.state.doc.nodeSize; i++) { - let m = view.state.doc.nodeAt(i); + const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor + for (let i = getPos() + 1; i < view.state.doc.content.size; i++) { + const m = view.state.doc.nodeAt(i); if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) { - return { node: m, pos: i } as { node: any, pos: number }; + return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; } } + const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" }); + view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc)); + setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0); return undefined; - } + }; this._collapsed.onpointerdown = (e: any) => { + e.stopPropagation(); + }; + this._collapsed.onpointerup = (e: any) => { const target = targetNode(); if (target) { - view.dispatch(view.state.tr. - setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true })); // update the attrs - setTimeout(() => node.attrs.hidden && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)), 100); + const expand = target.hidden; + const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); + view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs + setTimeout(() => { + expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { } + }, 0); } + e.stopPropagation(); }; this._collapsed.onpointerenter = (e: any) => { DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); @@ -715,11 +704,13 @@ export class DashDocView { return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale); } contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1; + outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this._textBox = tbox; this._dashSpan = document.createElement("div"); this._outer = document.createElement("span"); this._outer.style.position = "relative"; + this._outer.style.textIndent = "0"; this._outer.style.width = node.attrs.width; this._outer.style.height = node.attrs.height; this._outer.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -753,7 +744,11 @@ export class DashDocView { self._dashDoc = dashDoc; dashDoc.hideSidebar = true; if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") { - view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" })); + try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made + view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" })); + } catch (e) { + console.log(e); + } } this._reactionDisposer && this._reactionDisposer(); this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => { @@ -761,8 +756,9 @@ export class DashDocView { this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px"; }); ReactDOM.render(<DocumentView - fitToBox={BoolCast(dashDoc.fitToBox)} Document={dashDoc} + LibraryPath={tbox.props.LibraryPath} + fitToBox={BoolCast(dashDoc.fitToBox)} addDocument={returnFalse} removeDocument={removeDoc} ruleProvider={undefined} @@ -772,14 +768,14 @@ export class DashDocView { renderDepth={1} PanelWidth={self._dashDoc[WidthSym]} PanelHeight={self._dashDoc[HeightSym]} - focus={emptyFunction} + focus={self.outerFocus} backgroundColor={returnEmptyString} parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} - dontRegisterView={true} + dontRegisterView={false} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={this.contentScaling} @@ -787,7 +783,12 @@ export class DashDocView { } }); const self = this; - this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeydown = function (e: any) { + e.stopPropagation(); + if (e.key === "Tab" || e.key === "Enter") { + e.preventDefault(); + } + }; this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); }; this._dashSpan.onwheel = function (e: any) { e.preventDefault(); }; this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); }; @@ -938,7 +939,7 @@ export class FootnoteView { ignoreMutation() { return true; } } -export class SummarizedView { +export class SummaryView { _collapsed: HTMLElement; _view: any; constructor(node: any, view: any, getPos: any) { @@ -976,7 +977,8 @@ export class SummarizedView { className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); updateSummarizedText(start?: any) { - const mark = this._view.state.schema.marks.highlight.create(); + const mtype = this._view.state.schema.marks.summarize; + const mtypeInc = this._view.state.schema.marks.summarizeInclusive; let endPos = start; const visited = new Set(); @@ -984,7 +986,7 @@ export class SummarizedView { let skip = false; this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find((m: any) => m.type === mark.type)) { + if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { visited.add(node); endPos = i + node.nodeSize - 1; } @@ -1009,7 +1011,7 @@ const fromJson = schema.nodeFromJSON; schema.nodeFromJSON = (json: any) => { const node = fromJson(json); - if (json.type === "star") { + if (json.type === schema.marks.summarize.name) { node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice); } return node; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 7a9176bec..8ff54d052 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -34,9 +34,9 @@ export namespace SearchUtil { export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>; export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { query = query || "*"; //If we just have a filter query, search for * as the query - const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), { - qs: { ...options, q: query }, - })); + const rpquery = Utils.prepend("/search"); + const gotten = await rp.get(rpquery, { qs: { ...options, q: query } }); + const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); if (!returnDocs) { return result; } @@ -64,7 +64,7 @@ export namespace SearchUtil { const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc); for (let i = 0; i < textDocs.length; i++) { const testDoc = textDocs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) { + if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) { theDocs.push(Doc.GetProto(testDoc)); theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase()))); } @@ -74,7 +74,7 @@ export namespace SearchUtil { const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc); for (let i = 0; i < ids.length; i++) { const testDoc = docs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { + if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { theDocs.push(testDoc); theLines.push([]); } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index e01216e0f..86a7a620e 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,6 +2,9 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; +import { List } from "../../new_fields/List"; +import { DocumentDecorations } from "../views/DocumentDecorations"; +import RichTextMenu from "./RichTextMenu"; export namespace SelectionManager { @@ -27,18 +30,21 @@ export namespace SelectionManager { manager.SelectedDocuments.clear(); manager.SelectedDocuments.set(docView, true); } + Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } @action DeselectDoc(docView: DocumentView): void { if (manager.SelectedDocuments.get(docView)) { manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); + Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } } @action DeselectAll(): void { Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); + Doc.UserDoc().SelectedDocs = new List<Doc>([]); } } @@ -78,3 +84,4 @@ export namespace SelectionManager { return Array.from(manager.SelectedDocuments.keys()); } } + diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index 8310a0da6..2a38fe118 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -149,7 +149,7 @@ } svg { - fill: white; + fill: inherit; width: 18px; height: 18px; } @@ -181,7 +181,7 @@ } } -#colorPicker { +.colorPicker { position: relative; svg { diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 35a099c48..1c15dca7f 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,17 +1,13 @@ -import { action } from "mobx"; import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model"; import { wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc, Field, Opt } from "../../new_fields/Doc"; -import { Id } from "../../new_fields/FieldSymbols"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { FieldViewProps } from "../views/nodes/FieldView"; import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; -import { DocumentManager } from "./DocumentManager"; -import { DragManager } from "./DragManager"; import { LinkManager } from "./LinkManager"; import { schema } from "./RichTextSchema"; import "./TooltipTextMenu.scss"; @@ -20,12 +16,10 @@ import { updateBullets } from './ProsemirrorExampleTransfer'; import { DocumentDecorations } from '../views/DocumentDecorations'; import { SelectionManager } from './SelectionManager'; import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField'; -const { toggleMark, setBlockType } = require("prosemirror-commands"); -const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js"); +const { toggleMark } = require("prosemirror-commands"); //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { - public static Toolbar: HTMLDivElement | undefined; // editor state properties @@ -34,10 +28,7 @@ export class TooltipTextMenu { private fontStyles: Mark[] = []; private fontSizes: Mark[] = []; - private listTypes: (NodeType | any)[] = []; - private listTypeToIcon: Map<NodeType | any, string> = new Map(); - private _activeMarks: Mark[] = []; - private _marksToDoms: Map<Mark, HTMLSpanElement> = new Map(); + private _marksToDoms: Map<MarkType, HTMLSpanElement> = new Map(); private _collapsed: boolean = false; // editor doms @@ -47,20 +38,18 @@ export class TooltipTextMenu { // editor button doms private colorDom?: Node; private colorDropdownDom?: Node; - private highlightDom?: Node; - private highlightDropdownDom?: Node; - private linkEditor?: HTMLDivElement; - private linkText?: HTMLDivElement; - private linkDrag?: HTMLImageElement; - private _linkDropdownDom?: Node; + private linkDom?: Node; + private highighterDom?: Node; + private highlighterDropdownDom?: Node; + private linkDropdownDom?: Node; private _brushdom?: Node; private _brushDropdownDom?: Node; private fontSizeDom?: Node; private fontStyleDom?: Node; - private listTypeBtnDom?: Node; private basicTools?: HTMLElement; - + static createDiv(className: string) { const div = document.createElement("div"); div.className = className; return div; } + static createSpan(className: string) { const div = document.createElement("span"); div.className = className; return div; } constructor(view: EditorView) { this.view = view; @@ -68,74 +57,64 @@ export class TooltipTextMenu { this.initTooltip(view); // initialize the wrapper - this.wrapper = document.createElement("div"); - this.wrapper.className = "wrapper"; + this.wrapper = TooltipTextMenu.createDiv("wrapper"); this.wrapper.appendChild(this.tooltip); - // initialize the dragger -- appends it to the wrapper - this.createDragger(); - TooltipTextMenu.Toolbar = this.wrapper; } private async initTooltip(view: EditorView) { - // initialize tooltip dom - this.tooltip = document.createElement("div"); - this.tooltip.className = "tooltipMenu"; - this.basicTools = document.createElement("div"); - this.basicTools.className = "basic-tools"; - - // init buttons to the tooltip -- paths to svgs are obtained from fontawesome - const items = [ - { command: toggleMark(schema.marks.strong), dom: this.svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") }, - { command: toggleMark(schema.marks.em), dom: this.svgIcon("em", "Italic", "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z") }, - { command: toggleMark(schema.marks.underline), dom: this.svgIcon("underline", "Underline", "M32 64h32v160c0 88.22 71.78 160 160 160s160-71.78 160-160V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H272a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32v160a80 80 0 0 1-160 0V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm400 384H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z") }, - { command: toggleMark(schema.marks.strikethrough), dom: this.svgIcon("strikethrough", "Strikethrough", "M496 224H293.9l-87.17-26.83A43.55 43.55 0 0 1 219.55 112h66.79A49.89 49.89 0 0 1 331 139.58a16 16 0 0 0 21.46 7.15l42.94-21.47a16 16 0 0 0 7.16-21.46l-.53-1A128 128 0 0 0 287.51 32h-68a123.68 123.68 0 0 0-123 135.64c2 20.89 10.1 39.83 21.78 56.36H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h480a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-180.24 96A43 43 0 0 1 336 356.45 43.59 43.59 0 0 1 292.45 400h-66.79A49.89 49.89 0 0 1 181 372.42a16 16 0 0 0-21.46-7.15l-42.94 21.47a16 16 0 0 0-7.16 21.46l.53 1A128 128 0 0 0 224.49 480h68a123.68 123.68 0 0 0 123-135.64 114.25 114.25 0 0 0-5.34-24.36z") }, - { command: toggleMark(schema.marks.superscript), dom: this.svgIcon("superscript", "Superscript", "M496 160h-16V16a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 64h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") }, - { command: toggleMark(schema.marks.subscript), dom: this.svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") }, - // { command: toggleMark(schema.marks.highlight), dom: this.icon("H", 'blue', 'Blue') } + const self = this; + this.tooltip = TooltipTextMenu.createDiv("tooltipMenu"); + this.basicTools = TooltipTextMenu.createDiv("basic-tools"); + + const svgIcon = (name: string, title: string = name, dpath: string) => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "-100 -100 650 650"); + const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); + path.setAttributeNS(null, "d", dpath); + svg.appendChild(path); + + const span = TooltipTextMenu.createSpan(name + " menuicon"); + span.title = title; + span.appendChild(svg); + + return span; + }; + + const basicItems = [ // init basicItems in minimized toolbar -- paths to svgs are obtained from fontawesome + { mark: schema.marks.strong, dom: svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") }, + { mark: schema.marks.em, dom: svgIcon("em", "Italic", "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z") }, + { mark: schema.marks.underline, dom: svgIcon("underline", "Underline", "M32 64h32v160c0 88.22 71.78 160 160 160s160-71.78 160-160V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H272a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32v160a80 80 0 0 1-160 0V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm400 384H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z") }, + ]; + const items = [ // init items in full size toolbar + { mark: schema.marks.strikethrough, dom: svgIcon("strikethrough", "Strikethrough", "M496 224H293.9l-87.17-26.83A43.55 43.55 0 0 1 219.55 112h66.79A49.89 49.89 0 0 1 331 139.58a16 16 0 0 0 21.46 7.15l42.94-21.47a16 16 0 0 0 7.16-21.46l-.53-1A128 128 0 0 0 287.51 32h-68a123.68 123.68 0 0 0-123 135.64c2 20.89 10.1 39.83 21.78 56.36H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h480a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-180.24 96A43 43 0 0 1 336 356.45 43.59 43.59 0 0 1 292.45 400h-66.79A49.89 49.89 0 0 1 181 372.42a16 16 0 0 0-21.46-7.15l-42.94 21.47a16 16 0 0 0-7.16 21.46l.53 1A128 128 0 0 0 224.49 480h68a123.68 123.68 0 0 0 123-135.64 114.25 114.25 0 0 0-5.34-24.36z") }, + { mark: schema.marks.superscript, dom: svgIcon("superscript", "Superscript", "M496 160h-16V16a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 64h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") }, + { mark: schema.marks.subscript, dom: svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") }, ]; - // add menu items - this._marksToDoms = new Map(); - items.forEach(({ dom, command }) => { + basicItems.map(({ dom, mark }) => this.basicTools ?.appendChild(dom.cloneNode(true))); + basicItems.concat(items).forEach(({ dom, mark }) => { this.tooltip.appendChild(dom); - switch (dom.title) { - case "Bold": - this._marksToDoms.set(schema.mark(schema.marks.strong), dom); - this.basicTools && this.basicTools.appendChild(dom.cloneNode(true)); - break; - case "Italic": - this._marksToDoms.set(schema.mark(schema.marks.em), dom); - this.basicTools && this.basicTools.appendChild(dom.cloneNode(true)); - break; - case "Underline": - this._marksToDoms.set(schema.mark(schema.marks.underline), dom); - this.basicTools && this.basicTools.appendChild(dom.cloneNode(true)); - break; - } + this._marksToDoms.set(mark, dom); //pointer down handler to activate button effects dom.addEventListener("pointerdown", e => { - e.preventDefault(); this.view.focus(); if (dom.contains(e.target as Node)) { + e.preventDefault(); e.stopPropagation(); - command(this.view.state, this.view.dispatch, this.view); - // if (this.view.state.selection.empty) { - // if (dom.style.color === "white") { dom.style.color = "greenyellow"; } - // else { dom.style.color = "white"; } - // } + toggleMark(mark)(this.view.state, this.view.dispatch, this.view); + this.updateHighlightStateOfButtons(); } }); - }); - // highlight menu - this.highlightDom = this.createHighlightTool().render(this.view).dom; - this.highlightDropdownDom = this.createHighlightDropdown().render(this.view).dom; - this.tooltip.appendChild(this.highlightDom); - this.tooltip.appendChild(this.highlightDropdownDom); + // summarize menu + this.highighterDom = this.createHighlightTool().render(this.view).dom; + this.highlighterDropdownDom = this.createHighlightDropdown().render(this.view).dom; + this.tooltip.appendChild(this.highighterDom); + this.tooltip.appendChild(this.highlighterDropdownDom); // color menu this.colorDom = this.createColorTool().render(this.view).dom; @@ -144,46 +123,12 @@ export class TooltipTextMenu { this.tooltip.appendChild(this.colorDropdownDom); // link menu - this.updateLinkMenu(); - const dropdown = await this.createLinkDropdown(); - this._linkDropdownDom = dropdown.render(this.view).dom; - this.tooltip.appendChild(this._linkDropdownDom); + this.linkDom = this.createLinkTool().render(this.view).dom; + this.linkDropdownDom = this.createLinkDropdown("").render(this.view).dom; + this.tooltip.appendChild(this.linkDom); + this.tooltip.appendChild(this.linkDropdownDom); // list of font styles - this.initFontStyles(); - - // font sizes - this.initFontSizes(); - - // list types - this.initListTypes(); - - // init brush tool - this._brushdom = this.createBrush().render(this.view).dom; - this.tooltip.appendChild(this._brushdom); - this._brushDropdownDom = this.createBrushDropdown().render(this.view).dom; - this.tooltip.appendChild(this._brushDropdownDom); - - // star - this.tooltip.appendChild(this.createStar().render(this.view).dom); - - // list types dropdown - this.updateListItemDropdown(":", this.listTypeBtnDom); - - await this.updateFromDash(view, undefined, undefined); - } - - initFontStyles() { - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" })); - this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" })); - } - - initFontSizes() { this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 7 })); this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 8 })); this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 9 })); @@ -197,56 +142,89 @@ export class TooltipTextMenu { this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 32 })); this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 48 })); this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 72 })); - } - initListTypes() { - this.listTypeToIcon = new Map(); - //this.listTypeToIcon.set(schema.nodes.bullet_list, ":"); - this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "bullet" }), ":"); - this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1)"); - this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A)"); - // this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜"); - this.listTypes = Array.from(this.listTypeToIcon.keys()); - } + // font sizes + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" })); + this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" })); - // creates dragger element that allows dragging and collapsing (on double click) - // of editor and appends it to the wrapper - createDragger() { - const draggerWrapper = document.createElement("div"); - draggerWrapper.className = "dragger-wrapper"; - const dragger = document.createElement("div"); - dragger.className = "dragger"; + // init brush tool + this._brushdom = this.createBrushTool().render(this.view).dom; + this.tooltip.appendChild(this._brushdom); + this._brushDropdownDom = this.createBrushDropdown().render(this.view).dom; + this.tooltip.appendChild(this._brushDropdownDom); - const line1 = document.createElement("span"); - line1.className = "dragger-line"; - const line2 = document.createElement("span"); - line2.className = "dragger-line"; - const line3 = document.createElement("span"); - line3.className = "dragger-line"; + // summarizer tool + const summarizer = new MenuItem({ + title: "Summarize", + label: "Summarize", + icon: icons.join, + css: "fill:white;", + class: "menuicon", + execEvent: "", + run: (state, dispatch) => TooltipTextMenu.insertSummarizer(state, dispatch) + }); + this.tooltip.appendChild(summarizer.render(this.view).dom); - dragger.appendChild(line1); - dragger.appendChild(line2); - dragger.appendChild(line3); + // list types dropdown + const listDropdownTypes = [{ mapStyle: "bullet", label: ":" }, { mapStyle: "decimal", label: "1.1" }, { mapStyle: "multi", label: "A.1" }, { label: "X" }]; + const listTypes = new Dropdown(listDropdownTypes.map(({ mapStyle, label }) => + new MenuItem({ + title: "Set Bullet Style", + label: label, + execEvent: "", + class: "dropdown-item", + css: "color: black; width: 40px;", + enable() { return true; }, + run() { + const marks = self.view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks()); + if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => { + const tx3 = updateBullets(tx2, schema, mapStyle); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + view.dispatch(tx2); + })) { + const tx2 = view.state.tr; + const tx3 = updateBullets(tx2, schema, mapStyle); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + + view.dispatch(tx3); + } + } + })), { label: ":", css: "color:black; width: 40px;" }); + this.tooltip.appendChild(listTypes.render(this.view).dom); - draggerWrapper.appendChild(dragger); + await this.updateFromDash(view, undefined, undefined); + const draggerWrapper = TooltipTextMenu.createDiv("dragger-wrapper"); + const dragger = TooltipTextMenu.createDiv("dragger"); + dragger.appendChild(TooltipTextMenu.createSpan("dragger-line")); + dragger.appendChild(TooltipTextMenu.createSpan("dragger-line")); + dragger.appendChild(TooltipTextMenu.createSpan("dragger-line")); + draggerWrapper.appendChild(dragger); this.wrapper.appendChild(draggerWrapper); - this.dragElement(draggerWrapper); + this.setupDragElementInteractions(draggerWrapper); } - dragElement(elmnt: HTMLElement) { + setupDragElementInteractions(elmnt: HTMLElement) { var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (elmnt) { // if present, the header is where you move the DIV from: - elmnt.onpointerdown = dragMouseDown; + elmnt.onpointerdown = dragPointerDown; elmnt.ondblclick = onClick; } const self = this; - function dragMouseDown(e: PointerEvent) { + function dragPointerDown(e: PointerEvent) { e = e || window.event; - //e.preventDefault(); + e.preventDefault(); // get the mouse cursor position at startup: pos3 = e.clientX; pos4 = e.clientY; @@ -288,8 +266,6 @@ export class TooltipTextMenu { // stop moving when mouse button is released: document.onpointerup = null; document.onpointermove = null; - //self.highlightSearchTerms(self.state, ["hello"]); - //FormattedTextBox.Instance.unhighlightSearchTerms(); } } @@ -297,15 +273,33 @@ export class TooltipTextMenu { updateFontSizeDropdown(label: string) { //font SIZES const fontSizeBtns: MenuItem[] = []; - this.fontSizes.forEach(mark => { - fontSizeBtns.push(this.dropdownFontSizeBtn(String(mark.attrs.fontSize), "color: black; width: 50px;", mark, this.view, this.changeToFontSize)); - }); + const self = this; + this.fontSizes.forEach(mark => + fontSizeBtns.push(new MenuItem({ + title: "Set Font Size", + label: String(mark.attrs.fontSize), + execEvent: "", + class: "dropdown-item", + css: "color: black; width: 50px;", + enable() { return true; }, + run() { + const size = mark.attrs.fontSize; + if (size) { self.updateFontSizeDropdown(String(size) + " pt"); } + if (self.editorProps) { + const ruleProvider = self.editorProps.ruleProvider; + const heading = NumCast(self.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleSize_" + heading] = size; + } + } + TooltipTextMenu.setMark(self.view.state.schema.marks.pFontSize.create({ fontSize: size }), self.view.state, self.view.dispatch); + } + }))); - const newfontSizeDom = (new Dropdown(fontSizeBtns, { - label: label, - css: "color:black; min-width: 60px;" - }) as MenuItem).render(this.view).dom; - if (this.fontSizeDom) { this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); } + const newfontSizeDom = (new Dropdown(fontSizeBtns, { label: label, css: "color:black; min-width: 60px;" }) as MenuItem).render(this.view).dom; + if (this.fontSizeDom) { + this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom); + } else { this.tooltip.appendChild(newfontSizeDom); } @@ -316,112 +310,38 @@ export class TooltipTextMenu { updateFontStyleDropdown(label: string) { //font STYLES const fontBtns: MenuItem[] = []; - this.fontStyles.forEach((mark) => { - fontBtns.push(this.dropdownFontFamilyBtn(mark.attrs.family, "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;", mark, this.view, this.changeToFontFamily)); - }); + const self = this; + this.fontStyles.forEach(mark => + fontBtns.push(new MenuItem({ + title: "Set Font Family", + label: mark.attrs.family, + execEvent: "", + class: "dropdown-item", + css: "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;", + enable() { return true; }, + run() { + const fontName = mark.attrs.family; + if (fontName) { self.updateFontStyleDropdown(fontName); } + if (self.editorProps) { + const ruleProvider = self.editorProps.ruleProvider; + const heading = NumCast(self.editorProps.Document.heading); + if (ruleProvider && heading) { + ruleProvider["ruleFont_" + heading] = fontName; + } + } + TooltipTextMenu.setMark(self.view.state.schema.marks.pFontFamily.create({ family: fontName }), self.view.state, self.view.dispatch); + } + }))); - const newfontStyleDom = (new Dropdown(fontBtns, { - label: label, - css: "color:black; width: 125px;" - }) as MenuItem).render(this.view).dom; - if (this.fontStyleDom) { this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); } + const newfontStyleDom = (new Dropdown(fontBtns, { label: label, css: "color:black; width: 125px;" }) as MenuItem).render(this.view).dom; + if (this.fontStyleDom) { + this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom); + } else { this.tooltip.appendChild(newfontStyleDom); } this.fontStyleDom = newfontStyleDom; } - - updateLinkMenu() { - if (!this.linkEditor || !this.linkText) { - this.linkEditor = document.createElement("div"); - this.linkEditor.className = "ProseMirror-icon menuicon"; - this.linkText = document.createElement("div"); - 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(); }; - const linkBtn = document.createElement("div"); - linkBtn.textContent = ">>"; - linkBtn.style.width = "10px"; - linkBtn.style.height = "10px"; - linkBtn.style.color = "white"; - linkBtn.style.cssFloat = "left"; - linkBtn.onpointerdown = (e: PointerEvent) => { - const node = this.view.state.selection.$from.nodeAfter; - const link = node && node.marks.find(m => m.type.name === "link"); - if (link) { - const href: string = link.attrs.href; - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const docid = href.replace(Utils.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, false); - } - else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight"); - } - })); - } - // TODO This should have an else to handle external links - 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 = "15px"; - this.linkDrag.style.height = "15px"; - this.linkDrag.title = "Drag to create link"; - this.linkDrag.id = "link-drag"; - this.linkDrag.onpointerdown = (e: PointerEvent) => { - if (!this.editorProps) return; - const dragData = new DragManager.LinkDragData(this.editorProps.Document); - dragData.dontClearTextBox = true; - // hack to get source context -sy - const docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document); - e.stopPropagation(); - const ctrlKey = e.ctrlKey; - DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY, - { - handlers: { - dragComplete: action(() => { - if (dragData.linkDocument) { - const linkDoc = dragData.linkDocument; - const proto = Doc.GetProto(linkDoc); - if (proto && docView) { - proto.sourceContext = docView.props.ContainingCollectionDoc; - } - const text = this.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), ctrlKey ? "onRight" : "inTab"); - if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) { - proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link - } - } - }), - }, - hideSource: false - }); - e.stopPropagation(); - e.preventDefault(); - }; - this.linkEditor.appendChild(this.linkDrag); - this.tooltip.appendChild(this.linkEditor); - } - - const node = this.view.state.selection.$from.nodeAfter; - const 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(); - } - }; - } - async getTextLinkTargetTitle() { const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === "link"); @@ -455,8 +375,21 @@ export class TooltipTextMenu { } } - async createLinkDropdown() { - const targetTitle = await this.getTextLinkTargetTitle(); + // LINK TOOL + createLinkTool(active: boolean = false) { + return new MenuItem({ + title: "Link tool", + label: "Link tool", + icon: icons.link, + css: "fill:white;", + class: active ? "menuicon-active" : "menuicon", + execEvent: "", + run: async (state, dispatch) => { }, + active: (state) => true + }); + } + + createLinkDropdown(targetTitle: string) { const input = document.createElement("input"); // menu item for input for hyperlink url @@ -484,9 +417,7 @@ export class TooltipTextMenu { return div; }, enable() { return false; }, - run(p1, p2, p3, event) { - event.stopPropagation(); - } + run(p1, p2, p3, event) { event.stopPropagation(); } }); // menu item to update/apply the hyperlink to the selected text @@ -502,9 +433,22 @@ export class TooltipTextMenu { return button; }, enable() { return false; }, - run: (state, dispatch, view, event) => { + run: async (state, dispatch, view, event) => { event.stopPropagation(); - this.makeLinkToURL(input.value, "onRight"); + let node = this.view.state.selection.$from.nodeAfter; + let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: input.value, location: "onRight" }); + 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"); + + // update link menu + const linkDom = self.createLinkTool(true).render(self.view).dom; + const linkDropdownDom = self.createLinkDropdown(await self.getTextLinkTargetTitle()).render(self.view).dom; + self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom); + self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom); + self.linkDom = linkDom; + self.linkDropdownDom = linkDropdownDom; } }); @@ -526,190 +470,55 @@ export class TooltipTextMenu { }, enable() { return true; }, async run() { - self.deleteLink(); - // update link dropdown - const dropdown = await self.createLinkDropdown(); - const newLinkDropdowndom = dropdown.render(self.view).dom; - self._linkDropdownDom && self.tooltip.replaceChild(newLinkDropdowndom, self._linkDropdownDom); - self._linkDropdownDom = newLinkDropdowndom; - } - }); - - - const linkDropdown = new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem; - return linkDropdown; - } - - // makeLinkWithState = (state: EditorState, target: string, location: string) => { - // let link = state.schema.mark(state.schema.marks.link, { href: target, location: location }); - // } - - makeLink = (targetDoc: Doc, title: string, location: string): string => { - const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + targetDoc[Id]), title: title, location: location }); - this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link). - addMark(this.view.state.selection.from, this.view.state.selection.to, link)); - const node = this.view.state.selection.$from.nodeAfter; - if (node && node.text) { - return node.text; - } - return ""; - } - - makeLinkToURL = (target: String, lcoation: string) => { - let node = this.view.state.selection.$from.nodeAfter; - let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location }); - 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"); - } - - deleteLink = () => { - const node = this.view.state.selection.$from.nodeAfter; - const link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link); - const href = link!.attrs.href; - if (href) { - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - if (linkclicked) { - DocServer.GetRefField(linkclicked).then(async linkDoc => { + // delete the link + const node = self.view.state.selection.$from.nodeAfter; + const link = node && node.marks.find(m => m.type === self.view.state.schema.marks.link); + const href = link!.attrs.href; + if (href ?.indexOf(Utils.prepend("/doc/")) === 0) { + const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + linkclicked && DocServer.GetRefField(linkclicked).then(async linkDoc => { if (linkDoc instanceof Doc) { LinkManager.Instance.deleteLink(linkDoc); - this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link)); + self.view.dispatch(self.view.state.tr.removeMark(self.view.state.selection.from, self.view.state.selection.to, self.view.state.schema.marks.link)); } }); } - } - } - } - - deleteLinkItem() { - const icon = { - height: 16, width: 16, - path: "M15.898,4.045c-0.271-0.272-0.713-0.272-0.986,0l-4.71,4.711L5.493,4.045c-0.272-0.272-0.714-0.272-0.986,0s-0.272,0.714,0,0.986l4.709,4.711l-4.71,4.711c-0.272,0.271-0.272,0.713,0,0.986c0.136,0.136,0.314,0.203,0.492,0.203c0.179,0,0.357-0.067,0.493-0.203l4.711-4.711l4.71,4.711c0.137,0.136,0.314,0.203,0.494,0.203c0.178,0,0.355-0.067,0.492-0.203c0.273-0.273,0.273-0.715,0-0.986l-4.711-4.711l4.711-4.711C16.172,4.759,16.172,4.317,15.898,4.045z" - }; - return new MenuItem({ - title: "Delete Link", - label: "X", - icon: icon, - css: "color: red", - class: "summarize", - execEvent: "", - run: (state, dispatch) => { - this.deleteLink(); + // update link menu + const linkDom = self.createLinkTool(false).render(self.view).dom; + const linkDropdownDom = self.createLinkDropdown("").render(self.view).dom; + self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom); + self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom); + self.linkDom = linkDom; + self.linkDropdownDom = linkDropdownDom; } }); - } - createLink() { - const markType = schema.marks.link; - return new MenuItem({ - title: "Add or remove link", - label: "Add or remove link", - execEvent: "", - icon: icons.link, - css: "color:white;", - class: "menuicon", - enable(state) { return !state.selection.empty; }, - run: (state, dispatch, view) => { - // to remove link - let curLink = ""; - if (this.markActive(state, markType)) { - - const { from, $from, to, empty } = state.selection; - const node = state.doc.nodeAt(from); - node && node.marks.map(m => { - m.type === markType && (curLink = m.attrs.href); - }); - //toggleMark(markType)(state, dispatch); - //return true; - } - // to create link - openPrompt({ - title: "Create a link", - fields: { - href: new TextField({ - value: curLink, - label: "Link Target", - required: true - }), - title: new TextField({ label: "Title" }) - }, - callback(attrs: any) { - toggleMark(markType, attrs)(view.state, view.dispatch); - view.focus(); - }, - flyout_top: 0, - flyout_left: 0 - }); - } - }); + return new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem; } - //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown - updateListItemDropdown(label: string, listTypeBtn: any) { - //remove old btn - if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); } - - //Make a dropdown of all list types - const toAdd: MenuItem[] = []; - this.listTypeToIcon.forEach((icon, type) => { - toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); - }); - //option to remove the list formatting - toAdd.push(this.dropdownNodeBtn("X", "color: black; width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); - - listTypeBtn = (new Dropdown(toAdd, { - label: label, - css: "color:black; width: 40px;" - }) as MenuItem).render(this.view).dom; - - //add this new button and return it - this.tooltip.appendChild(listTypeBtn); - return listTypeBtn; - } - - createStar() { - return new MenuItem({ - title: "Summarize", - label: "Summarize", - icon: icons.join, - css: "color:white;", - class: "menuicon", - execEvent: "", - run: (state, dispatch) => { - TooltipTextMenu.insertStar(this.view.state, this.view.dispatch); - } - - }); - } - - public static insertStar(state: EditorState<any>, dispatch: any) { - if (state.selection.empty) return false; - const mark = state.schema.marks.highlight.create(); - const tr = state.tr; - tr.addMark(state.selection.from, state.selection.to, mark); - const content = tr.selection.content(); - const newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() }); - dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); - return true; + public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => { + const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId }); + this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link). + addMark(this.view.state.selection.from, this.view.state.selection.to, link)); + return this.view.state.selection.$from.nodeAfter ?.text || ""; } - public static insertComment(state: EditorState<any>, dispatch: any) { - if (state.selection.empty) return false; - const mark = state.schema.marks.highlight.create(); - const tr = state.tr; - tr.addMark(state.selection.from, state.selection.to, mark); - const content = tr.selection.content(); - const newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() }); - dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); - return true; + // SUMMARIZER TOOL + static insertSummarizer(state: EditorState<any>, dispatch: any) { + if (!state.selection.empty) { + const mark = state.schema.marks.summarize.create(); + const tr = state.tr.addMark(state.selection.from, state.selection.to, mark); + const content = tr.selection.content(); + const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); + dispatch ?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + } } + // HIGHLIGHTER TOOL createHighlightTool() { return new MenuItem({ title: "Highlight", - css: "color:white;", + css: "fill:white;", class: "menuicon", execEvent: "", render() { @@ -719,27 +528,22 @@ export class TooltipTextMenu { path.setAttributeNS(null, "d", "M0 479.98L99.92 512l35.45-35.45-67.04-67.04L0 479.98zm124.61-240.01a36.592 36.592 0 0 0-10.79 38.1l13.05 42.83-50.93 50.94 96.23 96.23 50.86-50.86 42.74 13.08c13.73 4.2 28.65-.01 38.15-10.78l35.55-41.64-173.34-173.34-41.52 35.44zm403.31-160.7l-63.2-63.2c-20.49-20.49-53.38-21.52-75.12-2.35L190.55 183.68l169.77 169.78L530.27 154.4c19.18-21.74 18.15-54.63-2.35-75.13z"); svg.appendChild(path); - const color = document.createElement("div"); - color.className = "buttonColor"; - color.style.backgroundColor = TooltipTextMenuManager.Instance.highlight.toString(); + const color = TooltipTextMenu.createDiv("buttonColor"); + color.style.backgroundColor = TooltipTextMenuManager.Instance.highlighter.toString(); - const wrapper = document.createElement("div"); - wrapper.id = "colorPicker"; + const wrapper = TooltipTextMenu.createDiv("colorPicker"); wrapper.appendChild(svg); wrapper.appendChild(color); return wrapper; }, - run: (state, dispatch) => { - TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlight, this.view.state, this.view.dispatch); - } + run: (state, dispatch) => TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, state, dispatch) }); } - public static insertHighlight(color: String, state: EditorState<any>, dispatch: any) { - if (state.selection.empty) return false; - - const highlightMark = state.schema.mark(state.schema.marks.marker, { highlight: color }); - dispatch(state.tr.addMark(state.selection.from, state.selection.to, highlightMark)); + static insertHighlight(color: String, state: EditorState<any>, dispatch: any) { + if (!state.selection.empty) { + toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch); + } } createHighlightDropdown() { @@ -754,8 +558,7 @@ export class TooltipTextMenu { const p = document.createElement("p"); p.textContent = "Change highlight:"; - const colorsWrapper = document.createElement("div"); - colorsWrapper.className = "colorPicker-wrapper"; + const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper"); const colors = [ PastelSchemaPalette.get("pink2"), @@ -772,22 +575,22 @@ export class TooltipTextMenu { colors.forEach(color => { const button = document.createElement("button"); - button.className = color === TooltipTextMenuManager.Instance.highlight ? "colorPicker active" : "colorPicker"; + button.className = color === TooltipTextMenuManager.Instance.highlighter ? "colorPicker active" : "colorPicker"; if (color) { button.style.backgroundColor = color; button.textContent = color === "transparent" ? "X" : ""; button.onclick = e => { - TooltipTextMenuManager.Instance.highlight = color; + TooltipTextMenuManager.Instance.highlighter = color; - TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlight, self.view.state, self.view.dispatch); + TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, self.view.state, self.view.dispatch); // update color menu const highlightDom = self.createHighlightTool().render(self.view).dom; const highlightDropdownDom = self.createHighlightDropdown().render(self.view).dom; - self.highlightDom && self.tooltip.replaceChild(highlightDom, self.highlightDom); - self.highlightDropdownDom && self.tooltip.replaceChild(highlightDropdownDom, self.highlightDropdownDom); - self.highlightDom = highlightDom; - self.highlightDropdownDom = highlightDropdownDom; + self.highighterDom && self.tooltip.replaceChild(highlightDom, self.highighterDom); + self.highlighterDropdownDom && self.tooltip.replaceChild(highlightDropdownDom, self.highlighterDropdownDom); + self.highighterDom = highlightDom; + self.highlighterDropdownDom = highlightDropdownDom; }; } colorsWrapper.appendChild(button); @@ -804,14 +607,14 @@ export class TooltipTextMenu { } }); - const colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem; - return colorDropdown; + return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem; } + // COLOR TOOL createColorTool() { return new MenuItem({ title: "Color", - css: "color:white;", + css: "fill:white;", class: "menuicon", execEvent: "", render() { @@ -821,27 +624,25 @@ export class TooltipTextMenu { path.setAttributeNS(null, "d", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"); svg.appendChild(path); - const color = document.createElement("div"); - color.className = "buttonColor"; + const color = TooltipTextMenu.createDiv("buttonColor"); color.style.backgroundColor = TooltipTextMenuManager.Instance.color.toString(); - const wrapper = document.createElement("div"); - wrapper.id = "colorPicker"; + const wrapper = TooltipTextMenu.createDiv("colorPicker"); wrapper.appendChild(svg); wrapper.appendChild(color); return wrapper; }, - run: (state, dispatch) => { - TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, this.view.state, this.view.dispatch); - } + run: (state, dispatch) => TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, state, dispatch) }); } - public static insertColor(color: String, state: EditorState<any>, dispatch: any) { - if (state.selection.empty) return false; - - const colorMark = state.schema.mark(state.schema.marks.color, { color: color }); - dispatch(state.tr.addMark(state.selection.from, state.selection.to, colorMark)); + static insertColor(color: String, state: EditorState<any>, dispatch: any) { + const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color }); + if (state.selection.empty) { + dispatch(state.tr.addStoredMark(colorMark)); + } else { + this.setMark(colorMark, state, dispatch); + } } createColorDropdown() { @@ -856,8 +657,7 @@ export class TooltipTextMenu { const p = document.createElement("p"); p.textContent = "Change color:"; - const colorsWrapper = document.createElement("div"); - colorsWrapper.className = "colorPicker-wrapper"; + const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper"); const colors = [ DarkPastelSchemaPalette.get("pink2"), @@ -900,16 +700,14 @@ export class TooltipTextMenu { return div; }, enable() { return false; }, - run(p1, p2, p3, event) { - event.stopPropagation(); - } + run(p1, p2, p3, event) { event.stopPropagation(); } }); - const colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem; - return colorDropdown; + return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem; } - createBrush(active: boolean = false) { + // BRUSH TOOL + createBrushTool(active: boolean = false) { const icon = { height: 32, width: 32, path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z" @@ -919,7 +717,7 @@ export class TooltipTextMenu { title: "Brush tool", label: "Brush tool", icon: icon, - css: "color:white;", + css: "fill:white;", class: active ? "menuicon-active" : "menuicon", execEvent: "", run: (state, dispatch) => { @@ -930,23 +728,23 @@ export class TooltipTextMenu { self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom); self._brushDropdownDom = newBrushDropdowndom; }, - active: (state) => { - return true; - } + active: (state) => true }); } brush_function(state: EditorState<any>, dispatch: any) { if (TooltipTextMenuManager.Instance._brushIsEmpty) { - const selected_marks = this.getMarksInSelection(this.view.state); - if (this._brushdom) { - if (selected_marks.size >= 0) { - TooltipTextMenuManager.Instance._brushMarks = selected_marks; - const newbrush = this.createBrush(true).render(this.view).dom; - this.tooltip.replaceChild(newbrush, this._brushdom); - this._brushdom = newbrush; - TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty; - } + // get marks in the selection + const selected_marks = new Set<Mark>(); + const { from, to } = state.selection as TextSelection; + state.doc.nodesBetween(from, to, (node) => node.marks ?.forEach(m => selected_marks.add(m))); + + if (this._brushdom && selected_marks.size >= 0) { + TooltipTextMenuManager.Instance._brushMarks = selected_marks; + const newbrush = this.createBrushTool(true).render(this.view).dom; + this.tooltip.replaceChild(newbrush, this._brushdom); + this._brushdom = newbrush; + TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty; } } else { @@ -956,13 +754,12 @@ export class TooltipTextMenu { if (TooltipTextMenuManager.Instance._brushMarks && to - from > 0) { this.view.dispatch(this.view.state.tr.removeMark(from, to)); Array.from(TooltipTextMenuManager.Instance._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => { - const markType = mark.type; - this.changeToMarkInGroup(markType, this.view, []); + TooltipTextMenu.setMark(mark, this.view.state, this.view.dispatch); }); } } else { - const newbrush = this.createBrush(false).render(this.view).dom; + const newbrush = this.createBrushTool(false).render(this.view).dom; this.tooltip.replaceChild(newbrush, this._brushdom); this._brushdom = newbrush; TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty; @@ -974,17 +771,12 @@ export class TooltipTextMenu { createBrushDropdown(active: boolean = false) { let label = "Stored marks: "; if (TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0) { - TooltipTextMenuManager.Instance._brushMarks.forEach((mark: Mark) => { - const markType = mark.type; - label += markType.name; - label += ", "; - }); + TooltipTextMenuManager.Instance._brushMarks.forEach((mark: Mark) => label += mark.type.name + ", "); label = label.substring(0, label.length - 2); } else { label = "No marks are currently stored"; } - const brushInfo = new MenuItem({ title: "", label: label, @@ -992,12 +784,11 @@ export class TooltipTextMenu { class: "button-setting-disabled", css: "", enable() { return false; }, - run(p1, p2, p3, event) { - event.stopPropagation(); - } + run(p1, p2, p3, event) { event.stopPropagation(); } }); const self = this; + const input = document.createElement("input"); const clearBrush = new MenuItem({ title: "Clear brush", execEvent: "", @@ -1007,7 +798,31 @@ export class TooltipTextMenu { const button = document.createElement("button"); button.textContent = "Clear brush"; + input.textContent = "editme"; + input.style.width = "75px"; + input.style.height = "30px"; + input.style.background = "white"; + input.setAttribute("contenteditable", "true"); + input.style.whiteSpace = "nowrap"; + input.type = "text"; + input.placeholder = "Enter URL"; + input.onpointerdown = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + input.onclick = (e: MouseEvent) => { + input.select(); + input.focus(); + }; + input.onkeypress = (e: KeyboardEvent) => { + if (e.key === "Enter") { + TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMap.set(input.value, TooltipTextMenuManager.Instance._brushMarks); + input.style.background = "lightGray"; + } + }; + const wrapper = document.createElement("div"); + wrapper.appendChild(input); wrapper.appendChild(button); return wrapper; }, @@ -1018,7 +833,7 @@ export class TooltipTextMenu { // update brush tool // TODO: this probably isn't very clean - const newBrushdom = self.createBrush().render(self.view).dom; + const newBrushdom = self.createBrushTool().render(self.view).dom; self._brushdom && self.tooltip.replaceChild(newBrushdom, self._brushdom); self._brushdom = newBrushdom; const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom; @@ -1028,295 +843,31 @@ export class TooltipTextMenu { }); const hasMarks = TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0; - const brushDom = new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem; - return brushDom; + return new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem; } - //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected textchangeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => { - changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => { - const { $cursor, ranges } = view.state.selection as TextSelection; - const state = view.state; - const 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; - for (let i = 0; !has && i < ranges.length; i++) { - const { $from, $to } = ranges[i]; - has = state.doc.rangeHasMark($from.pos, $to.pos, type); - } - for (const i of ranges) { - if (has) { - toggleMark(type)(view.state, view.dispatch, view); - } - } - } - } - }); - - if (markType) { - //actually apply font - if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) { - const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type, - { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema); - view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from)))); - } - else toggleMark(markType)(view.state, view.dispatch, view); - } - } - - changeToFontFamily = (mark: Mark, view: EditorView) => { - const { $cursor, ranges } = view.state.selection as TextSelection; - const state = view.state; - const dispatch = view.dispatch; - - //remove all other active font marks - if ($cursor) { - if (view.state.schema.marks.pFontFamily.isInSet(state.storedMarks || $cursor.marks())) { - dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontFamily)); - } - } else { - let has = false; - for (let i = 0; !has && i < ranges.length; i++) { - const { $from, $to } = ranges[i]; - has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontFamily); - } - for (const i of ranges) { - if (has) { - toggleMark(view.state.schema.marks.pFontFamily)(view.state, view.dispatch, view); - } - } - } - const fontName = mark.attrs.family; - if (fontName) { this.updateFontStyleDropdown(fontName); } - if (this.editorProps) { - const ruleProvider = this.editorProps.ruleProvider; - const heading = NumCast(this.editorProps.Document.heading); - if (ruleProvider && heading) { - ruleProvider["ruleFont_" + heading] = fontName; - } - } - //actually apply font - if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) { - const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type, - { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: fontName }), view.state.schema); - view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from)))); - } - else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontFamily.create({ family: fontName }))); - view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontFamily.create({ family: fontName })]; - } - - changeToFontSize = (mark: Mark, view: EditorView) => { - const { $cursor, ranges } = view.state.selection as TextSelection; - const state = view.state; - const dispatch = view.dispatch; - - //remove all other active font marks - if ($cursor) { - if (view.state.schema.marks.pFontSize.isInSet(state.storedMarks || $cursor.marks())) { - dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontSize)); - } - } else { - let has = false; - for (let i = 0; !has && i < ranges.length; i++) { - const { $from, $to } = ranges[i]; - has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontSize); - } - for (const i of ranges) { - if (has) { - toggleMark(view.state.schema.marks.pFontSize)(view.state, view.dispatch, view); - } - } - } - - const size = mark.attrs.fontSize; - if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } - if (this.editorProps) { - const ruleProvider = this.editorProps.ruleProvider; - const heading = NumCast(this.editorProps.Document.heading); - if (ruleProvider && heading) { - ruleProvider["ruleSize_" + heading] = size; - } - } - //actually apply font - if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) { - const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type, - { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: size }), view.state.schema); - view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from)))); - } - else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontSize.create({ fontSize: size }))); - view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontSize.create({ fontSize: size })]; - } - - //remove all node typeand apply the passed-in one to the selected text - changeToNodeType = (nodeType: NodeType | undefined) => { - //remove oldif (nodeType) { //add new - const view = this.view; - if (nodeType === schema.nodes.bullet_list) { - wrapInList(nodeType)(view.state, view.dispatch); - } else { - const marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks()); - if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - - view.dispatch(tx2); - })) { - const tx2 = view.state.tr; - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - - view.dispatch(tx3); - } - } - } - - //makes a button for the drop down FOR MARKS - //css is the style you want applied to the button - dropdownFontFamilyBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontFamily: (mark: Mark<any>, view: EditorView) => any) { - return new MenuItem({ - title: "", - label: label, - execEvent: "", - class: "dropdown-item", - css: css, - enable() { return true; }, - run() { - changeFontFamily(mark, view); - } - }); - } - //makes a button for the drop down FOR MARKS - //css is the style you want applied to the button - dropdownFontSizeBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontSize: (markType: Mark<any>, view: EditorView) => any) { - return new MenuItem({ - title: "", - label: label, - execEvent: "", - class: "dropdown-item", - css: css, - enable() { return true; }, - run() { - changeFontSize(mark, view); - } - }); - } - - //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: "dropdown-item", - css: css, - enable() { return true; }, - run() { - changeToNodeInGroup(nodeType, view, groupNodes); - } - }); - } - - markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) { - const { from, $from, to, empty } = state.selection; - if (empty) return type.isInSet(state.storedMarks || $from.marks()); - else return state.doc.rangeHasMark(from, to, type); - }; - - // Helper function to create menu icons - icon(text: string, name: string, title: string = name) { - const span = document.createElement("span"); - span.className = name + " menuicon"; - span.title = title; - span.textContent = text; - span.style.color = "white"; - return span; - } - - svgIcon(name: string, title: string = name, dpath: string) { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("viewBox", "-100 -100 650 650"); - const path = document.createElementNS('http://www.w3.org/2000/svg', "path"); - path.setAttributeNS(null, "d", dpath); - svg.appendChild(path); - - const span = document.createElement("span"); - span.className = name + " menuicon"; - span.title = title; - span.appendChild(svg); - - return span; - } - - //method for checking whether node can be inserted - canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) { - const $from = state.selection.$from; - for (let d = $from.depth; d >= 0; d--) { - const 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) { - const attrs = {}; - - if (state.selection instanceof NodeSelection) { - const sel: NodeSelection = state.selection; - const $from = sel.$from; - const to = sel.to; - const 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") - }; - } - - getMarksInSelection(state: EditorState<any>) { - const found = new Set<Mark>(); - const { from, to } = state.selection as TextSelection; - state.doc.nodesBetween(from, to, (node) => { - const marks = node.marks; - if (marks) { - marks.forEach(m => { - found.add(m); + static setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => { + if (mark) { + const node = (state.selection as NodeSelection).node; + if (node ?.type === schema.nodes.ordered_list) { + let attrs = node.attrs; + if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize }; + if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color }; + const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); + dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); + } else { + toggleMark(mark.type, mark.attrs)(state, (tx: any) => { + const { from, $from, to, empty } = tx.selection; + if (!tx.doc.rangeHasMark(from, to, mark.type)) { + toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); + } else dispatch(tx); }); } - }); - return found; - } - - reset_mark_doms() { - const iterator = this._marksToDoms.values(); - let next = iterator.next(); - while (!next.done) { - next.value.style.color = "white"; - next = iterator.next(); } } + // called by Prosemirror update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); } //updates the tooltip menu when the selection changes public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { @@ -1325,64 +876,45 @@ export class TooltipTextMenu { return; } this.view = view; - const state = view.state; DocumentDecorations.Instance.showTextBar(); props && (this.editorProps = props); - // Don't do anything if the document/selection didn't change - if (lastState && lastState.doc.eq(state.doc) && - lastState.selection.eq(state.selection)) return; - - this.reset_mark_doms(); - - // Hide the tooltip if the selection is empty - if (state.selection.empty) { - //this.tooltip.style.display = "none"; - //return; - } - // update link dropdown - const linkDropdown = await this.createLinkDropdown(); - const newLinkDropdowndom = linkDropdown.render(this.view).dom; - this._linkDropdownDom && this.tooltip.replaceChild(newLinkDropdowndom, this._linkDropdownDom); - this._linkDropdownDom = newLinkDropdowndom; - - //UPDATE FONT STYLE DROPDOWN - const activeStyles = this.activeFontFamilyOnSelection(); - if (activeStyles !== undefined) { - if (activeStyles.length === 1) { - console.log("updating font style dropdown", activeStyles[0]); - activeStyles[0] && this.updateFontStyleDropdown(activeStyles[0]); - } else this.updateFontStyleDropdown(activeStyles.length ? "various" : "default"); - } - - //UPDATE FONT SIZE DROPDOWN - const activeSizes = this.activeFontSizeOnSelection(); - if (activeSizes !== undefined) { - if (activeSizes.length === 1) { //if there's only one active font size - activeSizes[0] && this.updateFontSizeDropdown(String(activeSizes[0]) + " pt"); - } else this.updateFontSizeDropdown(activeSizes.length ? "various" : "default"); + // Don't do anything if the document/selection didn't change + if (!lastState || !lastState.doc.eq(view.state.doc) || !lastState.selection.eq(view.state.selection)) { + + // UPDATE LINK DROPDOWN + const linkTarget = await this.getTextLinkTargetTitle(); + const linkDom = this.createLinkTool(linkTarget ? true : false).render(this.view).dom; + const linkDropdownDom = this.createLinkDropdown(linkTarget).render(this.view).dom; + this.linkDom && this.tooltip.replaceChild(linkDom, this.linkDom); + this.linkDropdownDom && this.tooltip.replaceChild(linkDropdownDom, this.linkDropdownDom); + this.linkDom = linkDom; + this.linkDropdownDom = linkDropdownDom; + + //UPDATE FONT STYLE DROPDOWN + const activeStyles = this.activeFontFamilyOnSelection(); + this.updateFontStyleDropdown(activeStyles.length === 1 ? activeStyles[0] : activeStyles.length ? "various" : "default"); + + //UPDATE FONT SIZE DROPDOWN + const activeSizes = this.activeFontSizeOnSelection(); + this.updateFontSizeDropdown(activeSizes.length === 1 ? String(activeSizes[0]) + " pt" : activeSizes.length ? "various" : "default"); + + //UPDATE ALL OTHER BUTTONS + this.updateHighlightStateOfButtons(); } - - this.update_mark_doms(); } - update_mark_doms() { - this.reset_mark_doms(); - this._activeMarks.forEach((mark) => { - if (this._marksToDoms.has(mark)) { - const dom = this._marksToDoms.get(mark); - if (dom) dom.style.color = "greenyellow"; - } - }); + + updateHighlightStateOfButtons() { + Array.from(this._marksToDoms.values()).forEach(val => val.style.fill = "white"); + this.activeMarksOnSelection().filter(mark => this._marksToDoms.has(mark)).forEach(mark => + this._marksToDoms.get(mark)!.style.fill = "greenyellow"); // keeps brush tool highlighted if active when switching between textboxes - if (!TooltipTextMenuManager.Instance._brushIsEmpty) { - if (this._brushdom) { - const newbrush = this.createBrush(true).render(this.view).dom; - this.tooltip.replaceChild(newbrush, this._brushdom); - this._brushdom = newbrush; - } + if (!TooltipTextMenuManager.Instance._brushIsEmpty && this._brushdom) { + const newbrush = this.createBrushTool(true).render(this.view).dom; + this.tooltip.replaceChild(newbrush, this._brushdom); + this._brushdom = newbrush; } - } //finds fontSize at start of selection @@ -1410,24 +942,21 @@ export class TooltipTextMenu { return activeFamilies; } //finds all active marks on selection in given group - activeMarksOnSelection(markGroup: MarkType[]) { + activeMarksOnSelection() { + const markGroup = Array.from(this._marksToDoms.keys()); + if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); //current selection const { empty, ranges, $to } = this.view.state.selection as TextSelection; const state = this.view.state; - const dispatch = this.view.dispatch; - let activeMarks: MarkType[]; + let activeMarks: MarkType[] = []; if (!empty) { activeMarks = markGroup.filter(mark => { const has = false; for (let i = 0; !has && i < ranges.length; i++) { - const { $from, $to } = ranges[i]; - return state.doc.rangeHasMark($from.pos, $to.pos, mark); + return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark); } return false; }); - - const refnode = this.reference_node($to); - this._activeMarks = refnode.marks; } else { const pos = this.view.state.selection.$from; @@ -1438,20 +967,14 @@ export class TooltipTextMenu { else { return []; } - this._activeMarks = ref_node.marks; activeMarks = markGroup.filter(mark_type => { if (mark_type === state.schema.marks.pFontSize) { return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); } const mark = state.schema.mark(mark_type); return ref_node.marks.includes(mark); - return false; }); } - else { - return []; - } - } return activeMarks; } @@ -1488,20 +1011,21 @@ export class TooltipTextMenu { } -class TooltipTextMenuManager { +export class TooltipTextMenuManager { private static _instance: TooltipTextMenuManager; + private _isPinned: boolean = false; public pinnedX: number = 0; public pinnedY: number = 0; public unpinnedX: number = 0; public unpinnedY: number = 0; - private _isPinned: boolean = false; public _brushMarks: Set<Mark> | undefined; + public _brushMap: Map<string, Set<Mark>> = new Map(); public _brushIsEmpty: boolean = true; public color: String = "#000"; - public highlight: String = "transparent"; + public highlighter: String = "transparent"; public activeMenu: TooltipTextMenu | undefined; @@ -1512,11 +1036,7 @@ class TooltipTextMenuManager { return TooltipTextMenuManager._instance; } - public get isPinned() { - return this._isPinned; - } + public get isPinned() { return this._isPinned; } - public toggleIsPinned() { - this._isPinned = !this._isPinned; - } + public toggleIsPinned() { this._isPinned = !this._isPinned; } } |
