diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/DictationManager.ts | 4 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 55 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 27 | ||||
| -rw-r--r-- | src/client/util/DropConverter.ts | 7 | ||||
| -rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 19 | ||||
| -rw-r--r-- | src/client/util/Import & Export/ImageUtils.ts | 2 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 54 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts | 54 | ||||
| -rw-r--r-- | src/client/util/RichTextMenu.tsx | 29 | ||||
| -rw-r--r-- | src/client/util/RichTextRules.ts | 547 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 130 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 2 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.scss | 2 | ||||
| -rw-r--r-- | src/client/util/type_decls.d | 1 |
14 files changed, 504 insertions, 429 deletions
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 3394cb93d..569c1ef6d 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -326,7 +326,7 @@ export namespace DictationManager { ["open fields", { action: (target: DocumentView) => { const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - target.props.addDocTab(kvp, target.props.DataDoc, "onRight"); + target.props.addDocTab(kvp, "onRight"); } }], @@ -340,7 +340,7 @@ export namespace DictationManager { const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; proto.data = new RichTextField(proseMirrorState); proto.backgroundColor = "#eeffff"; - target.props.addDocTab(newBox, proto, "onRight"); + target.props.addDocTab(newBox, "onRight"); } }] diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index fb4c2155a..c639f07f5 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,5 +1,5 @@ import { action, computed, observable } from 'mobx'; -import { Doc, DocListCastAsync, DocListCast } from '../../new_fields/Doc'; +import { Doc, DocListCastAsync, DocListCast, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; import { List } from '../../new_fields/List'; import { Cast, NumCast, StrCast } from '../../new_fields/Types'; @@ -85,17 +85,17 @@ export class DocumentManager { return this.getDocumentViewById(toFind[Id], preferredCollection); } - public getFirstDocumentView(toFind: Doc): DocumentView | undefined { + public getFirstDocumentView(toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined { const views = this.getDocumentViews(toFind); - return views.length ? views[0] : undefined; + return views?.find(view => view.props.Document !== originatingDoc); } public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; DocumentManager.Instance.DocumentViews.map(view => - view.props.Document === toFind && toReturn.push(view)); + view.props.Document.presBox === undefined && 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)); + view.props.Document.presBox === undefined && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); return toReturn; } @@ -127,20 +127,20 @@ export class DocumentManager { return pairs; } - public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => { + public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false, originatingDoc: Opt<Doc> = undefined): Promise<void> => { const highlight = () => { const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc); finalDocView && (finalDocView.Document.scrollToLinkID = linkId); finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document); }; - const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc); + const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc, originatingDoc); 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? - docView.props.focus(docView.props.Document, false); + docView.props.focus(docView.props.Document, willZoom); highlight(); } else { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; @@ -148,7 +148,7 @@ export class DocumentManager { const targetDocContext = (annotatedDoc ? annotatedDoc : contextDoc); if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default - (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); + (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); highlight(); } else { const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext); @@ -164,12 +164,12 @@ export class DocumentManager { retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context } else { if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document); - targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target + targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); // otherwise create a new view of the target } highlight(); }, 0); } else { // there's no context view so we need to create one first and try again - (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined); + (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext); setTimeout(() => { const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc); const finalDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext); @@ -199,7 +199,7 @@ export class DocumentManager { const targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined; 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]); + DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id], undefined, doc); } else if (link) { DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined); } @@ -208,7 +208,7 @@ export class DocumentManager { @action zoomIntoScale = (docDelegate: Doc, scale: number) => { const docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate)); - docView && docView.props.zoomToScale(scale); + docView?.props.zoomToScale(scale); } getScaleOfDocView = (docDelegate: Doc) => { @@ -221,34 +221,5 @@ export class DocumentManager { return 1; } } - - @action - animateBetweenPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - expandedDocs && expandedDocs.map(expDoc => { - if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC - if (expDoc.isMinimized) { // docs are never actaully at the minimized location. so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location - expDoc.isMinimized = false; - expDoc.animateToPos = new List<number>([...scrpt, 0]); - expDoc.animateToDimensions = new List<number>([0, 0]); - } - setTimeout(() => { - expDoc.isAnimating = "max"; - expDoc.animateToPos = new List<number>([0, 0, 1]); - expDoc.animateToDimensions = new List<number>([NumCast(expDoc.width), NumCast(expDoc.height)]); - setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600); - }, 0); - } else { // MINIMIZE DOC - expDoc.isAnimating = "min"; - expDoc.animateToPos = new List<number>([...scrpt, 0]); - expDoc.animateToDimensions = new List<number>([0, 0]); - setTimeout(() => { - if (expDoc.isAnimating === "min") { - expDoc.isMinimized = true; - expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined; - } - }, 600); - } - }); - } } Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 5d4b8fc8a..1cfebf414 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -133,8 +133,8 @@ export namespace DragManager { userDropAction: dropActionType; embedDoc?: boolean; moveDocument?: MoveFunction; - isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts applyAsTemplate?: boolean; + isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts } export class LinkDragData { constructor(linkSourceDoc: Doc) { @@ -196,8 +196,11 @@ export namespace DragManager { 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)); + 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, finishDrag); @@ -306,7 +309,7 @@ export namespace DragManager { dragElement.style.transformOrigin = "0 0"; dragElement.style.borderRadius = getComputedStyle(ele).borderRadius; dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; - dragElement.style.transform = `translate(${rect.left + (options ?.offsetX || 0)}px, ${rect.top + (options ?.offsetY || 0)}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`; @@ -335,8 +338,8 @@ export namespace DragManager { return dragElement; }); - const hideSource = options ?.hideSource ? true : false; - eles.map(ele => ele.parentElement && ele.parentElement ?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (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; @@ -347,7 +350,7 @@ export namespace DragManager { } if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) { AbortDrag(); - finishDrag ?.(new DragCompleteEvent(true, dragData)); + finishDrag?.(new DragCompleteEvent(true, dragData)); CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, @@ -361,13 +364,13 @@ export namespace DragManager { lastX = e.pageX; lastY = e.pageY; dragElements.map((dragElement, i) => (dragElement.style.transform = - `translate(${(xs[i] += moveX) + (options ?.offsetX || 0)}px, ${(ys[i] += moveY) + (options ?.offsetY || 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.parentElement && ele.parentElement ?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (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); @@ -377,14 +380,14 @@ export namespace DragManager { AbortDrag = () => { hideDragShowOriginalElements(); SelectionManager.SetIsDragging(false); - options ?.dragComplete ?.(new DragCompleteEvent(true, dragData)); + 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)); + options?.dragComplete?.(new DragCompleteEvent(false, dragData)); endDrag(); }; document.addEventListener("pointermove", moveHandler, true); @@ -405,7 +408,7 @@ export namespace DragManager { }); if (target) { const complete = new DragCompleteEvent(false, dragData); - finishDrag ?.(complete); + finishDrag?.(complete); console.log(complete.aborted); target.dispatchEvent( new CustomEvent<DropEvent>("dashOnDrop", { diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index d0f1d86cb..3c7caa60b 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -8,7 +8,7 @@ import { ScriptField, ComputedField } from "../../new_fields/ScriptField"; import { RichTextField } from "../../new_fields/RichTextField"; import { ImageField } from "../../new_fields/URLField"; -export function makeTemplate(doc: Doc): boolean { +export function makeTemplate(doc: Doc, first: boolean = true): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0]; const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, ""); @@ -18,9 +18,12 @@ export function makeTemplate(doc: Doc): boolean { if (!StrCast(d.title).startsWith("-")) { any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any; } else if (d.type === DocumentType.COL || d.data instanceof RichTextField) { - any = makeTemplate(d) || any; + any = makeTemplate(d, false) || any; } }); + if (!docs.length && first) { + any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any; + } if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { if (!StrCast(layoutDoc.title).startsWith("-")) { any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc)); diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 071015193..3d8bcbab7 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -22,7 +22,7 @@ import "./DirectoryImportBox.scss"; import { Networking } from "../../Network"; import { BatchedArray } from "array-batcher"; import * as path from 'path'; -import { AcceptibleMedia } from "../../../server/SharedMediaTypes"; +import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes"; const unsupported = ["text/html", "text/plain"]; @@ -107,21 +107,22 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); const batched = BatchedArray.from(validated, { batchSize: 15 }); - const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => { - const formData = new FormData(); - + const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => { batch.forEach(file => { sizes.push(file.size); modifiedDates.push(file.lastModified); - formData.append(Utils.GenerateGuid(), file); }); - - collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData))); + collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch))); runInAction(() => this.completed += batch.length); }); - await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => { - const path = Utils.prepend(clientAccessPath); + await Promise.all(uploads.map(async response => { + const { source: { type }, result } = response; + if (result instanceof Error) { + return; + } + const { accessPaths, exifData } = result; + const path = Utils.prepend(accessPaths.agnostic.client); const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name }); const { data, error } = exifData; if (document) { diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index ff909cc6b..ab8c73d15 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -24,7 +24,7 @@ export namespace ImageUtils { const proto = Doc.GetProto(document); proto["data-nativeWidth"] = nativeWidth; proto["data-nativeHeight"] = nativeHeight; - proto.contentSize = contentSize; + proto.contentSize = contentSize ? contentSize : undefined; return data !== undefined; }; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 5f3667acc..4457f41e2 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -40,7 +40,7 @@ export class LinkManager { public getAllLinks(): Doc[] { const ldoc = LinkManager.Instance.LinkManagerDoc; if (ldoc) { - const docs = DocListCast(ldoc.allLinks); + const docs = DocListCast(ldoc.data); return docs; } return []; @@ -50,7 +50,7 @@ export class LinkManager { const linkList = LinkManager.Instance.getAllLinks(); linkList.push(linkDoc); if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + LinkManager.Instance.LinkManagerDoc.data = new List<Doc>(linkList); return true; } return false; @@ -62,7 +62,7 @@ export class LinkManager { if (index > -1) { linkList.splice(index, 1); if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList); + LinkManager.Instance.LinkManagerDoc.data = new List<Doc>(linkList); return true; } } @@ -135,35 +135,13 @@ export class LinkManager { return DocListCast(linkDoc.anchor2Groups); } } - - // sets the groups of the given anchor in the given link - public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) { - if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { - linkDoc.anchor1Groups = new List<Doc>(groups); - } else { - linkDoc.anchor2Groups = new List<Doc>(groups); - } - } - public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) { - const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); - const index = groups.findIndex(gDoc => { - return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase(); - }); - if (index > -1 && replace) { - groups[index] = groupDoc; - } - if (index === -1) { - groups.push(groupDoc); - } - LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups); + linkDoc.linkRelationship = groupDoc.linkRelationship; } // removes group doc of given group type only from given anchor on given link public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) { - const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor); - const newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase()); - LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups); + linkDoc.linkRelationship = "-ungrouped-"; } // returns map of group type to anchor's links in that group type @@ -171,19 +149,10 @@ export class LinkManager { const related = this.getAllRelatedLinks(anchor); const anchorGroups = new Map<string, Array<Doc>>(); related.forEach(link => { - const groups = LinkManager.Instance.getAnchorGroups(link, anchor); - - if (groups.length > 0) { - groups.forEach(groupDoc => { - const groupType = StrCast(groupDoc.type); - if (groupType === "") { - const group = anchorGroups.get("*"); - anchorGroups.set("*", group ? [...group, link] : [link]); - } else { - const group = anchorGroups.get(groupType); - anchorGroups.set(groupType, group ? [...group, link] : [link]); - } - }); + if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") { + const group = anchorGroups.get(StrCast(link.linkRelationship)); + anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]); + } else { // if link is in no groups then put it in default group const group = anchorGroups.get("*"); @@ -215,10 +184,7 @@ export class LinkManager { const md: Doc[] = []; const allLinks = LinkManager.Instance.getAllLinks(); allLinks.forEach(linkDoc => { - const anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null)); - const anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null)); - anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); - anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } }); + if (StrCast(linkDoc.linkRelationship).toUpperCase() === groupType.toUpperCase()) { md.push(linkDoc); } }); return md; } diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index da3815181..ec5d1e72a 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -6,6 +6,11 @@ import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "./SelectionManager"; +import { Docs } from "../documents/Documents"; +import { NumCast, BoolCast, Cast } from "../../new_fields/Types"; +import { Doc } from "../../new_fields/Doc"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { Id } from "../../new_fields/FieldSymbols"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -25,7 +30,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) }); return tx2; }; -export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap { +export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { const keys: { [key: string]: any } = {}; function bind(key: string, cmd: any) { @@ -144,13 +149,43 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: console.log("bullet demote fail"); } }); + bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + const layoutDoc = props.Document; + const originalDoc = layoutDoc.expandedTemplate || layoutDoc; + if (originalDoc instanceof Doc) { + const newDoc = Docs.Create.TextDocument("", { + title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), + x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) + }); + FormattedTextBox.SelectOnLoad = newDoc[Id]; + originalDoc instanceof Doc && props.addDocument(newDoc); + } + }); const splitMetadata = (marks: any, tx: Transaction) => { marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); return tx; }; + const addTextOnRight = (force: boolean) => { + const layoutDoc = props.Document; + const originalDoc = layoutDoc.expandedTemplate || layoutDoc; + if (force || props.Document._singleLine) { + const newDoc = Docs.Create.TextDocument("", { + title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), + x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) + }); + FormattedTextBox.SelectOnLoad = newDoc[Id]; + props.addDocument(newDoc); + return true; + } + return false; + } + bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + return addTextOnRight(true); + }); bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + if (addTextOnRight(false)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!splitListItem(schema.nodes.list_item)(state, dispatch)) { if (!splitBlockKeepMarks(state, (tx3: Transaction) => { @@ -175,13 +210,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: }); const path = (state.doc.resolve(state.selection.from - 1) as any).path; const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1; - const textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end); - const text = range ? state.doc.textBetween(textsel.from, textsel.to) : ""; - let whitespace = text.length - 1; - for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { } - if (text.endsWith(":")) { - dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any). - addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any)); + const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator; + if (anchor >= 0) { + const textsel = TextSelection.create(state.doc, anchor, range!.end); + const text = range ? state.doc.textBetween(textsel.from, textsel.to) : ""; + let whitespace = text.length - 1; + for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { } + if (text.endsWith(":")) { + dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any). + addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any)); + } } return false; }); diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx index e07efe056..460f1fa37 100644 --- a/src/client/util/RichTextMenu.tsx +++ b/src/client/util/RichTextMenu.tsx @@ -8,11 +8,10 @@ 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 { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons"; import { updateBullets } from "./ProsemirrorExampleTransfer"; import { FieldViewProps } from "../views/nodes/FieldView"; -import { NumCast, Cast, StrCast } from "../../new_fields/Types"; +import { Cast, StrCast } from "../../new_fields/Types"; import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox"; import { unimplementedFunction, Utils } from "../../Utils"; import { wrapInList } from "prosemirror-schema-list"; @@ -24,7 +23,7 @@ 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); +library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); @observer export default class RichTextMenu extends AntimodeMenu { @@ -41,6 +40,7 @@ export default class RichTextMenu extends AntimodeMenu { private fontColors: (string | undefined)[]; private highlightColors: (string | undefined)[]; + @observable private collapsed: boolean = false; @observable private boldActive: boolean = false; @observable private italicsActive: boolean = false; @observable private underlineActive: boolean = false; @@ -275,6 +275,7 @@ export default class RichTextMenu extends AntimodeMenu { } destroy() { + this.fadeOut(true); } @action @@ -755,9 +756,18 @@ export default class RichTextMenu extends AntimodeMenu { } } + @action + protected toggleCollapse = (e: React.MouseEvent) => { + this.collapsed = !this.collapsed; + setTimeout(() => { + const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width); + RichTextMenu.Instance.jumpTo(x, this._top); + }, 0); + } + render() { - const row1 = <div className="antimodeMenu-row" key="row1">{[ + const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[ 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)), @@ -772,13 +782,18 @@ export default class RichTextMenu extends AntimodeMenu { ]}</div>; const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2"> - <div key="row"> + <div key="row" style={{ display: this.collapsed ? "none" : undefined }}> {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]} </div> <div key="button"> - <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <div key="collapser"> + <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> + <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transition: "transform 0.3s", transform: this.collapsed ? "rotate(180deg)" : "" }} /> + </button> + </div> + <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> {this.getDragger()} diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 8411cc6ee..af3b1a81e 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -1,289 +1,302 @@ -import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules"; -import { schema } from "./RichTextSchema"; -import { wrappingInputRule } from "./prosemirrorPatches"; +import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules"; import { NodeSelection, TextSelection } from "prosemirror-state"; -import { StrCast, Cast, NumCast } from "../../new_fields/Types"; -import { Doc, DataSym } from "../../new_fields/Doc"; -import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { Docs, DocUtils } from "../documents/Documents"; +import { DataSym, Doc } from "../../new_fields/Doc"; import { Id } from "../../new_fields/FieldSymbols"; -import { DocServer } from "../DocServer"; +import { ComputedField } from "../../new_fields/ScriptField"; +import { Cast, NumCast } from "../../new_fields/Types"; import { returnFalse, Utils } from "../../Utils"; +import { DocServer } from "../DocServer"; +import { Docs, DocUtils } from "../documents/Documents"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { wrappingInputRule } from "./prosemirrorPatches"; import RichTextMenu from "./RichTextMenu"; -import { RichTextField } from "../../new_fields/RichTextField"; -import { ComputedField } from "../../new_fields/ScriptField"; +import { schema } from "./RichTextSchema"; -export const inpRules = { - rules: [ - ...smartQuotes, - ellipsis, - emDash, +export class RichTextRules { + public Document: Doc; + public TextBox: FormattedTextBox; + public EnteringStyle: boolean = false; + constructor(doc: Doc, textBox: FormattedTextBox) { + this.Document = doc; + this.TextBox = textBox; + } + public inpRules = { + rules: [ + ...smartQuotes, + ellipsis, + emDash, - // > blockquote - wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote), + // > blockquote + wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote), - // 1. ordered list - wrappingInputRule( - /^1\.\s$/, - schema.nodes.ordered_list, - () => { - return ({ mapStyle: "decimal", bulletStyle: 1 }); - }, - (match: any, node: any) => { - return node.childCount + node.attrs.order === +match[1]; - }, - (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } }) - ), - // a. alphabbetical list - wrappingInputRule( - /^a\.\s$/, - schema.nodes.ordered_list, - // match => { - () => { - return ({ mapStyle: "alpha", bulletStyle: 1 }); - // return ({ order: +match[1] }) - }, - (match: any, node: any) => { - return node.childCount + node.attrs.order === +match[1]; - }, - (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } }) - ), + // 1. ordered list + wrappingInputRule( + /^1\.\s$/, + schema.nodes.ordered_list, + () => { + return ({ mapStyle: "decimal", bulletStyle: 1 }); + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } }) + ), + // a. alphabbetical list + wrappingInputRule( + /^a\.\s$/, + schema.nodes.ordered_list, + // match => { + () => { + return ({ mapStyle: "alpha", bulletStyle: 1 }); + // return ({ order: +match[1] }) + }, + (match: any, node: any) => { + return node.childCount + node.attrs.order === +match[1]; + }, + (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } }) + ), - // * bullet list - wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), + // * bullet list + wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), - // ``` code block - textblockTypeInputRule(/^```$/, schema.nodes.code_block), + // ``` code block + textblockTypeInputRule(/^```$/, schema.nodes.code_block), - // # heading - textblockTypeInputRule( - new RegExp(/^(#{1,6})\s$/), - schema.nodes.heading, - match => { - return ({ level: match[1].length }); - } - ), + // # heading + textblockTypeInputRule( + new RegExp(/^(#{1,6})\s$/), + schema.nodes.heading, + match => { + return ({ level: match[1].length }); + } + ), - // set the font size using #<font-size> - new InputRule( - new RegExp(/%([0-9]+)\s$/), - (state, match, start, end) => { - const size = Number(match[1]); - return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); - }), + // set the font size using #<font-size> + new InputRule( + new RegExp(/%([0-9]+)\s$/), + (state, match, start, end) => { + const size = Number(match[1]); + return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); + }), - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - new InputRule( - new RegExp(/\[\[([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\]\]$/), - (state, match, start, end) => { - if (!match[2]) { - const docId = match[1]; - DocServer.GetRefField(docId).then(docx => { - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true, }, docId); - DocUtils.Publish(target, docId, returnFalse, returnFalse); - DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", ""); - }); - const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docId), location: "onRight", title: docId, targetId: docId }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); - } - const fieldView = state.schema.nodes.dashField.create({ fieldKey: match[2]?.substring(1), docid: match[1] }); - return state.tr.deleteRange(start, end).insert(start, fieldView); - }), - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - new InputRule( - new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/), - (state, match, start, end) => { - const docId = match[1]; - DocServer.GetRefField(docId).then(docx => { - if (!(docx instanceof Doc && docx)) { - const docx = Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true }, docId); - DocUtils.Publish(docx, docId, returnFalse, returnFalse); + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc + new InputRule( + new RegExp(/\[\[([a-zA-Z_#@\? \-0-9]*)(:[a-zA-Z_#@\? \-0-9]+)?\]\]$/), + (state, match, start, end) => { + const fieldKey = match[1]; + const docid = match[2]?.substring(1); + if (!fieldKey) { + if (docid) { + DocServer.GetRefField(docid).then(docx => { + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); + DocUtils.Publish(target, docid, returnFalse, returnFalse); + DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal link", ""); + }); + const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); + } + return state.tr; } - }); - const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid: docId, float: "right", fieldKey: match[2]?.substring(1), alias: Utils.GenerateGuid() }); - const sm = state.storedMarks || undefined; - return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - }), - new InputRule( - new RegExp(/##$/), - (state, match, start, end) => { - const schemaDoc = Doc.GetDataDoc((schema as any).Document); - const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!); - const numInlines = NumCast(textDoc.inlineTextCount); - textDoc.inlineTextCount = numInlines + 1; - const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to - const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" }); - textDocInline.title = inlineFieldKey; // give the annotation its own title - textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc - textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point - textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name }); - textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text - textDoc[inlineFieldKey] = ""; // set a default value for the annotation - const node = (state.doc.resolve(start) as any).nodeAfter; - const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] }); - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" }); - const sm = state.storedMarks || undefined; - 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; - }), - // stop using active style - new InputRule( - new RegExp(/%%$/), - (state, match, start, end) => { - 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; - }), + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + return state.tr.deleteRange(start, end).insert(start, fieldView); + }), + // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc + new InputRule( + new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/), + (state, match, start, end) => { + const fieldKey = match[1]; + const docid = match[2]?.substring(1); + if (!fieldKey && !docid) return state.tr; + docid && DocServer.GetRefField(docid).then(docx => { + if (!(docx instanceof Doc && docx)) { + const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); + DocUtils.Publish(docx, docid, returnFalse, returnFalse); + } + }); + const node = (state.doc.resolve(start) as any).nodeAfter; + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey, float: "right", alias: Utils.GenerateGuid() }); + const sm = state.storedMarks || undefined; + return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; + }), + new InputRule( + new RegExp(/##$/), + (state, match, start, end) => { + const textDoc = this.Document[DataSym]; + const numInlines = NumCast(textDoc.inlineTextCount); + textDoc.inlineTextCount = numInlines + 1; + const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to + const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation + const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" }); + textDocInline.title = inlineFieldKey; // give the annotation its own title + textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc + textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point + textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] + textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name }); + textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text + textDoc[inlineFieldKey] = ""; // set a default value for the annotation + const node = (state.doc.resolve(start) as any).nodeAfter; + const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" }); + const sm = state.storedMarks || undefined; + 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; + }), + // stop using active style + new InputRule( + new RegExp(/%%$/), + (state, match, start, end) => { + 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(/[ti!x]$/), - (state, match, start, end) => { - 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; - 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 Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/[ti!x]$/), + (state, match, start, end) => { + if (state.selection.to === state.selection.from || !this.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; + 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; + // 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("%") && !this.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; - }), + 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(/(%h|h)$/), - (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; + // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/(%h|h)$/), + (state, match, start, end) => { + if (!match[0].startsWith("%") && !this.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(/(%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; + }), + // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/(%q|q)$/), + (state, match, start, end) => { + if (!match[0].startsWith("%") && !this.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 }); } - } - return null; - }), + 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); - }), - // left 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); - }), - // right 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); - }), - new InputRule( - new RegExp(/%\(/), - (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - 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, end, - schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); - }), - new InputRule( - new RegExp(/%\)/), - (state, match, start, end) => { - return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); - }), - new InputRule( - new RegExp(/%f$/), - (state, match, start, end) => { - const newNode = schema.nodes.footnote.create({}); - const tr = state.tr; - tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote. - return tr.setSelection(new NodeSelection( // select the footnote node to open its display - 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))); - }), + // 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + }), + // left 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + }), + // right 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 replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + }), + new InputRule( + new RegExp(/%\(/), + (state, match, start, end) => { + const node = (state.doc.resolve(start) as any).nodeAfter; + 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, end, + schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); + }), + new InputRule( + new RegExp(/%\)/), + (state, match, start, end) => { + return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); + }), + new InputRule( + new RegExp(/%f$/), + (state, match, start, end) => { + const newNode = schema.nodes.footnote.create({}); + const tr = state.tr; + tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote. + return tr.setSelection(new NodeSelection( // select the footnote node to open its display + 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))); + }), - // 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 = RichTextMenu.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; - }), - ] -}; + // 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 = RichTextMenu.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 287a1049b..4a80a1af8 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,27 +1,31 @@ -import { reaction, IReactionDisposer, observable, runInAction } from "mobx"; +import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { baseKeymap, toggleMark } from "prosemirror-commands"; import { redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state"; +import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state"; import { StepMap } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import { Doc, WidthSym, HeightSym, DataSym, Field } from "../../new_fields/Doc"; +import { Doc, Field, HeightSym, WidthSym, DocListCast } from "../../new_fields/Doc"; +import { Id } from "../../new_fields/FieldSymbols"; +import { ObjectField } from "../../new_fields/ObjectField"; +import { ComputedField } from "../../new_fields/ScriptField"; +import { BoolCast, NumCast, StrCast, Cast } from "../../new_fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { DocumentView } from "../views/nodes/DocumentView"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; import { DocumentManager } from "./DocumentManager"; import ParagraphNodeSpec from "./ParagraphNodeSpec"; import { Transform } from "./Transform"; import React = require("react"); -import { BoolCast, NumCast, StrCast } from "../../new_fields/Types"; -import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { ObjectField } from "../../new_fields/ObjectField"; -import { ComputedField } from "../../new_fields/ScriptField"; -import { observer } from "mobx-react"; -import { Id } from "../../new_fields/FieldSymbols"; +import { CollectionSchemaBooleanCell } from "../views/collections/CollectionSchemaCells"; +import { ContextMenu } from "../views/ContextMenu"; +import { ContextMenuProps } from "../views/ContextMenuItem"; +import { Docs } from "../documents/Documents"; +import { CollectionView } from "../views/collections/CollectionView"; const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -134,6 +138,7 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { src: {}, + agnostic: { default: null }, width: { default: 100 }, alt: { default: null }, title: { default: null }, @@ -613,7 +618,7 @@ export class ImageResizeView { DocServer.GetRefField(node.attrs.docid).then(async linkDoc => (linkDoc instanceof Doc) && DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document, - document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false)); + document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false)); } }; this._handle.onpointerdown = function (e: any) { @@ -723,7 +728,7 @@ export class DashDocView { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer); 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; + contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? 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; @@ -731,6 +736,7 @@ export class DashDocView { this._outer = document.createElement("span"); this._outer.style.position = "relative"; this._outer.style.textIndent = "0"; + this._outer.style.border = "1px solid " + StrCast(tbox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); this._outer.style.width = node.attrs.width; this._outer.style.height = node.attrs.height; this._outer.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -766,8 +772,8 @@ export class DashDocView { if (!(dashDoc instanceof Doc)) { alias && DocServer.GetRefField(docid).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const aliasedDoc = Doc.MakeDelegate(dashDocBase, docid + alias); - aliasedDoc.layoutKey = "layout_" + node.attrs.fieldKey; + const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias); + aliasedDoc.layoutKey = node.attrs.fieldKey === "layout" ? "layout" : "layout" + (node.attrs.fieldKey ? "_" + node.attrs.fieldKey : ""); self.doRender(aliasedDoc, removeDoc, node, view, getPos); } }); @@ -790,7 +796,6 @@ export class DashDocView { } doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) { this._dashDoc = dashDoc; - dashDoc._hideSidebar = true; if (node.attrs.width !== dashDoc._width + "px" || node.attrs.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" })); @@ -807,13 +812,14 @@ export class DashDocView { if (finalLayout !== dashDoc && finalKey) { const finalLayoutField = finalLayout[finalKey]; if (finalLayoutField instanceof ObjectField) { - finalLayout._textTemplate = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name }); + finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name }); } } - this._reactionDisposer && this._reactionDisposer(); - this._reactionDisposer = reaction(() => [finalLayout[WidthSym](), finalLayout[HeightSym]()], (dim) => { - this._dashSpan.style.width = this._outer.style.width = dim[0] + "px"; - this._dashSpan.style.height = this._outer.style.height = dim[1] + "px"; + this._reactionDisposer?.(); + this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => { + this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px"; + this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; + this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); ReactDOM.render(<DocumentView Document={finalLayout} @@ -825,7 +831,7 @@ export class DashDocView { ScreenToLocalTransform={this.getDocTransform} addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} - renderDepth={1} + renderDepth={self._textBox.props.renderDepth + 1} PanelWidth={finalLayout[WidthSym]} PanelHeight={finalLayout[HeightSym]} focus={this.outerFocus} @@ -843,30 +849,87 @@ export class DashDocView { } } destroy() { - this._reactionDisposer && this._reactionDisposer(); + this._reactionDisposer?.(); } } export class DashFieldView { - _fieldWrapper: HTMLDivElement; - _labelSpan: HTMLSpanElement; - _fieldSpan: HTMLSpanElement; + _fieldWrapper: HTMLDivElement; // container for label and value + _labelSpan: HTMLSpanElement; // field label + _fieldSpan: HTMLDivElement; // field value _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; @observable _dashDoc: Doc | undefined; + _fieldKey: string; + _options: Doc[] = []; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + this._fieldKey = node.attrs.fieldKey; this._textBoxDoc = tbox.props.Document; this._fieldWrapper = document.createElement("div"); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.position = "relative"; - this._fieldWrapper.style.display = "inline"; + this._fieldWrapper.style.display = "inline-block"; - this._fieldSpan = document.createElement("span"); + const self = this; + this._fieldSpan = document.createElement("div"); + this._fieldSpan.id = Utils.GenerateGuid(); + this._fieldSpan.contentEditable = "true"; this._fieldSpan.style.position = "relative"; - this._fieldSpan.style.display = "inline"; + this._fieldSpan.style.display = "inline-block"; + this._fieldSpan.style.minWidth = "50px"; + this._fieldSpan.style.backgroundColor = "rgba(155, 155, 155, 0.24)"; + this._fieldSpan.onkeypress = function (e: any) { e.stopPropagation(); }; + this._fieldSpan.onkeyup = function (e: any) { e.stopPropagation(); }; + this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); }; + this._fieldSpan.oncontextmenu = function (e: any) { + ContextMenu.Instance.addItem({ + description: "Show Enumeration Templates", event: () => { + e.stopPropagation(); + DocServer.GetRefField(node.attrs.fieldKey).then(collview => collview instanceof Doc && tbox.props.addDocTab(collview, "onRight")); + }, icon: "expand-arrows-alt" + }); + }; + + const setDashDoc = (doc: Doc) => { + self._dashDoc = doc; + if (this._dashDoc && self._options?.length && !this._dashDoc[node.attrs.fieldKey]) { + this._dashDoc[node.attrs.fieldKey] = StrCast(self._options[0].title); + } + } + this._fieldSpan.onkeydown = function (e: any) { + e.stopPropagation(); + if ((e.key === "a" && e.ctrlKey) || (e.key === "a" && e.metaKey)) { + if (window.getSelection) { + const range = document.createRange(); + range.selectNodeContents(self._fieldSpan); + window.getSelection()!.removeAllRanges(); + window.getSelection()!.addRange(range); + } + e.preventDefault(); + } + if (e.key === "Enter") { + e.preventDefault(); + if (e.ctrlKey) { + Doc.addEnumerationToTextField(self._textBoxDoc, node.attrs.fieldKey, [Docs.Create.TextDocument(self._fieldSpan.innerText, { title: self._fieldSpan.innerText })]); + } + let newText = self._fieldSpan.innerText.startsWith(":=") ? ":=-computed-" : self._fieldSpan.innerText; + // look for a document whose id === the fieldKey being displayed. If there's a match, then that document + // holds the different enumerated values for the field in the titles of its collected documents. + // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. + + // alternatively, if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key + DocServer.GetRefField(node.attrs.fieldKey).then(options => { + (options instanceof Doc) && DocListCast(options.data).forEach(opt => StrCast(opt.title).startsWith(newText) && (newText = StrCast(opt.title))); + self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = newText; + if (newText.startsWith(":=") && self._dashDoc && e.data === null && !e.inputType.includes("delete")) { + Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2)); + } + }); + } + }; this._labelSpan = document.createElement("span"); this._labelSpan.style.position = "relative"; @@ -875,13 +938,15 @@ export class DashFieldView { this._labelSpan.style.fontSize = "larger"; this._labelSpan.innerHTML = `${node.attrs.fieldKey}: `; if (node.attrs.docid) { - const self = this; - DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => self._dashDoc = dashDoc)); + DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => setDashDoc(dashDoc))); } else { - this._dashDoc = tbox.props.DataDoc || tbox.dataDoc; + setDashDoc(tbox.props.DataDoc || tbox.dataDoc); } this._reactionDisposer?.(); - this._reactionDisposer = reaction(() => this._dashDoc?.[node.attrs.fieldKey], fval => this._fieldSpan.innerHTML = Field.toString(fval as Field), { fireImmediately: true }); + this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes + const dashVal = this._dashDoc?.[node.attrs.fieldKey]; + return StrCast(dashVal).startsWith(":=") || !dashVal ? Doc.Layout(tbox.props.Document)[this._fieldKey] : dashVal; + }, fval => this._fieldSpan.innerHTML = Field.toString(fval as Field) || "(null)", { fireImmediately: true }); this._fieldWrapper.appendChild(this._labelSpan); this._fieldWrapper.appendChild(this._fieldSpan); @@ -890,6 +955,7 @@ export class DashFieldView { destroy() { this._reactionDisposer?.(); } + selectNode() { } } export class OrderedListView { @@ -1104,7 +1170,7 @@ const fromJson = schema.nodeFromJSON; schema.nodeFromJSON = (json: any) => { const node = fromJson(json); - if (json.type === schema.marks.summarize.name) { + if (json.type === schema.nodes.summary.name) { node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice); } return node; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 4fd8abb12..0e281e77e 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -3,8 +3,6 @@ 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 { diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 7a0fb0741..6513cb223 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,6 +1,6 @@ @import "../views/globalCssVariables"; -.dialogue-box { +.settings-interface { background-color: whitesmoke !important; color: grey; width: 450px; diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 127f7b798..97f6b79fb 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -207,4 +207,5 @@ declare const Docs: { StackingDocument(documents: Doc[], options?: DocumentOptions): Doc; }; +declare function assignDoc(doc:Doc, field:any, id:any):string; declare function d(...args:any[]):any; |
