aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/formattedText
diff options
context:
space:
mode:
authorMelissa Zhang <mzhang19096@gmail.com>2020-07-03 17:32:40 -0700
committerMelissa Zhang <mzhang19096@gmail.com>2020-07-03 17:56:11 -0700
commit58b780563c7fc4a1496f5c676f2d14faddb096e0 (patch)
treeda4779eb6168ea931378cca2fd91a399494b7b81 /src/client/views/nodes/formattedText
parent1934623881cf548e4dbe8e70eb2995ec2f6b8399 (diff)
merge with master
Diffstat (limited to 'src/client/views/nodes/formattedText')
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss297
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx66
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx4
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx42
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts20
-rw-r--r--src/client/views/nodes/formattedText/prosemirrorPatches.js55
7 files changed, 423 insertions, 63 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 678494b27..0a094ba6a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -37,6 +37,7 @@
position: absolute;
}
}
+
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -72,7 +73,7 @@
.collectionfreeformview-container {
position: relative;
}
-
+
>.formattedTextBox-sidebar-handle {
right: unset;
left: -5;
@@ -95,7 +96,7 @@
.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected,
.formattedTextBox-inner, .formattedTextBox-inner-selected {
height: 100%;
- white-space: pre-wrap;
+ white-space: pre-wrap;
.ProseMirror:hover {
background: rgba(200,200,200,0.8);
}
@@ -262,19 +263,19 @@ footnote::after {
border:unset;
padding:0px;
}
-
+
.prosemirror-links a {
float: left;
color: white;
text-decoration: none;
border-radius: 3px;
}
-
+
.prosemirror-links a:hover {
background-color: #eee;
color: black;
}
-
+
.prosemirror-anchor:hover .prosemirror-links {
display: grid;
}
@@ -321,7 +322,7 @@ footnote::after {
.multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
.multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
.multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
-
+
.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
.decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
@@ -331,7 +332,7 @@ footnote::after {
.decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
.decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
.decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
-
+
.multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
.multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
.multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
@@ -346,4 +347,284 @@ footnote::after {
.ProseMirror:hover {
background: unset;
}
-} \ No newline at end of file
+}
+
+@media only screen and (max-width: 1000px) {
+ @import "../../globalCssVariables";
+
+ .ProseMirror {
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+ }
+
+ .ProseMirror:focus {
+ outline: none !important;
+ }
+
+ .formattedTextBox-cont {
+ touch-action: none;
+ cursor: text;
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $intermediate-color;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: initial;
+ max-height: 100%;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+
+ .formattedTextBox-dictation {
+ height: 12px;
+ width: 10px;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
+ }
+
+ .formattedTextBox-outer {
+ position: relative;
+ overflow: auto;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ }
+
+ .formattedTextBox-sidebar-handle {
+ position: absolute;
+ top: calc(50% - 17.5px);
+ width: 10px;
+ height: 35px;
+ background: lightgray;
+ border-radius: 20px;
+ cursor:grabbing;
+ }
+
+ .formattedTextBox-cont>.formattedTextBox-sidebar-handle {
+ right: 0;
+ left: unset;
+ }
+
+ .formattedTextBox-sidebar,
+ .formattedTextBox-sidebar-inking {
+ border-left: dashed 1px black;
+ height: 100%;
+ display: inline-block;
+ position: absolute;
+ right: 0;
+
+ .collectionfreeformview-container {
+ position: relative;
+ }
+
+ >.formattedTextBox-sidebar-handle {
+ right: unset;
+ left: -5;
+ }
+ }
+
+ .formattedTextBox-sidebar-inking {
+ pointer-events: all;
+ }
+
+ .formattedTextBox-inner-rounded {
+ height: 70%;
+ width: 85%;
+ position: absolute;
+ overflow: auto;
+ top: 15%;
+ left: 10%;
+ }
+
+ .formattedTextBox-inner-rounded,
+ .formattedTextBox-inner {
+ height: 100%;
+ white-space: pre-wrap;
+ hr {
+ display: block;
+ unicode-bidi: isolate;
+ margin-block-start: 0.5em;
+ margin-block-end: 0.5em;
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ overflow: hidden;
+ border-style: inset;
+ border-width: 1px;
+ }
+ }
+
+ // .menuicon {
+ // display: inline-block;
+ // border-right: 1px solid rgba(0, 0, 0, 0.2);
+ // color: #888;
+ // line-height: 1;
+ // padding: 0 7px;
+ // margin: 1px;
+ // cursor: pointer;
+ // text-align: center;
+ // min-width: 1.4em;
+ // }
+
+ .strong,
+ .heading {
+ font-weight: bold;
+ }
+
+ .em {
+ font-style: italic;
+ }
+
+ .userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+ }
+
+ .userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 10px;
+ min-height: 10px;
+ text-align: center;
+ align-content: center;
+ }
+
+ footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+
+ div {
+ padding: 0 !important;
+ }
+ }
+
+ footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+ }
+
+ .ProseMirror {
+ counter-reset: prosemirror-footnote;
+ }
+
+ .footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+ }
+
+ .prosemirror-attribution {
+ font-size: 8px;
+ }
+
+ .footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+ }
+
+
+ .formattedTextBox-inlineComment {
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::before {
+ content: "→";
+ }
+ &:hover {
+ background: orange;
+ }
+ }
+
+ .formattedTextBox-summarizer {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "←";
+ }
+ }
+
+ .formattedTextBox-summarizer-collapsed {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "...";
+ }
+ }
+
+ .ProseMirror {
+ touch-action: none;
+ span {
+ font-family: inherit;
+ }
+
+ ol, ul {
+ counter-reset: deci1 0 multi1 0;
+ padding-left: 1em;
+ font-family: inherit;
+ }
+ ol {
+ margin-left: 1em;
+ font-family: inherit;
+ }
+
+ .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; }
+ .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;}
+ .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;}
+ .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; }
+
+ .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em }
+ .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
+ .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
+
+ .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
+ .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
+ .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
+ .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
+ .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
+ .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
+ .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
+
+ .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
+ .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
+ .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
+ .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
+ }
+}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 664141607..69708e7a0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -8,7 +8,7 @@ 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 { Fragment, Mark, Node, Slice, Schema } from "prosemirror-model";
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
@@ -16,6 +16,7 @@ import { DateField } from '../../../../fields/DateField';
import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import applyDevTools = require("prosemirror-dev-tools");
+import { removeMarkWithAttrs } from "./prosemirrorPatches";
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -56,7 +57,7 @@ import { DocumentButtonBar } from '../../DocumentButtonBar';
import { AudioBox } from '../AudioBox';
import { FieldView, FieldViewProps } from "../FieldView";
import "./FormattedTextBox.scss";
-import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
+import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } from './FormattedTextBoxComment';
import React = require("react");
library.add(faEdit);
@@ -87,6 +88,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _editorView: Opt<EditorView>;
private _applyingChange: boolean = false;
private _searchIndex = 0;
+ private _cachedLinks: Doc[] = [];
private _undoTyping?: UndoManager.Batch;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -141,6 +143,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+ // removes all hyperlink anchors for the removed linkDoc
+ // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one.
+ // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
+ public RemoveLinkFromDoc(linkDoc?: Doc) {
+ const state = this._editorView?.state;
+ if (state && linkDoc && this._editorView) {
+ var allLinks: any[] = [];
+ state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => {
+ const foundMark = findLinkMark(node.marks);
+ const newHrefs = foundMark?.attrs.allLinks.filter((a: any) => a.href.includes(linkDoc[Id])) || [];
+ allLinks = newHrefs.length ? newHrefs : allLinks;
+ return true;
+ });
+ if (allLinks.length) {
+ this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allLinks }));
+ }
+ }
+ }
+ // removes all the specified link referneces from the selection.
+ // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
+ public RemoveLinkFromSelection(allLinks: { href: string, title: string, linkId: string, targetId: string }[]) {
+ const state = this._editorView?.state;
+ if (state && this._editorView) {
+ this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allLinks }));
+ }
+ }
+
linkOnDeselect: Map<string, string> = new Map();
doLinkOnDeselect() {
@@ -177,8 +206,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.linkOnDeselect.set(key, value);
const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
- const allHrefs = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }];
- const link = this._editorView.state.schema.marks.link.create({ allHrefs, location: "onRight", title: value });
+ const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }];
+ const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "onRight", title: value });
const mval = this._editorView.state.schema.marks.metadataVal.create();
const offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
@@ -246,8 +275,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const lastSel = Math.min(flattened.length - 1, this._searchIndex);
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!;
- const allHrefs = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }];
- const link = this._editorView.state.schema.marks.link.create({ allHrefs, title: "a link", location });
+ const allLinks = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }];
+ const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, title: "a link", location });
this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link));
}
}
@@ -267,7 +296,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
public unhighlightSearchTerms = () => {
- if (this._editorView && (this._editorView as any).docView) {
+ if (window.screen.width < 600) null;
+ else if (this._editorView && (this._editorView as any).docView) {
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
const end = this._editorView.state.doc.nodeSize - 2;
@@ -614,9 +644,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
let tr = state.tr.addMark(sel.from, sel.to, splitter);
sel.from !== sel.to && 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 allHrefs = [{ href, title, targetId, linkId }];
- allHrefs.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.link.name)?.attrs.allHrefs ?? []));
- const link = state.schema.marks.link.create({ allHrefs, title, location, linkId });
+ const allLinks = [{ href, title, targetId, linkId }];
+ allLinks.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allLinks ?? []));
+ const link = state.schema.marks.linkAnchor.create({ allLinks, title, location, linkId });
tr = tr.addMark(pos, pos + node.nodeSize, link);
}
});
@@ -626,6 +656,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
componentDidMount() {
+ this._cachedLinks = DocListCast(this.Document.links);
+ 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
+ newLinks => {
+ this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
+ this._cachedLinks = newLinks;
+ });
this._disposers.buttonBar = reaction(
() => DocumentButtonBar.Instance,
instance => {
@@ -731,8 +767,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return node.copy(content.frag);
}
const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
- return linkIndex !== -1 && marks[linkIndex].attrs.allHrefs.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined;
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor);
+ return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined;
};
let start = 0;
@@ -914,8 +950,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const allHrefs = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }];
- const link = view.state.schema.mark(view.state.schema.marks.link, { allHrefs, location: "onRight", title, docref: true });
+ const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }];
+ const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "onRight", title, docref: true });
marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
}
@@ -1138,7 +1174,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
e.stopPropagation();
const view = this._editorView as any;
- // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
+ // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
// are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
if (view.mouseDown) {
const originalUpHandler = view.mouseDown.up;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 90f2c0aa6..4c90b6afd 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -27,7 +27,7 @@ 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.link);
+ return marks.find(m => m.type === schema.marks.linkAnchor);
}
export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
let before = 0;
@@ -182,7 +182,7 @@ export class FormattedTextBoxComment {
state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
child = child || (nbef && state.selection.$from.nodeBefore);
const mark = child ? findLinkMark(child.marks) : undefined;
- const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allHrefs.find((item: { href: string }) => item.href)?.href || forceUrl;
+ const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href || forceUrl;
if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) {
FormattedTextBoxComment.tooltipText.textContent = "external => " + href;
(FormattedTextBoxComment.tooltipText as any).href = href;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 0fcddfc43..95d6c9fac 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -737,7 +737,7 @@ export default class RichTextMenu extends AntimodeMenu {
const node = this.view.state.selection.$from.nodeAfter;
const link = node && node.marks.find(m => m.type.name === "link");
if (link) {
- const href = link.attrs.allHrefs.length > 0 ? link.attrs.allHrefs[0].href : undefined;
+ const href = link.attrs.allLinks.length > 0 ? link.attrs.allLinks[0].href : undefined;
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
@@ -772,40 +772,28 @@ export default class RichTextMenu extends AntimodeMenu {
}
deleteLink = () => {
- if (!this.view) return;
-
- const node = this.view.state.selection.$from.nodeAfter;
- const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
- const href = link!.attrs.allHrefs.length > 0 ? link!.attrs.allHrefs[0].href : undefined;
- if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (linkclicked) {
- DocServer.GetRefField(linkclicked).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- LinkManager.Instance.deleteLink(linkDoc);
- this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
- }
- });
- }
- } else {
- if (node) {
- const { tr, schema, selection } = this.view.state;
- const extension = this.linkExtend(selection.$anchor, href);
- this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
- }
+ if (this.view) {
+ const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor);
+ if (link) {
+ const allLinks = link.attrs.allLinks.slice();
+ this.TextView.RemoveLinkFromSelection(link.attrs.allLinks);
+ // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected.
+ allLinks.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => {
+ const linkId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ linkId && DocServer.GetRefField(linkId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
+ });
}
}
}
linkExtend($start: ResolvedPos, href: string) {
- const mark = this.view!.state.schema.marks.link;
+ const mark = this.view!.state.schema.marks.linkAnchor;
let startIndex = $start.index();
let endIndex = $start.indexAfter();
- while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) startIndex--;
- while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) endIndex++;
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) endIndex++;
let startPos = $start.start();
let endPos = startPos;
@@ -933,7 +921,7 @@ interface ButtonDropdownProps {
}
@observer
-class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+export class ButtonDropdown extends React.Component<ButtonDropdownProps> {
@observable private showDropdown: boolean = false;
private ref: HTMLDivElement | null = null;
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 612a824fc..ca30dde9d 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -279,7 +279,7 @@ export class RichTextRules {
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 });
+ const link = state.schema.marks.linkAnchor.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;
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 54b61aa20..3d7d71b14 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -17,12 +17,12 @@ export const marks: { [index: string]: MarkSpec } = {
return ["div", { className: "dummy" }, 0];
}
},
- // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // :: 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`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
- link: {
+ linkAnchor: {
attrs: {
- allHrefs: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] },
+ allLinks: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] },
showPreview: { default: true },
location: { default: null },
title: { default: null },
@@ -31,22 +31,22 @@ export const marks: { [index: string]: MarkSpec } = {
inclusive: false,
parseDOM: [{
tag: "a[href]", getAttrs(dom: any) {
- return { allHrefs: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), };
+ return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), };
}
}],
toDOM(node: any) {
- const targetids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, "");
- const linkids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, "");
+ const targetids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, "");
+ const linkids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, "");
return node.attrs.docref && node.attrs.title ?
- ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allHrefs[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
- node.attrs.allHrefs.length === 1 ?
- ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allHrefs[0].href }, 0] :
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
+ node.attrs.allLinks.length === 1 ?
+ ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] :
["div", { class: "prosemirror-anchor" },
["span", { class: "prosemirror-linkBtn" },
["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0],
["input", { class: "prosemirror-hrefoptions" }],
],
- ["div", { class: "prosemirror-links" }, ...node.attrs.allHrefs.map((item: { href: string, title: string }) =>
+ ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) =>
["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title]
)]
];
diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js
index 763961958..0969ea4ef 100644
--- a/src/client/views/nodes/formattedText/prosemirrorPatches.js
+++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js
@@ -9,6 +9,7 @@ var prosemirrorModel = require('prosemirror-model');
exports.liftListItem = liftListItem;
exports.sinkListItem = sinkListItem;
exports.wrappingInputRule = wrappingInputRule;
+exports.removeMarkWithAttrs = removeMarkWithAttrs;
// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// Create a command to lift the list item around the selection up into
// a wrapping list.
@@ -139,3 +140,57 @@ function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWith
}
+// :: ([Mark]) → ?Mark
+// Tests whether there is a mark of this type in the given set.
+function isInSetWithAttrs(mark, set, attrs) {
+ for (var i = 0; i < set.length; i++) {
+ if (set[i].type == mark) {
+ if (Array.from(Object.keys(attrs)).reduce((p, akey) => {
+ return p && JSON.stringify(set[i].attrs[akey]) === JSON.stringify(attrs[akey]);
+ }, true)) {
+ return set[i];
+ }
+ }
+ }
+};
+
+// :: (number, number, ?union<Mark, MarkType>) → this
+// Remove marks from inline nodes between `from` and `to`. When `mark`
+// is a single mark, remove precisely that mark. When it is a mark type,
+// remove all marks of that type. When it is null, remove all marks of
+// any type.
+function removeMarkWithAttrs(tr, from, to, mark, attrs) {
+ if (mark === void 0) mark = null;
+
+ var matched = [], step = 0;
+ tr.doc.nodesBetween(from, to, function (node, pos) {
+ if (!node.isInline) { return }
+ step++;
+ var toRemove = null;
+ if (mark) {
+ if (isInSetWithAttrs(mark, node.marks, attrs)) { toRemove = [mark]; }
+ } else {
+ toRemove = node.marks;
+ }
+ if (toRemove && toRemove.length) {
+ var end = Math.min(pos + node.nodeSize, to);
+ for (var i = 0; i < toRemove.length; i++) {
+ var style = toRemove[i], found$1 = (void 0);
+ for (var j = 0; j < matched.length; j++) {
+ var m = matched[j];
+ if (m.step == step - 1 && style.eq(matched[j].style)) { found$1 = m; }
+ }
+ if (found$1) {
+ found$1.to = end;
+ found$1.step = step;
+ } else {
+ matched.push({ style: style, from: Math.max(pos, from), to: end, step: step });
+ }
+ }
+ }
+ });
+ matched.forEach(function (m) { return tr.step(new prosemirrorTransform.RemoveMarkStep(m.from, m.to, m.style)); });
+ return tr
+};
+
+