aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DictationManager.ts4
-rw-r--r--src/client/util/DocumentManager.ts102
-rw-r--r--src/client/util/DragManager.ts42
-rw-r--r--src/client/util/DropConverter.ts32
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx19
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts2
-rw-r--r--src/client/util/InteractionUtils.tsx18
-rw-r--r--src/client/util/LinkManager.ts54
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts54
-rw-r--r--src/client/util/RichTextMenu.tsx31
-rw-r--r--src/client/util/RichTextRules.ts562
-rw-r--r--src/client/util/RichTextSchema.tsx320
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/util/SearchUtil.ts8
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SettingsManager.scss2
-rw-r--r--src/client/util/type_decls.d1
17 files changed, 743 insertions, 512 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..e0ffaf7e0 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';
@@ -28,7 +28,6 @@ export class DocumentManager {
//private constructor so no other class can create a nodemanager
private constructor() {
- // this.DocumentViews = new Array<DocumentView>();
}
//gets all views
@@ -55,7 +54,7 @@ export class DocumentManager {
}
public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
-
+ if (!id) return undefined;
let toReturn: DocumentView | undefined;
const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
@@ -85,17 +84,24 @@ 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[] = [];
+ // heurstic to return the "best" documents first:
+ // choose an exact match over an alias match
+ // choose documents that have a PanelWidth() over those that don't (the treeview documents have no panelWidth)
+ DocumentManager.Instance.DocumentViews.map(view =>
+ view.props.Document.presBox === undefined && view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view));
DocumentManager.Instance.DocumentViews.map(view =>
- view.props.Document === toFind && toReturn.push(view));
+ view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && 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.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
+ DocumentManager.Instance.DocumentViews.map(view =>
+ view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
@@ -127,20 +133,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);
- let annotatedDoc = await Cast(docView?.props.Document.annotationOn, Doc);
+ const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc, originatingDoc);
+ let annotatedDoc = await Cast(targetDoc.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 +154,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);
@@ -158,18 +164,22 @@ export class DocumentManager {
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
// now find the target document within the context
- setTimeout(() => {
- const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
- if (retryDocView) {
- 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
- }
- highlight();
- }, 0);
+ if (targetDoc.displayTimecode) { // the target should show up once the video scrubs to the display timecode;
+ targetDocContext.currentTimecode = targetDoc.displayTimecode;
+ } else {
+ setTimeout(() => {
+ const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
+ if (retryDocView) {
+ 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))); // 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);
@@ -192,14 +202,19 @@ export class DocumentManager {
const second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
const linkDoc = first.length ? first[0] : second.length ? second[0] : undefined;
const linkFollowDocs = first.length ? [await first[0].anchor2 as Doc, await first[0].anchor1 as Doc] : second.length ? [await second[0].anchor1 as Doc, await second[0].anchor2 as Doc] : undefined;
- const linkFollowDocContexts = first.length ? [await first[0].anchor2Context as Doc, await first[0].anchor1Context as Doc] : second.length ? [await second[0].anchor1Context as Doc, await second[0].anchor2Context as Doc] : [undefined, undefined];
- const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2Timecode), NumCast(first[0].anchor1Timecode)] : second.length ? [NumCast(second[0].anchor1Timecode), NumCast(second[0].anchor2Timecode)] : [undefined, undefined];
+ const linkFollowDocContexts = first.length ? [await first[0].context as Doc, await first[0].context as Doc] : second.length ? [await second[0].context as Doc, await second[0].context as Doc] : [undefined, undefined];
+ const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2_timecode), NumCast(first[0].anchor1_timecode)] : second.length ? [NumCast(second[0].anchor1_timecode), NumCast(second[0].anchor2_timecode)] : [undefined, undefined];
if (linkFollowDocs && linkDoc) {
- const maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+ const maxLocation = StrCast(linkDoc.maximizeLocation, "inTab");
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]);
+ const annotatedDoc = await Cast(target.annotationOn, Doc);
+ if (annotatedDoc) {
+ annotatedDoc.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
+ } else {
+ 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], undefined, doc);
} else if (link) {
DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined);
}
@@ -208,7 +223,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 +236,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 deb2da939..07cd2e98f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,15 +7,18 @@ import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
import { ScriptField } from "../../new_fields/ScriptField";
import { List } from "../../new_fields/List";
import { PrefetchProxy } from "../../new_fields/Proxy";
import { listSpec } from "../../new_fields/Schema";
import { Scripting } from "./Scripting";
import { convertDropDataToButtons } from "./DropConverter";
+import { AudioBox } from "../views/nodes/AudioBox";
+import { DateField } from "../../new_fields/DateField";
+import { DocumentView } from "../views/nodes/DocumentView";
-export type dropActionType = "alias" | "copy" | undefined;
+export type dropActionType = "place" | "alias" | "copy" | undefined;
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
docFunc: () => Doc | Promise<Doc> | undefined,
@@ -130,11 +133,11 @@ export namespace DragManager {
dontHideOnDrop?: boolean;
offset: number[];
dropAction: dropActionType;
+ removeDropProperties?: string[];
userDropAction: dropActionType;
embedDoc?: boolean;
moveDocument?: MoveFunction;
isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
- applyAsTemplate?: boolean;
}
export class LinkDragData {
constructor(linkSourceDoc: Doc) {
@@ -144,6 +147,7 @@ export namespace DragManager {
linkSourceDocument: Doc;
dontClearTextBox?: boolean;
linkDocument?: Doc;
+ linkDropCallback?: (data: LinkDragData) => void;
}
export class ColumnDragData {
constructor(colKey: SchemaHeaderField) {
@@ -180,7 +184,7 @@ export namespace DragManager {
);
}
element.dataset.canDrop = "true";
- const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
+ const handler = (e: Event) => { dropFunc(e, (e as CustomEvent<DropEvent>).detail); };
element.addEventListener("dashOnDrop", handler);
return () => {
element.removeEventListener("dashOnDrop", handler);
@@ -190,14 +194,20 @@ export namespace DragManager {
// 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) {
+ const addAudioTag = (dropDoc: any) => {
+ dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
+ dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline"));
+ return dropDoc;
+ };
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.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(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?.removeDropProperties || []).concat(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);
@@ -206,10 +216,9 @@ export namespace DragManager {
// 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 finishDrag = (e: DragCompleteEvent) => {
- const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: title });
- bd.onClick = ScriptField.MakeScript(script);
+ const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title, isButton: true, 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);
+ initialize?.(bd);
bd.buttonParams = new List<string>(params);
e.docDragData && (e.docDragData.droppedDocuments = [bd]);
};
@@ -217,7 +226,7 @@ export namespace DragManager {
}
// drag links and drop link targets (aliasing them if needed)
- export async function StartLinkTargetsDrag(dragEle: HTMLElement, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
+ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, 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) {
@@ -229,12 +238,11 @@ export namespace DragManager {
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);
+ docView.props.removeDocument?.(doc);
addDocument(doc);
return true;
};
- const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+ const containingView = docView.props.ContainingCollectionView;
const finishDrag = (e: DragCompleteEvent) =>
e.docDragData && (e.docDragData.droppedDocuments =
dragData.draggedDocuments.reduce((droppedDocs, d) => {
@@ -266,6 +274,10 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartImgDrag(ele: HTMLElement, downX: number, downY: number) {
+ StartDrag([ele], {}, downX, downY);
+ }
+
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
console.log("drag");
eles = eles.filter(e => e);
@@ -340,7 +352,7 @@ export namespace DragManager {
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
- dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
+ dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
}
if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
AbortDrag();
@@ -381,8 +393,8 @@ export namespace DragManager {
hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
- options?.dragComplete?.(new DragCompleteEvent(false, dragData));
endDrag();
+ options?.dragComplete?.(new DragCompleteEvent(false, dragData));
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index d0f1d86cb..69dc303cd 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,5 +1,5 @@
import { DragManager } from "./DragManager";
-import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../new_fields/Doc";
import { DocumentType } from "../documents/DocumentTypes";
import { ObjectField } from "../../new_fields/ObjectField";
import { StrCast } from "../../new_fields/Types";
@@ -8,39 +8,57 @@ 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 {
+//
+// converts 'doc' into a template that can be used to render other documents.
+// the title of doc is used to determine which field is being templated, so
+// passing a value for 'rename' allows the doc to be given a meangingful name
+// after it has been converted to
+export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ if (layoutDoc.layout instanceof Doc) { // its already a template
+ return true;
+ }
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
docs.forEach(d => {
if (!StrCast(d.title).startsWith("-")) {
- any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
+ const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "");
+ if (params) {
+ any = makeTemplate(d, false) || any;
+ d.PARAMS = params;
+ } else {
+ 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));
}
}
+ rename && (doc.title = rename);
return any;
}
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
+ if (!doc.onDragStart && !doc.isButtonBar) {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type === DocumentType.COL || layoutDoc.type === DocumentType.TEXT || layoutDoc.type === DocumentType.IMG) {
- makeTemplate(layoutDoc);
+ !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
} else {
(layoutDoc.layout instanceof Doc) && !data.userDropAction;
}
layoutDoc.isTemplateDoc = true;
- dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+ dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
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/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 184e37ba5..2eec02a42 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,3 +1,5 @@
+import React = require("react");
+
export namespace InteractionUtils {
export const MOUSETYPE = "mouse";
export const TOUCHTYPE = "touch";
@@ -90,8 +92,22 @@ export namespace InteractionUtils {
return myTouches;
}
+ // TODO: find a way to reference this function from InkingStroke instead of copy pastign here. copied bc of weird error when on mobile view
+ export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+ const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+ return (
+ <polyline
+ points={pts}
+ style={{
+ fill: "none",
+ stroke: color,
+ strokeWidth: width
+ }}
+ />
+ );
+ }
+
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
- console.log(e.button);
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
case PENTYPE:
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..42247f177 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.rootDocument || 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];
+ 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.rootDocument || 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..3f0ec7aa5 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;
@@ -146,7 +146,7 @@ export default class RichTextMenu extends AntimodeMenu {
public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
if (this.view) {
- const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, 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 || "";
@@ -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..b0a124cb8 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,289 +1,317 @@
-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]*)?(:[a-zA-Z_#@\? \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1];
+ const docid = match[3]?.substring(1);
+ const value = 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 to");
+ });
+ 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;
+ }
+ if (value !== "" && value !== undefined) {
+ this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : value;
}
- });
- 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 tag stored under the '#' field
+ new InputRule(
+ new RegExp(/#([a-zA-Z_\-0-9]+)\s$/),
+ (state, match, start, end) => {
+ const tag = match[1];
+ if (!tag) return state.tr;
+ this.Document[DataSym]["#"] = tag;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ 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-Z0-9…._\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1] || "";
+ const fieldParam = match[2]?.replace("…", "...") || "";
+ const docid = match[3]?.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: fieldKey + fieldParam, 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..094cd58f3 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, DocListCast, Field, HeightSym, WidthSym } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
+import { List } from "../../new_fields/List";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { listSpec } from "../../new_fields/Schema";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
+import { Docs } from "../documents/Documents";
+import { CollectionViewType } from "../views/collections/CollectionView";
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";
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 },
@@ -295,6 +300,7 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
href: {},
targetId: { default: "" },
+ linkId: { default: "" },
showPreview: { default: true },
location: { default: null },
title: { default: null },
@@ -309,7 +315,7 @@ export const marks: { [index: string]: MarkSpec } = {
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, id: node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
}
},
@@ -504,7 +510,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
},
group: "inline",
toDOM(node: any) {
@@ -520,7 +526,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_tag: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
tag: { default: "" }
},
group: "inline",
@@ -613,7 +619,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) {
@@ -717,13 +723,14 @@ export class DashDocView {
_outer: HTMLElement;
_dashDoc: Doc | undefined;
_reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
_textBox: FormattedTextBox;
getDocTransform = () => {
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 +738,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";
@@ -761,13 +769,14 @@ export class DashDocView {
};
const alias = node.attrs.alias;
- const docid = node.attrs.docid || tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+ const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
DocServer.GetRefField(docid + alias).then(async dashDoc => {
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 = "layout";
+ node.attrs.fieldKey && DocumentView.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
self.doRender(aliasedDoc, removeDoc, node, view, getPos);
}
});
@@ -790,106 +799,247 @@ 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" }));
- } catch (e) {
- console.log(e);
- }
- }
const self = this;
- const finalLayout = Doc.expandTemplateLayout(dashDoc, !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.Document) ? this._textBox.dataDoc : undefined);
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
else {
- const layoutKey = StrCast(finalLayout.layoutKey);
- const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
- if (finalLayout !== dashDoc && finalKey) {
- const finalLayoutField = finalLayout[finalKey];
- if (finalLayoutField instanceof ObjectField) {
- finalLayout._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}
- DataDoc={!node.attrs.docid ? this._textBox.dataDoc : undefined}
- LibraryPath={this._textBox.props.LibraryPath}
- fitToBox={BoolCast(dashDoc._fitToBox)}
- addDocument={returnFalse}
- removeDocument={removeDoc}
- ScreenToLocalTransform={this.getDocTransform}
- addDocTab={this._textBox.props.addDocTab}
- pinToPres={returnFalse}
- renderDepth={1}
- PanelWidth={finalLayout[WidthSym]}
- PanelHeight={finalLayout[HeightSym]}
- focus={this.outerFocus}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- dontRegisterView={false}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContentScaling={this.contentScaling}
- />, this._dashSpan);
+ const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ 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" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ };
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+ if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ finalLayout.rootDocument = dashDoc.aliasOf;
+ }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ },
+ (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
+ { fireImmediately: true });
}
}
destroy() {
- this._reactionDisposer && this._reactionDisposer();
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ 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
+ _fieldCheck: HTMLInputElement;
+ _enumerables: 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-flex";
+
+ const self = this;
+ this._enumerables = document.createElement("div");
+ this._enumerables.style.width = "10px";
+ this._enumerables.style.height = "10px";
+ this._enumerables.style.position = "relative";
+ this._enumerables.style.display = "none";
+ this._enumerables.style.background = "dimGray";
+
+ this._enumerables.onpointerdown = async (e) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ collview instanceof Doc && tbox.props.addDocTab(collview, "onRight");
+ };
+ const updateText = (forceMatch: boolean) => {
+ self._enumerables.style.display = "none";
+ const newText = self._fieldSpan.innerText.startsWith(":=") || 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.
+ DocServer.GetRefField(self._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = modText;
+ Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, []);
+ } // 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
+ else if (self._fieldSpan.innerText.startsWith(":=")) {
+ self._dashDoc![self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
+ } else if (self._fieldSpan.innerText.startsWith("=:=")) {
+ Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(3));
+ } else {
+ self._dashDoc![self._fieldKey] = newText;
+ }
+ });
+ };
+
+
+ this._fieldCheck = document.createElement("input");
+ this._fieldCheck.id = Utils.GenerateGuid();
+ this._fieldCheck.type = "checkbox";
+ this._fieldCheck.style.position = "relative";
+ this._fieldCheck.style.display = "none";
+ this._fieldCheck.style.minWidth = "12px";
+ this._fieldCheck.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
+ this._fieldCheck.onchange = function (e: any) {
+ self._dashDoc![self._fieldKey] = e.target.checked;
+ };
- this._fieldSpan = document.createElement("span");
+ 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 = "none";
+ this._fieldSpan.style.minWidth = "12px";
+ 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(); self._enumerables.style.display = "inline-block"; };
+ this._fieldSpan.onblur = function (e: any) { updateText(false); };
+
+ const setDashDoc = (doc: Doc) => {
+ self._dashDoc = doc;
+ if (self._options?.length && !self._dashDoc[self._fieldKey]) {
+ self._dashDoc[self._fieldKey] = StrCast(self._options[0].title);
+ }
+ // NOTE: if the field key starts with "@", then the actual field key is stored in the field 'fieldKey' (removing the @).
+ self._fieldKey = self._fieldKey.startsWith("@") ? StrCast(tbox.props.Document[StrCast(self._fieldKey).substring(1)]) : self._fieldKey;
+ this._labelSpan.innerHTML = `${self._fieldKey}: `;
+ const fieldVal = Cast(this._dashDoc?.[self._fieldKey], "boolean", null);
+ this._fieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(fieldVal === true || fieldVal === false) ? "inline-block" : "none";
+ };
+ 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();
+ e.ctrlKey && Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ updateText(true);
+ }
+ };
this._labelSpan = document.createElement("span");
+ this._labelSpan.style.backgroundColor = "rgba(155, 155, 155, 0.44)";
this._labelSpan.style.position = "relative";
- this._labelSpan.style.display = "inline";
- this._labelSpan.style.fontWeight = "bold";
- this._labelSpan.style.fontSize = "larger";
- this._labelSpan.innerHTML = `${node.attrs.fieldKey}: `;
+ this._labelSpan.style.display = "inline-block";
+ this._labelSpan.style.fontSize = "small";
+ this._labelSpan.title = "click to see related tags";
+ this._labelSpan.onpointerdown = function (e: any) {
+ e.stopPropagation();
+ let container = tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(self._fieldKey) === -1 && list.push(new SchemaHeaderField(self._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = self._fieldKey;
+ tbox.props.addDocTab(alias, "onRight");
+ }
+ };
+ this._labelSpan.innerHTML = `${self._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?.[self._fieldKey];
+ return StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(tbox.props.Document)[self._fieldKey] : dashVal;
+ }, fval => {
+ const boolVal = Cast(fval, "boolean", null);
+ if (boolVal === true || boolVal === false) {
+ this._fieldCheck.checked = boolVal;
+ } else {
+ this._fieldSpan.innerHTML = Field.toString(fval as Field) || "";
+ }
+ this._fieldCheck.style.display = (boolVal === true || boolVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(boolVal === true || boolVal === false) ? "inline-block" : "none";
+ }, { fireImmediately: true });
this._fieldWrapper.appendChild(this._labelSpan);
+ this._fieldWrapper.appendChild(this._fieldCheck);
this._fieldWrapper.appendChild(this._fieldSpan);
+ this._fieldWrapper.appendChild(this._enumerables);
(this as any).dom = this._fieldWrapper;
+ //updateText(false);
}
destroy() {
this._reactionDisposer?.();
}
+ selectNode() { }
}
export class OrderedListView {
@@ -945,12 +1095,12 @@ export class FootnoteView {
"Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
"Mod-b": toggleMark(schema.marks.strong)
}),
- new Plugin({
- view(newView) {
- // TODO -- make this work with RichTextMenu
- // return FormattedTextBox.getToolTip(newView);
- }
- })
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
],
}),
@@ -1104,7 +1254,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/Scripting.ts b/src/client/util/Scripting.ts
index 0fa96963e..ce21b7fa7 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -215,6 +215,8 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+ if (options.params && !options.params.this) options.params.this = Doc.name;
+ if (options.params && !options.params.self) options.params.self = Doc.name;
if (options.globals) {
Scripting.setScriptingGlobals(options.globals);
}
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 64874b994..b597f1e07 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -34,7 +34,7 @@ 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 rpquery = Utils.prepend("/search");
+ const rpquery = Utils.prepend("/dashsearch");
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) {
@@ -52,7 +52,7 @@ export namespace SearchUtil {
const newLines: string[][] = [];
await Promise.all(fileids.map(async (tr: string, i: number) => {
const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- const docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
+ const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
newIds.push(...docResult.ids);
newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
}));
@@ -121,12 +121,12 @@ export namespace SearchUtil {
export async function GetAllDocs() {
const query = "*";
- let response = await rp.get(Utils.prepend('/search'), {
+ const response = await rp.get(Utils.prepend('/dashsearch'), {
qs:
{ start: 0, rows: 10000, q: query },
});
- let result: IdSearchResult = JSON.parse(response);
+ const result: IdSearchResult = JSON.parse(response);
const { ids, numFound, highlighting } = result;
//console.log(ids.length);
const docMap = await DocServer.GetRefFields(ids);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 89b900115..b7d88c54c 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;