aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText
diff options
context:
space:
mode:
authorMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-04-28 17:31:27 -0400
committerMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-04-28 17:31:27 -0400
commit813ac366831c95f3fa11e01b9588cf18cbe466bc (patch)
tree24a98e427543ff57c9396918ff12ae1cf81a5a92 /src/client/views/nodes/formattedText
parentf8503355ff82930e640369637c33d989fd7eaff3 (diff)
parent22fe2791b6a6e92cc4d0ad953363120b51bd6e2c (diff)
Handle merge conflicts with jenny work
Diffstat (limited to 'src/client/views/nodes/formattedText')
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx112
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx10
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts21
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts34
4 files changed, 137 insertions, 40 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 6192b6829..29117794e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,25 +1,25 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap, selectAll } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
import { Fragment, Mark, Node, Slice } from "prosemirror-model";
-import { ReplaceStep } from 'prosemirror-transform';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc";
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from "../../../../fields/RichTextField";
import { RichTextUtils } from '../../../../fields/RichTextUtils';
-import { Cast, DateCast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { Cast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -29,6 +29,7 @@ import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from "../../../util/DragManager";
import { makeTemplate } from '../../../util/DropConverter';
+import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from '../../../util/SnappingManager';
import { undoBatch, UndoManager } from "../../../util/UndoManager";
@@ -38,10 +39,11 @@ import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
import { ViewBoxAnnotatableComponent } from "../../DocComponent";
import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
-import { AudioBox } from '../AudioBox';
import { FieldView, FieldViewProps } from "../FieldView";
import { LinkDocPreview } from '../LinkDocPreview';
import { DashDocCommentView } from "./DashDocCommentView";
@@ -60,9 +62,6 @@ import { schema } from "./schema_rts";
import { SummaryView } from "./SummaryView";
import applyDevTools = require("prosemirror-dev-tools");
import React = require("react");
-import { SidebarAnnos } from '../../SidebarAnnos';
-import { Colors } from '../../global/globalEnums';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
const translateGoogleApi = require("translate-google-api");
export interface FormattedTextBoxProps {
@@ -220,6 +219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return undefined;
});
AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
+ AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
* This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -245,7 +245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._editorView.updateState(state);
const tsel = this._editorView.state.selection.$from;
- tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
+ //tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box
const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
@@ -346,37 +346,68 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ autoLink = () => {
+ if (this._editorView) {
+ const newAutoLinks = new Set<Doc>();
+ const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+ const f = this._editorView.state.selection.from;
+ const t = this._editorView.state.selection.to;
+ var tr = this._editorView.state.tr as any;
+ const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
+ tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
+ DocListCast(Doc.UserDoc().myPublishedDocs).forEach(term => tr = this.hyperlinkTerm(tr, term, newAutoLinks));
+ tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+ this._editorView?.dispatch(tr);
+ oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
+ }
+ }
+
updateTitle = () => {
+ const title = StrCast(this.dataDoc.title);
if (!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
- StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.dataDoc["title-custom"] &&
+ (title.startsWith("-") || title.startsWith("@")) && this._editorView && !this.dataDoc["title-custom"] &&
(Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === "text")) {
let node = this._editorView.state.doc;
while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild;
const str = node.textContent;
- this.dataDoc.title = "-" + str.substr(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : "");
+ const prefix = str.startsWith("@") ? "" : "-";
+ this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : "");
+ if (str.startsWith("@") && str.length > 1) {
+ Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", this.rootDoc);
+ }
}
}
- // needs a better API for taking in a set of words with target documents instead of just one target
- hyperlinkTerms = (terms: string[], target: Doc) => {
- if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
- const res1 = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- let tr = this._editorView.state.tr;
- const flattened1: TextSelection[] = [];
- res1.map(r => r.map(h => flattened1.push(h)));
+ // creates links between terms in a document and documents which have a matching Id
+ hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
+ const editorView = this._editorView;
+ if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) {
+ const autoLinkTerm = StrCast(target.title).replace(/^@/, "");
+ const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
+ var alink: Doc | undefined;
flattened1.forEach((flat, i) => {
- const flattened: TextSelection[] = [];
- const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- res.map(r => r.map(h => flattened.push(h)));
+ const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm);
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
- const anchor = Docs.Create.TextanchorDocument();
- const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!;
- const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }];
- const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location });
- tr = tr.addMark(flattened[i].from, flattened[i].to, link);
+ alink = alink ?? (DocListCast(this.Document.links).find(link =>
+ Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) &&
+ Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target },
+ LinkManager.AutoKeywords)!);
+ newAutoLinks.add(alink);
+ const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
+ const sel = flattened[i];
+ tr = tr.addMark(sel.from, sel.to, splitter);
+ tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
+ if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
+ const allAnchors = [{ href: Doc.localServerPath(target), title: "a link", anchorId: this.props.Document[Id] }];
+ allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
+ const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: "auto term", location: "add:right" });
+ tr = tr.addMark(pos, pos + node.nodeSize, link);
+ }
+ });
+ tr = tr.removeMark(sel.from, sel.to, splitter);
});
- this._editorView.dispatch(tr);
}
+ return tr;
}
highlightSearchTerms = (terms: string[], backward: boolean) => {
if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
@@ -728,7 +759,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
- const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to), unrendered: true });
+ const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: "#" + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -784,6 +815,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
};
let start = 0;
+ this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
if (this._editorView && textAnchorId) {
const editor = this._editorView;
const ret = findAnchorFrag(editor.state.doc.content, editor);
@@ -803,7 +835,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
- return this._focusSpeed;
+ return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed
}
// if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
@@ -829,7 +861,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight));
}, { fireImmediately: true });
- this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+ this._disposers.links = reaction(() => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
this._cachedLinks = newLinks;
@@ -894,6 +926,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
if (this._editorView && selected) {
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
+ this.autoLink();
}
}), { fireImmediately: true });
@@ -1098,6 +1131,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
});
}
+ _didScroll = false;
setupEditor(config: any, fieldKey: string) {
const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]);
const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
@@ -1112,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const scrollRef = self._scrollRef.current;
const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined;
const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined;
- if ((topOff || botOff) && scrollRef) {
+ if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) {
const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale;
if (this._focusSpeed !== undefined) {
@@ -1120,6 +1154,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} else {
scrollRef.scrollTo({ top: scrollPos });
}
+ this._didScroll = true;
}
return true;
},
@@ -1256,7 +1291,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
!this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
while (target && !target.dataset?.targethrefs) target = target.parentElement;
- FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs);
+ FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
}
(e.nativeEvent as any).formattedHandled = true;
@@ -1399,6 +1434,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
onBlur = (e: any) => {
+ this.autoLink();
FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
@@ -1421,6 +1457,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} catch (e: any) { console.log(e.message); }
this._lastText = curText;
}
+ if (StrCast(this.rootDoc.title).startsWith("@") && !this.dataDoc["title-custom"]) {
+ UndoManager.RunInBatch(() => {
+ this.dataDoc["title-custom"] = true;
+ this.dataDoc.showTitle = "title";
+ const tr = this._editorView!.state.tr;
+ this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection());
+ }, "titler");
+ }
}
onKeyDown = (e: React.KeyboardEvent) => {
// single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container)
@@ -1486,7 +1530,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (children) {
const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ if (this.props.setHeight && scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 1fde6e5f0..3e673c0b2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,6 +2,7 @@ import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc } from "../../../../fields/Doc";
+import { DocServer } from "../../../DocServer";
import { LinkDocPreview } from "../LinkDocPreview";
import { FormattedTextBox } from "./FormattedTextBox";
import './FormattedTextBoxComment.scss';
@@ -9,7 +10,7 @@ import { schema } from "./schema_rts";
export function findOtherUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); }
export function findUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); }
-export function findLinkMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.type === schema.marks.linkAnchor); }
+export function findLinkMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.type === schema.marks.autoLinkAnchor || m.type === schema.marks.linkAnchor); }
export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
let before = 0, nbef = rpos.nodeBefore;
while (nbef && finder(nbef.marks)) {
@@ -82,14 +83,14 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.tooltip.style.display = "";
}
- static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = "") {
+ static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = "", linkDoc: string = "") {
FormattedTextBoxComment.textBox = textBox;
if ((hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) {
- FormattedTextBoxComment.setupPreview(view, textBox, hrefs?.trim().split(" ").filter(h => h));
+ FormattedTextBoxComment.setupPreview(view, textBox, hrefs?.trim().split(" ").filter(h => h), linkDoc);
}
}
- static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[]) {
+ static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) {
const state = view.state;
// this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
if (state.selection.$from) {
@@ -115,6 +116,7 @@ export class FormattedTextBoxComment {
nbef && naft && LinkDocPreview.SetLinkInfo({
docProps: textBox.props,
linkSrc: textBox.rootDoc,
+ linkDoc: linkDoc ? DocServer.GetCachedRefField(linkDoc) as Doc : undefined,
location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
hrefs,
showHeader: true
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index bafae84dc..427e05edb 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -275,7 +275,7 @@ export class RichTextRules {
this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
}
const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined);
+ DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to:portal from", undefined);
const fstate = this.TextBox.EditorView?.state;
if (fstate && selection) {
@@ -294,6 +294,25 @@ export class RichTextRules {
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
+ // wiki:title
+ new InputRule(
+ new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/),
+ (state, match, start, end) => {
+ const title = match[1];
+ this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))));
+
+ this.TextBox.makeLinkAnchor(undefined, "add:right", `https://en.wikipedia.org/wiki/${title.trim()}`, "wikipedia reference");
+
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate) {
+ const tr = fstate?.tr.deleteRange(start, start + 5);
+ return tr.setSelection(new TextSelection(tr.doc.resolve(end - 5))).insertText(" ");
+ }
+ return state.tr;
+ }),
+
// create an inline view of a document {{ <layoutKey> : <Doc> }}
// {{:Doc}} => show default view of document
// {{<layout>}} => show layout for this doc
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 6103a28d6..1f6ce014f 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -17,7 +17,39 @@ export const marks: { [index: string]: MarkSpec } = {
return ["div", { className: "dummy" }, 0];
}
},
- // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each link has an href URL and a title for use in menus and hover (Dash links have linkIDs & targetIDs). `title`
+
+
+ // :: MarkSpec an autoLinkAnchor. These are automatically generated anchors to "published" documents based on the anchor text matching the
+ // published document's title.
+ // NOTE: unlike linkAnchors, the autoLinkAnchor's href's indicate the target anchor of the hyperlink and NOT the source. This is because
+ // automatic links do not create a text selection Marker document for the source anchor, but use the text document itself. Since
+ // multiple automatic links can be created each having the same source anchor (the whole document), the target href of the link is needed to
+ // disambiguate links from one another.
+ // Rendered and parsed as an `<a>`
+ // element.
+ autoLinkAnchor: {
+ attrs: {
+ allAnchors: { default: [] as { href: string, title: string, anchorId: string }[] },
+ location: { default: null },
+ title: { default: null },
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return {
+ location: dom.getAttribute("location"),
+ title: dom.getAttribute("title")
+ };
+ }
+ }],
+ toDOM(node: any) {
+ const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.href : item.href, "");
+ const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.anchorId : item.anchorId, "");
+ return ["a", { class: anchorids, "data-targethrefs": targethrefs, "data-linkdoc": node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0];
+ }
+ },
+ // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each linkAnchor specifies an href to the URL of the source selection Marker text,
+ // and a title for use in menus and hover. `title`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
linkAnchor: {