From 9b8b6f79bef78be68a9982ea008e308bba4872b3 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 11:37:48 -0400 Subject: fixed text menu to make button commands undoable. fixed hiding of menu to not disappear after pulldown command selection in a cleaner way --- .../views/nodes/formattedText/RichTextMenu.tsx | 61 ++++++++++++++-------- 1 file changed, 39 insertions(+), 22 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index a06fcec9a..7c7a76e1b 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faIndent, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; +import { action, observable, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -23,6 +23,7 @@ import { updateBullets } from "./ProsemirrorExampleTransfer"; import "./RichTextMenu.scss"; import { schema } from "./schema_rts"; import { TraceMobx } from "../../../../fields/util"; +import { UndoManager } from "../../../util/UndoManager"; const { toggleMark } = require("prosemirror-commands"); library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); @@ -68,6 +69,8 @@ export default class RichTextMenu extends AntimodeMenu { @observable private currentLink: string | undefined = ""; @observable private showLinkDropdown: boolean = false; + _reaction: IReactionDisposer | undefined; + _delayHide = false; constructor(props: Readonly<{}>) { super(props); RichTextMenu.Instance = this; @@ -138,6 +141,16 @@ export default class RichTextMenu extends AntimodeMenu { ]; } + componentDidMount() { + this._reaction = reaction(() => SelectionManager.SelectedDocuments(), + () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true)); + } + componentWillUnmount() { + this._reaction?.(); + } + + public delayHide = () => { this._delayHide = true; } + @action changeView(view: EditorView) { this.view = view; @@ -147,16 +160,6 @@ export default class RichTextMenu extends AntimodeMenu { this.updateFromDash(view, lastState, this.editorProps); } - 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, 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 || ""; - } - return ""; - } - @action public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { if (!view) { @@ -310,8 +313,11 @@ export default class RichTextMenu extends AntimodeMenu { function onClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && command && command(self.view.state, self.view.dispatch, self.view); - self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => { + self.view && command && command(self.view.state, self.view.dispatch, self.view); + self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); + }, "rich text menu command"); self.setActiveMarkButtons(self.getActiveMarksOnSelection()); } @@ -338,9 +344,10 @@ export default class RichTextMenu extends AntimodeMenu { function onChange(e: React.ChangeEvent) { e.stopPropagation(); e.preventDefault(); + self.TextView.endUndoTypingBatch(); options.forEach(({ label, mark, command }) => { if (e.target.value === label) { - self.view && mark && command(mark, self.view); + UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); } }); } @@ -361,9 +368,10 @@ export default class RichTextMenu extends AntimodeMenu { const self = this; function onChange(val: string) { + self.TextView.endUndoTypingBatch(); options.forEach(({ label, node, command }) => { if (val === label) { - self.view && node && command(node); + UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown"); } }); } @@ -461,7 +469,8 @@ export default class RichTextMenu extends AntimodeMenu { function onBrushClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.fillBrush(self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush"); } let label = "Stored marks: "; @@ -528,19 +537,24 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } @action setActiveColor(color: string) { this.activeFontColor = color; } + get TextView() { return (this.view as any).TextView as FormattedTextBox } createColorButton() { const self = this; function onColorClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.TextView.EditorView!.focus() } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveColor(color); - self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.TextView.EditorView!.focus() } const button = @@ -585,13 +599,15 @@ export default class RichTextMenu extends AntimodeMenu { function onHighlightClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highligher"); } function changeHighlight(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveHighlight(color); - self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter"); } const button = @@ -631,7 +647,8 @@ export default class RichTextMenu extends AntimodeMenu { const self = this; function onLinkChange(e: React.ChangeEvent) { - self.setCurrentLink(e.target.value); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), "link change"); } const link = this.currentLink ? this.currentLink : ""; @@ -766,7 +783,7 @@ export default class RichTextMenu extends AntimodeMenu { return ref_node; } - @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; } + @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } @action -- cgit v1.2.3-70-g09d2 From 3712228f853e80ff05eced253a0bf197bdf47bde Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 12:26:54 -0400 Subject: added several new commands to RichTextMenu --- .../views/nodes/formattedText/RichTextMenu.tsx | 73 +++++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 7c7a76e1b..6fdcaceda 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faIndent, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; +import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, observable, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; @@ -26,7 +26,7 @@ import { TraceMobx } from "../../../../fields/util"; import { UndoManager } from "../../../util/UndoManager"; const { toggleMark } = require("prosemirror-commands"); -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); +library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); @observer @@ -443,6 +443,63 @@ export default class RichTextMenu extends AntimodeMenu { return true; } + insetParagraph(state: EditorState, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + outsetParagraph(state: EditorState, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + + indentParagraph(state: EditorState, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; + const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + + hangingIndentParagraph(state: EditorState, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; + const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + insertBlockquote(state: EditorState, dispatch: any) { const path = (state.selection.$from as any).path; if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { @@ -453,6 +510,11 @@ export default class RichTextMenu extends AntimodeMenu { return true; } + insertHorizontalRule(state: EditorState, dispatch: any) { + dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView()); + return true; + } + @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; } // todo: add brushes to brushMap to save with a style name @@ -822,6 +884,10 @@ export default class RichTextMenu extends AntimodeMenu { this.createButton("align-left", "Align Left", undefined, this.alignLeft), this.createButton("align-center", "Align Center", undefined, this.alignCenter), this.createButton("align-right", "Align Right", undefined, this.alignRight), + this.createButton("indent", "Inset More", undefined, this.insetParagraph), + this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), + this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), + this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), ]}; const row2 =
@@ -831,7 +897,8 @@ export default class RichTextMenu extends AntimodeMenu { this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), - this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote),]} + this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), + this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule),]}
{/*
-- cgit v1.2.3-70-g09d2 From 54e38339b5292c3eddca0ef9665ec0c2b4d30ba2 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 14:20:53 -0400 Subject: added divider lines in richTextMen --- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 -- src/client/views/nodes/formattedText/RichTextMenu.scss | 6 ++++++ src/client/views/nodes/formattedText/RichTextMenu.tsx | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2a79e2f13..7e5509e85 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1049,7 +1049,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action onFocused = (e: React.FocusEvent): void => { - console.log("FOUCSS") FormattedTextBox.FocusedBox = this; this.tryUpdateHeight(); @@ -1181,7 +1180,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } public static HadSelection: boolean = false; onBlur = (e: any) => { - console.log("BLURRR") FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; //DictationManager.Controls.stop(false); this.endUndoTypingBatch(); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index 7a0718c16..fbc468292 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -77,6 +77,12 @@ color: white; } } + .richTextMenu-divider { + margin: auto; + border-left: solid #ffffff70 0.5px; + height: 20px; + width: 1px; + } } .link-menu { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 6fdcaceda..57719a8eb 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -876,11 +876,13 @@ export default class RichTextMenu extends AntimodeMenu { this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), +
]}, this.createColorButton(), this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), +
, this.createButton("align-left", "Align Left", undefined, this.alignLeft), this.createButton("align-center", "Align Center", undefined, this.alignCenter), this.createButton("align-right", "Align Right", undefined, this.alignRight), @@ -893,12 +895,15 @@ export default class RichTextMenu extends AntimodeMenu { const row2 =
{this.collapsed ? this.getDragger() : (null)}
+
, {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), +
, this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), - this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule),]} + this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), +
,]}
{/*
-- cgit v1.2.3-70-g09d2 From eff0c84e6f42bd6be1c58f1009d8d766bcf0a819 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 15:42:22 -0400 Subject: fixed warnings --- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/collections/CollectionSchemaView.tsx | 8 ++++---- src/client/views/collections/CollectionSubView.tsx | 1 - src/client/views/collections/CollectionViewChromes.tsx | 6 +++--- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 14 +++++--------- src/client/views/nodes/formattedText/RichTextMenu.tsx | 8 ++++---- src/fields/Doc.ts | 2 +- 7 files changed, 18 insertions(+), 23 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ef216001c..40bc24917 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -368,7 +368,7 @@ export class CurrentUserUtils { doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "compare", _width: 300, _height: 300 }); } if (doc.emptyScript === undefined) { - doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script" }) + doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script" }); } if (doc.emptyDocHolder === undefined) { doc.emptyDocHolder = Docs.Create.DocumentDocument( diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 23e8a4d49..3c42a2f1c 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -581,14 +581,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onZoomMenu = (e: React.WheelEvent) => { this.props.active(true) && e.stopPropagation(); if (this.menuCoordinates[0] > e.screenX) { - this.menuCoordinates[0] -= e.screenX //* this.scale; + this.menuCoordinates[0] -= e.screenX; //* this.scale; } else { - this.menuCoordinates[0] += e.screenX //* this.scale; + this.menuCoordinates[0] += e.screenX; //* this.scale; } if (this.menuCoordinates[1] > e.screenY) { - this.menuCoordinates[1] -= e.screenY //* this.scale; + this.menuCoordinates[1] -= e.screenY; //* this.scale; } else { - this.menuCoordinates[1] += e.screenY //* this.scale; + this.menuCoordinates[1] += e.screenY; //* this.scale; } } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ecd16d22f..ed8535ecb 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -9,7 +9,6 @@ import { ScriptField } from "../../../fields/ScriptField"; import { WebField } from "../../../fields/URLField"; import { Cast, ScriptCast, NumCast, StrCast } from "../../../fields/Types"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; -import { Upload } from "../../../server/SharedMediaTypes"; import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Networking } from "../../Network"; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index ab0df88f8..4e91a2928 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -452,9 +452,9 @@ export class CollectionStackingViewChrome extends React.Component { this.props.CollectionView.props.Document._columnsSort = - this.props.CollectionView.props.Document._columnsSort === "descending" ? "ascending" : - this.props.CollectionView.props.Document._columnsSort === "ascending" ? undefined : "descending"; - }; + this.props.CollectionView.props.Document._columnsSort === "descending" ? "ascending" : + this.props.CollectionView.props.Document._columnsSort === "ascending" ? undefined : "descending"; + } @action resetValue = () => { this._currentKey = this.pivotField; }; render() { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 7e5509e85..664141607 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -68,15 +68,10 @@ export interface FormattedTextBoxProps { xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yMargin?: number; } - -const richTextSchema = createSchema({ - documentText: "string", -}); - export const GoogleRef = "googleDocId"; -type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>; -const RichTextDocument = makeInterface(richTextSchema, documentSchema); +type RichTextDocument = makeInterface<[typeof documentSchema]>; +const RichTextDocument = makeInterface(documentSchema); type PullHandler = (exportState: Opt, dataDoc: Doc) => void; @@ -661,7 +656,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp incomingValue => { if (incomingValue !== undefined && this._editorView && !this._applyingChange) { const updatedState = JSON.parse(incomingValue); - if (JSON.stringify(this._editorView!.state.toJSON()) !== JSON.stringify(updatedState)) { + if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) { this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } @@ -1293,7 +1288,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", fontSize: Cast(this.layoutDoc._fontSize, "number", null), - fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit") + fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), + transition: "opacity 1s" }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 57719a8eb..0fcddfc43 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -149,7 +149,7 @@ export default class RichTextMenu extends AntimodeMenu { this._reaction?.(); } - public delayHide = () => { this._delayHide = true; } + public delayHide = () => this._delayHide = true; @action changeView(view: EditorView) { @@ -599,7 +599,7 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } @action setActiveColor(color: string) { this.activeFontColor = color; } - get TextView() { return (this.view as any).TextView as FormattedTextBox } + get TextView() { return (this.view as any).TextView as FormattedTextBox; } createColorButton() { const self = this; @@ -608,7 +608,7 @@ export default class RichTextMenu extends AntimodeMenu { e.stopPropagation(); self.TextView.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus() + self.TextView.EditorView!.focus(); } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); @@ -616,7 +616,7 @@ export default class RichTextMenu extends AntimodeMenu { self.setActiveColor(color); self.TextView.endUndoTypingBatch(); UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus() + self.TextView.EditorView!.focus(); } const button = diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e4d11dd4d..4a35ef018 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -965,7 +965,7 @@ export namespace Doc { container._docFilters = new List(docFilters); } } - }) + }); } export function readDocRangeFilter(doc: Doc, key: string) { const docRangeFilters = Cast(doc._docRangeFilters, listSpec("string"), []); -- cgit v1.2.3-70-g09d2 From 17b9f3672fde08864343afd9808881dd23023643 Mon Sep 17 00:00:00 2001 From: Lionel Han <47760119+IGoByJoe@users.noreply.github.com> Date: Wed, 1 Jul 2020 16:47:47 -0700 Subject: added color picker for highlighter --- .../views/nodes/formattedText/RichTextMenu.tsx | 2 +- src/client/views/pdf/PDFMenu.scss | 19 ++++++ src/client/views/pdf/PDFMenu.tsx | 68 ++++++++++++++++++++-- 3 files changed, 82 insertions(+), 7 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 0fcddfc43..b9ca82243 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -933,7 +933,7 @@ interface ButtonDropdownProps { } @observer -class ButtonDropdown extends React.Component { +export class ButtonDropdown extends React.Component { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index 3c08ba80d..fa43a99b2 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -3,4 +3,23 @@ width: 200px; padding: 5px; grid-template-columns: 90px 20px 90px; +} + +.color-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + button.color-button { + width: 20px; + height: 20px; + border-radius: 15px !important; + margin: 3px; + border: 2px solid transparent !important; + padding: 3px; + + &.active { + border: 2px solid white; + } + } } \ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 6dcf5cce6..00c56d73e 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,22 +1,43 @@ import React = require("react"); import "./PDFMenu.scss"; -import { observable, action, } from "mobx"; +import { observable, action, computed, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction, returnFalse } from "../../../Utils"; +import { unimplementedFunction, returnFalse, Utils } from "../../../Utils"; import AntimodeMenu from "../AntimodeMenu"; import { Doc, Opt } from "../../../fields/Doc"; +import { ColorState } from "react-color"; +import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; + @observer export default class PDFMenu extends AntimodeMenu { static Instance: PDFMenu; private _commentCont = React.createRef(); + private _palette = [ + "rgba(208, 2, 27, 0.8)", + "rgba(238, 0, 0, 0.8)", + "rgba(245, 166, 35, 0.8)", + "rgba(248, 231, 28, 0.8)", + "rgba(245, 230, 95, 0.616)", + "rgba(139, 87, 42, 0.8)", + "rgba(126, 211, 33, 0.8)", + "rgba(65, 117, 5, 0.8)", + "rgba(144, 19, 254, 0.8)", + "rgba(238, 169, 184, 0.8)", + "rgba(224, 187, 228, 0.8)", + "rgba(225, 223, 211, 0.8)", + "rgba(255, 255, 255, 0.8)", + "rgba(155, 155, 155, 0.8)", + "rgba(0, 0, 0, 0.8)"]; @observable private _keyValue: string = ""; @observable private _valueValue: string = ""; @observable private _added: boolean = false; + @observable private highlightColor: string = "rgba(245, 230, 95, 0.616)"; + @observable public _colorBtn = false; @observable public Highlighting: boolean = false; @observable public Status: "pdf" | "annotation" | "" = ""; @@ -70,11 +91,47 @@ export default class PDFMenu extends AntimodeMenu { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight + if (!this.Highlight(this.highlightColor) && this.Pinned) { this.Highlighting = !this.Highlighting; } } + @computed get highlighter() { + const button = + ; + + const dropdownContent = +
+

Change highlighter color:

+
+ {this._palette.map(color => { + if (color) { + return this.highlightColor === color ? + : + ; + } + })} +
+
; + return ( + + ); + } + + @action + changeHighlightColor = (color: string, e: React.PointerEvent) => { + const col: ColorState = { + hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, + rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", + }; + e.preventDefault(); + e.stopPropagation(); + this.highlightColor = Utils.colorString(col); + } + deleteClicked = (e: React.PointerEvent) => { this.Delete(); } @@ -101,12 +158,11 @@ export default class PDFMenu extends AntimodeMenu { render() { const buttons = this.Status === "pdf" ? [ - , + this.highlighter, , + , ] : [ , -- cgit v1.2.3-70-g09d2 From fcbf3a161839111ac609ec737cd56c71efde18a1 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 21:13:10 -0400 Subject: added basic synchronizatoin between links and their hypertext anchors. more work is needed to handle overlapping anchors. --- src/client/util/LinkManager.ts | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 39 ++++++++++++++- .../views/nodes/formattedText/RichTextMenu.tsx | 32 ++++--------- .../nodes/formattedText/prosemirrorPatches.js | 55 ++++++++++++++++++++++ 4 files changed, 103 insertions(+), 25 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 9b4dc2630..749fabfcc 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -49,7 +49,7 @@ export class LinkManager { } public deleteLink(linkDoc: Doc): boolean { - if (LinkManager.Instance.LinkManagerDoc) { + if (LinkManager.Instance.LinkManagerDoc && linkDoc instanceof Doc) { Doc.RemoveDocFromList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc); return true; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 664141607..f32c17563 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; 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 allHrefs: any[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => { + const foundMark = findLinkMark(node.marks); + const newHrefs = foundMark?.attrs.allHrefs.filter((a: any) => a.href.includes(linkDoc[Id])) || []; + allHrefs = newHrefs.length ? newHrefs : allHrefs; + return true; + }); + if (allHrefs.length) { + this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.link, { allHrefs })); + } + } + } + // 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(allHrefs: { 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, { allHrefs })); + } + } + linkOnDeselect: Map = new Map(); doLinkOnDeselect() { @@ -626,6 +655,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 => { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 0fcddfc43..6366f299d 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -772,28 +772,16 @@ 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.link); + if (link) { + const allHrefs = link.attrs.allHrefs.slice(); + this.TextView.RemoveLinkFromSelection(link.attrs.allHrefs); + // 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. + allHrefs.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)) + }); } } } 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) → 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 +}; + + -- cgit v1.2.3-70-g09d2 From 19a48bfe9b2087d23ab6bbb384f8edc18ed07d81 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 Jul 2020 21:26:35 -0400 Subject: renamed link to linkAnchor and allHrefs to allLinks in rich text code --- .../views/nodes/formattedText/FormattedTextBox.tsx | 36 +++++++++++----------- .../formattedText/FormattedTextBoxComment.tsx | 4 +-- .../views/nodes/formattedText/RichTextMenu.tsx | 18 +++++------ .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/client/views/nodes/formattedText/marks_rts.ts | 20 ++++++------ src/fields/RichTextUtils.ts | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) (limited to 'src/client/views/nodes/formattedText/RichTextMenu.tsx') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f32c17563..9c7c46ae9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -149,24 +149,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public RemoveLinkFromDoc(linkDoc?: Doc) { const state = this._editorView?.state; if (state && linkDoc && this._editorView) { - var allHrefs: any[] = []; + 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.allHrefs.filter((a: any) => a.href.includes(linkDoc[Id])) || []; - allHrefs = newHrefs.length ? newHrefs : allHrefs; + const newHrefs = foundMark?.attrs.allLinks.filter((a: any) => a.href.includes(linkDoc[Id])) || []; + allLinks = newHrefs.length ? newHrefs : allLinks; return true; }); - if (allHrefs.length) { - this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.link, { allHrefs })); + 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(allHrefs: { href: string, title: string, linkId: string, targetId: string }[]) { + 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, { allHrefs })); + this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allLinks })); } } @@ -206,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); @@ -275,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)); } } @@ -643,9 +643,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); } }); @@ -766,8 +766,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; @@ -949,8 +949,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); } 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 6366f299d..3ec75e680 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]; @@ -773,27 +773,27 @@ export default class RichTextMenu extends AntimodeMenu { deleteLink = () => { if (this.view) { - const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.link); + const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); if (link) { - const allHrefs = link.attrs.allHrefs.slice(); - this.TextView.RemoveLinkFromSelection(link.attrs.allHrefs); + 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. - allHrefs.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => { + 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)) + 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; 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 `` // 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/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 7c7bf3e12..a590c88c4 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -392,7 +392,7 @@ export namespace RichTextUtils { const { attrs } = mark; switch (converted) { case "link": - let url = attrs.allHrefs.length ? attrs.allHrefs[0].href : ""; + let url = attrs.allLinks.length ? attrs.allLinks[0].href : ""; const delimiter = "/doc/"; const alreadyShared = "?sharing=true"; if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { -- cgit v1.2.3-70-g09d2