diff options
| author | bobzel <zzzman@gmail.com> | 2024-09-02 09:26:37 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-09-02 09:26:37 -0400 |
| commit | cda69e48361fce8d71a4dc66edd9dd976a27f52d (patch) | |
| tree | 82b9a1a5967ae88a9534f89f7eaed3aeb289652f /src/client/views/nodes/formattedText | |
| parent | c01828308714874589d1f60c33ca59df4c656c0c (diff) | |
| parent | a958577d4c27b276aa37484e3f895e196138b17c (diff) | |
Merge branch 'master' into alyssa-starter
Diffstat (limited to 'src/client/views/nodes/formattedText')
12 files changed, 436 insertions, 435 deletions
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index 3ec49fa27..0304ddc86 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -5,18 +5,20 @@ import { IReactionDisposer, computed, reaction } from 'mobx'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; import { NumCast } from '../../../../fields/Types'; +import { Node } from 'prosemirror-model'; +import { EditorView } from 'prosemirror-view'; interface IDashDocCommentViewInternal { docId: string; - view: any; - getPos: any; + view: EditorView; + getPos: () => number; setHeight: (height: number) => void; } export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> { _reactionDisposer: IReactionDisposer | undefined; - constructor(props: any) { + constructor(props: IDashDocCommentViewInternal) { super(props); this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); this.onPointerEnterCollapsed = this.onPointerEnterCollapsed.bind(this); @@ -43,19 +45,19 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV return DocServer.GetRefField(this.props.docId); } - onPointerLeaveCollapsed = (e: any) => { + onPointerLeaveCollapsed = (e: React.PointerEvent) => { this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); e.preventDefault(); e.stopPropagation(); }; - onPointerEnterCollapsed = (e: any) => { + onPointerEnterCollapsed = (e: React.PointerEvent) => { this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); e.preventDefault(); e.stopPropagation(); }; - onPointerUpCollapsed = (e: any) => { + onPointerUpCollapsed = (e: React.PointerEvent) => { const target = this.targetNode(); if (target) { @@ -65,7 +67,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV setTimeout(() => { expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { - this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); + this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, (this.props.getPos() ?? 0) + (expand ? 2 : 1)))); } catch (err) { /* empty */ } @@ -74,7 +76,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV e.stopPropagation(); }; - onPointerDownCollapsed = (e: any) => { + onPointerDownCollapsed = (e: React.PointerEvent) => { e.stopPropagation(); }; @@ -84,7 +86,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { const m = state.doc.nodeAt(i); if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) { - return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean }; + return { node: m, pos: i, hidden: m.attrs.hidden } as { node: Node; pos: number; hidden: boolean }; } } @@ -119,10 +121,10 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV // the comment can be toggled on/off with the '<-' text anchor. export class DashDocCommentView { dom: HTMLDivElement; // container for label and value - root: any; - node: any; + root: ReactDOM.Root; + node: Node; - constructor(node: any, view: any, getPos: any) { + constructor(node: Node, view: EditorView, getPos: () => number | undefined) { this.node = node; this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; @@ -130,22 +132,22 @@ export class DashDocCommentView { this.dom.style.fontWeight = 'bold'; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-block'; - this.dom.onkeypress = function (e: any) { + this.dom.onkeypress = function (e) { e.stopPropagation(); }; - this.dom.onkeydown = function (e: any) { + this.dom.onkeydown = function (e) { e.stopPropagation(); }; - this.dom.onkeyup = function (e: any) { + this.dom.onkeyup = function (e) { e.stopPropagation(); }; - this.dom.onmousedown = function (e: any) { + this.dom.onmousedown = function (e) { e.stopPropagation(); }; + const getPosition = () => getPos() ?? 0; this.root = ReactDOM.createRoot(this.dom); - this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />); - (this as any).dom = this.dom; + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPosition} setHeight={this.setHeight} docId={node.attrs.docId} />); } setHeight = (hgt: number) => { diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 93371685d..e7f2cdba8 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; @@ -16,6 +15,8 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../DocumentView'; import { FocusViewOptions } from '../FocusViewOptions'; import { FormattedTextBox } from './FormattedTextBox'; +import { EditorView } from 'prosemirror-view'; +import { Node } from 'prosemirror-model'; const horizPadding = 3; // horizontal padding to container to allow cursor to show up on either side. interface IDashDocViewInternal { @@ -26,9 +27,9 @@ interface IDashDocViewInternal { height: string; hidden: boolean; fieldKey: string; - view: any; - node: any; - getPos: any; + view: EditorView; + node: Node; + getPos: () => number; } @observer @@ -109,7 +110,7 @@ export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewIn }; outerFocus = (target: Doc, options: FocusViewOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target - onKeyDown = (e: any) => { + onKeyDown = (e: React.KeyboardEvent) => { e.stopPropagation(); if (e.key === 'Tab' || e.key === 'Enter') { e.preventDefault(); @@ -176,29 +177,31 @@ export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewIn export class DashDocView { dom: HTMLSpanElement; // container for label and value - root: any; + root: ReactDOM.Root; - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + constructor(node: Node, view: EditorView, getPos: () => number | undefined, tbox: FormattedTextBox) { this.dom = document.createElement('span'); this.dom.style.position = 'relative'; this.dom.style.textIndent = '0'; this.dom.style.width = (+node.attrs.width.toString().replace('px', '') + horizPadding).toString(); this.dom.style.height = node.attrs.height; this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block'; - (this.dom.style as any).float = node.attrs.float; - this.dom.onkeypress = function (e: any) { + this.dom.style.float = node.attrs.float; + this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeydown = function (e: any) { + this.dom.onkeydown = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeyup = function (e: any) { + this.dom.onkeyup = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onmousedown = function (e: any) { + this.dom.onmousedown = function (e: MouseEvent) { e.stopPropagation(); }; + const getPosition = () => getPos() ?? 0; + this.root = ReactDOM.createRoot(this.dom); this.root.render( <DashDocViewInternal @@ -211,7 +214,7 @@ export class DashDocView { tbox={tbox} view={view} node={node} - getPos={getPos} + getPos={getPosition} /> ); } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 9903d0e8a..f0313fba4 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,6 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; @@ -26,6 +23,8 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../OpenWhere'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; +import { Node } from 'prosemirror-model'; +import { EditorView } from 'prosemirror-view'; @observer export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -34,7 +33,7 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { static createFieldView: (e: React.MouseEvent) => void = emptyFunction; static toggleFieldHide: () => void = emptyFunction; static toggleValueHide: () => void = emptyFunction; - constructor(props: any) { + constructor(props: AntimodeMenuProps) { super(props); DashFieldViewMenu.Instance = this; } @@ -100,8 +99,8 @@ interface IDashFieldViewInternal { height: number; editable: boolean; nodeSelected: () => boolean; - node: any; - getPos: any; + node: Node; + getPos: () => number; unclickable: () => boolean; } @@ -274,7 +273,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi <select className="dashFieldView-select" tabIndex={-1} defaultValue={this._dashDoc && Field.toKeyValueString(this._dashDoc, this._fieldKey)} onChange={this.selectVal}> <option value="-unset-">-unset-</option> {this.values.map(val => ( - <option value={val.value}>{val.label}</option> + <option key={val.value} value={val.value}> + {val.label} + </option> ))} </select> )} @@ -284,16 +285,17 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } export class DashFieldView { dom: HTMLDivElement; // container for label and value - root: any; - node: any; + root: ReactDOM.Root; + node: Node; tbox: FormattedTextBox; - getpos: any; + getpos: () => number | undefined; @observable _nodeSelected = false; NodeSelected = () => this._nodeSelected; - unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some(m => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + constructor(node: Node, view: EditorView, getPos: () => number | undefined, tbox: FormattedTextBox) { makeObservable(this); + const getPosition = () => getPos() ?? 0; this.node = node; this.tbox = tbox; this.getpos = getPos; @@ -312,7 +314,7 @@ export class DashFieldView { const editor = tbox.EditorView; if (editor) { const { state } = editor; - for (let i = this.getpos() + 1; i < state.doc.content.size; i++) { + for (let i = getPosition() + 1; i < state.doc.content.size; i++) { if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); return; @@ -321,10 +323,10 @@ export class DashFieldView { } } }; - this.dom.onkeyup = function (e: any) { + this.dom.onkeyup = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onmousedown = function (e: any) { + this.dom.onmousedown = function (e: MouseEvent) { e.stopPropagation(); }; @@ -333,7 +335,7 @@ export class DashFieldView { <DashFieldViewInternal node={node} unclickable={this.unclickable} - getPos={getPos} + getPos={getPosition} fieldKey={node.attrs.fieldKey} docId={node.attrs.docId} width={node.attrs.width} diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx index d9b1a2cf8..8bb4a0a26 100644 --- a/src/client/views/nodes/formattedText/EquationEditor.tsx +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -3,15 +3,12 @@ import React, { Component, createRef } from 'react'; // Import JQuery, required for the functioning of the equation editor import $ from 'jquery'; - import './EquationEditor.scss'; -// @ts-ignore -window.jQuery = $; - -// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(window as any).jQuery = $; require('mathquill/build/mathquill'); - +// eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).MathQuill = (window as any).MathQuill.getInterface(1); type EquationEditorProps = { @@ -36,17 +33,18 @@ type EquationEditorProps = { * @extends {Component<EquationEditorProps>} */ class EquationEditor extends Component<EquationEditorProps> { - element: any; + element: React.RefObject<HTMLSpanElement>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any mathField: any; ignoreEditEvents: number; // Element needs to be in the class format and thus requires a constructor. The steps that are run // in the constructor is to make sure that React can succesfully communicate with the equation // editor. - constructor(props: any) { + constructor(props: EquationEditorProps) { super(props); - this.element = createRef(); + this.element = createRef<HTMLSpanElement>(); this.mathField = null; // MathJax apparently fire 2 edit events on startup. @@ -74,6 +72,7 @@ class EquationEditor extends Component<EquationEditorProps> { autoOperatorNames, }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.mathField = (window as any).MathQuill.MathField(this.element.current, config); this.mathField.latex(value || ''); } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 5167c8f2a..df1421a33 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,22 +1,23 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; +import { Node } from 'prosemirror-model'; import { TextSelection } from 'prosemirror-state'; +import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; import EquationEditor from './EquationEditor'; import { FormattedTextBox } from './FormattedTextBox'; -import { DocData } from '../../../../fields/DocSymbols'; interface IEquationViewInternal { fieldKey: string; tbox: FormattedTextBox; width: number; height: number; - getPos: () => number; + getPos: () => number | undefined; setEditor: (editor: EquationEditor | undefined) => void; } @@ -27,7 +28,7 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> _fieldKey: string; _ref: React.RefObject<EquationEditor> = React.createRef(); - constructor(props: any) { + constructor(props: IEquationViewInternal) { super(props); this._fieldKey = props.fieldKey; this._textBoxDoc = props.tbox.Document; @@ -46,7 +47,7 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> className="equationView" onKeyDown={e => { if (e.key === 'Enter') { - this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new TextSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos() + 1)))); + this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new TextSelection(this.props.tbox.EditorView!.state.doc.resolve((this.props.getPos() ?? 0) + 1)))); this.props.tbox.EditorView!.focus(); e.preventDefault(); } @@ -63,7 +64,7 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> <EquationEditor ref={this._ref} value={StrCast(this._textBoxDoc[DocData][this._fieldKey])} - onChange={(str: any) => { + onChange={str => { this._textBoxDoc[DocData][this._fieldKey] = str; }} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" @@ -77,25 +78,27 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> export class EquationView { dom: HTMLDivElement; // container for label and value - root: any; + root: ReactDOM.Root; tbox: FormattedTextBox; - view: any; - constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + view: EditorView; + _editor: EquationEditor | undefined; + getPos: () => number | undefined; + constructor(node: Node, view: EditorView, getPos: () => number | undefined, tbox: FormattedTextBox) { this.tbox = tbox; this.view = view; + this.getPos = getPos; this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-block'; - this.dom.onmousedown = function (e: any) { + this.dom.onmousedown = (e: MouseEvent) => { e.stopPropagation(); }; this.root = ReactDOM.createRoot(this.dom); this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />); } - _editor: EquationEditor | undefined; setEditor = (editor?: EquationEditor) => { this._editor = editor; }; @@ -106,6 +109,7 @@ export class EquationView { this._editor?.mathField.focus(); } selectNode() { + this.view.dispatch(this.view.state.tr.setSelection(new TextSelection(this.view.state.doc.resolve(this.getPos() ?? 0)))); this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus setTimeout(() => { this._editor?.mathField.focus(); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index faef78469..73b20e6c2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,9 +1,8 @@ /* eslint-disable no-use-before-define */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -14,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -27,7 +26,7 @@ import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; -import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType, gptImageLabel } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -65,8 +64,6 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; -import { URLField } from '../../../../fields/URLField'; -import { gptImageLabel } from '../../../apis/gpt/GPT'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -78,28 +75,45 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } - private static nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; + public static MakeConfig(rules?: RichTextRules, props?: FormattedTextBoxProps) { + return { + schema, + plugins: [ + inputRules(rules?.inpRules ?? { rules: [] }), + ...(props ? [FormattedTextBox.richTextMenuPlugin(props)] : []), + history(), + keymap(buildKeymap(schema, props ?? {})), + keymap(baseKeymap), + new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), + new Plugin({ view: () => new FormattedTextBoxComment() }), + ], + }; + } /** * Initialize the class with all the plugin node view components * @param nodeViews prosemirror plugins that render a custom UI for specific node types */ - public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) { - FormattedTextBox.nodeViews = nodeViews; - } + public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) { FormattedTextBox._nodeViews = nodeViews; } // prettier-ignore + + public static PasteOnLoad: ClipboardEvent | undefined; + public static DontSelectInitialText = false; // whether initial text should be selected or not + public static SelectOnLoadChar = ''; public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection - static _globalHighlightsCache: string = ''; - static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); - static _highlightStyleSheet = addStyleSheet(); - static _bulletStyleSheet = addStyleSheet(); - static _userStyleSheet = addStyleSheet(); - static _hadSelection: boolean = false; + + private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; + private static _globalHighlightsCache: string = ''; + private static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); + private static _highlightStyleSheet = addStyleSheet(); + private static _bulletStyleSheet = addStyleSheet(); + private static _userStyleSheet = addStyleSheet(); + + private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: HTMLDivElement | null = null; - private _editorView: Opt<EditorView>; - public _applyingChange: string = ''; + private _editorView: Opt<EditorView & { TextView?: FormattedTextBox | undefined }>; private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; @@ -110,85 +124,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _recordingStart: number = 0; private _ignoreScroll = false; private _focusSpeed: Opt<number>; - private _keymap: any = undefined; private _rules: RichTextRules | undefined; private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle private _break = true; + + public _applyingChange: string = ''; public ProseRef?: HTMLDivElement; - public get EditorView() { - return this._editorView; - } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } - @computed get noSidebar() { - return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; - } - @computed get layout_sidebarWidthPercent() { - return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - @computed get layout_autoHeight() { - return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; - } - @computed get textHeight() { - return NumCast(this.dataDoc[this.fieldKey + '_height']); - } - @computed get scrollHeight() { - return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); - } - @computed get sidebarHeight() { - return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); - } - @computed get titleHeight() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; - } - @computed get layout_autoHeightMargins() { - return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); - } - @computed get _recordingDictation() { - return this.dataDoc?.mediaState === mediaState.Recording; - } + @observable _showSidebar = false; + + @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore + @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore + @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore + set _recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } - @computed get config() { - this._keymap = buildKeymap(schema, this._props); - this._rules = new RichTextRules(this.Document, this); - return { - schema, - plugins: [ - inputRules(this._rules.inpRules), - this.richTextMenuPlugin(), - history(), - keymap(this._keymap), - keymap(baseKeymap), - new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), - new Plugin({ - view(/* editorView */) { - return new FormattedTextBoxComment(); - }, - }), - ], - }; - } - - // State for GPT - @observable - private gptRes: string = ''; - - // public makeAIFlashcards: () => void = unimplementedFunction; - public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; - - public static PasteOnLoad: ClipboardEvent | undefined; - public static DontSelectInitialText = false; // whether initial text should be selected or not - public static SelectOnLoadChar = ''; + @computed get _recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore + @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore + @computed get noSidebar() { return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } // prettier-ignore + @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore + @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore + @computed get layout_autoHeight() { return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } // prettier-ignore + @computed get textHeight() { return NumCast(this.dataDoc[this.fieldKey + '_height']); } // prettier-ignore + @computed get scrollHeight() { return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } // prettier-ignore + @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.sidebarKey + '_height']); } // prettier-ignore + @computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore + @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore + @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this._props); } // prettier-ignore + @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore constructor(props: FormattedTextBoxProps) { super(props); @@ -196,6 +162,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._recordingStart = Date.now(); } + public get EditorView() { return this._editorView; } // prettier-ignore + + // public makeAIFlashcards: () => void = unimplementedFunction; + public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; + // 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. @@ -207,9 +178,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (state && a1 && a2 && this._editorView) { this.removeDocument(a1); this.removeDocument(a2); - let allFoundLinkAnchors: any[] = []; - state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any /* , pos: number, parent: any */) => { - const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; + let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: Node /* , pos: number, parent: any */) => { + const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: { href: string; title: string; anchorId: string }) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; }); @@ -257,7 +228,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; - let stopFunc: any; + let stopFunc: () => void = emptyFunction; const targetData = target[DocData]; targetData.mediaState = mediaState.Recording; DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore @@ -275,10 +246,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); }; - AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'); - return undefined; - }, 'highlght text'); + AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -294,7 +262,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + const docView = this.DocumentView?.(); + docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); @@ -347,7 +316,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; - state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any /* , pos: number, parent: any */) => { + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: Node /* , pos: number, parent: any */) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { accumTags.push(node.attrs.fieldKey); } @@ -413,8 +382,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); if (this._editorView && linkTime) { const { state } = this._editorView; - const { path } = state.selection.$from as any; - if (linkAnchor && path[path.length - 3].type !== state.schema.nodes.code_block) { + const node = state.selection.$from.node(); + if (linkAnchor && node.type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; const { from } = state.selection; @@ -479,7 +448,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB * function of a freeform view that is driven by the text box's text. The include directive will copy the code of the published * document into the code being evaluated. */ - hyperlinkTerm = (trIn: any, target: Doc, newAutoLinks: Set<Doc>) => { + hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => { let tr = trIn; const editorView = this._editorView; if (editorView && !Doc.AreProtosEqual(target, this.Document)) { @@ -496,7 +465,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ) { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); tr = tr.addMark(sel.from, sel.to, splitter); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number /* , parent: any */) => { + tr.doc.nodesBetween(sel.from, sel.to, (node: Node, pos: number /* , parent: any */) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? @@ -649,15 +618,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (node.isBlock) { // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < (context.content as any).content.length; i++) { - const result = this.getNodeEndpoints((context.content as any).content[i], node); + for (let i = 0; i < context.content.childCount; i++) { + const result = this.getNodeEndpoints(context.content.child(i), node); if (result) { return { from: result.from + offset + (context.type.name === 'doc' ? 0 : 1), to: result.to + offset + (context.type.name === 'doc' ? 0 : 1), }; } - offset += (context.content as any).content[i].nodeSize; + offset += context.content.child(i).nodeSize; } } return null; @@ -743,18 +712,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; - @observable _showSidebar = false; - @computed get SidebarShown() { - return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); - } - @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); if (preview) this._showSidebar = true; else { - this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; + this.layoutDoc[this.sidebarKey + '_freeform_scale_max'] = 1; this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; } @@ -822,10 +786,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this._props.dontSelect?.()) return; const cm = ContextMenu.Instance; - let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> - while (target && !target.dataset?.targethrefs) target = target.parentElement; + let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> + while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement; const editor = this._editorView; - if (editor && target && !(e.nativeEvent as any).dash) { + if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -834,10 +798,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB .lastElement() .replace(Doc.localServerPath(), '') .split('?')[0]; - const deleteMarkups = undoBatch(() => { + const deleteMarkups = undoable(() => { const { selection } = editor.state; editor.dispatch(editor.state.tr.removeMark(selection.from, selection.to, editor.state.schema.marks.linkAnchor)); - }); + }, 'delete markups'); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( @@ -861,21 +825,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const changeItems: ContextMenuProps[] = []; changeItems.push({ description: 'plain', - event: undoBatch(() => { + event: undoable(() => { Doc.setNativeView(this.Document); this.layoutDoc.layout_autoHeightMargins = undefined; - }), + }, 'set plain view'), icon: 'eye', }); changeItems.push({ description: 'metadata', - event: undoBatch(() => { + event: undoable(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; setTimeout(() => { this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; }, 50); - }), + }, 'set metadata view'), icon: 'eye', }); const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null); @@ -883,11 +847,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const icon: IconProp = StrCast(note.icon) as IconProp; changeItems.push({ description: StrCast(note.title), - event: undoBatch(() => { - this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.Document); - DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); - }), + event: undoable( + () => { + this.layoutDoc.layout_autoHeightMargins = undefined; + Doc.setNativeView(this.Document); + DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); + }, + `set ${StrCast(note.title)} view}` + ), icon: icon, }); }); @@ -909,12 +876,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }) ); const appearance = cm.findByDescription('Appearance...'); - const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; + const appearanceItems = appearance?.subitems ?? []; // appearanceItems.push({ // description: 'Find image tags', // event: this.findImageTags, // icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', // }); + appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => { @@ -968,7 +936,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); - const optionItems = options && 'subitems' in options ? options.subitems : []; + const optionItems = options?.subitems ?? []; optionItems.push({ description: `Toggle auto update from template`, event: () => { @@ -997,7 +965,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const help = cm.findByDescription('Help...'); - const helpItems = help && 'subitems' in help ? help.subitems : []; + const helpItems = help?.subitems ?? []; helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: <BsMarkdownFill /> }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; @@ -1067,7 +1035,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB askGPT = action(async () => { try { - GPTPopup.Instance.setSidebarId(this.SidebarKey); + GPTPopup.Instance.setSidebarId(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { @@ -1170,7 +1138,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); - tr.doc.nodesBetween(selection.from, selection.to, (node: any, pos: number /* , parent: any */) => { + tr.doc.nodesBetween(selection.from, selection.to, (node: Node, pos: number /* , parent: any */) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allAnchors = [{ href, title, anchorId: anchor[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); @@ -1194,7 +1162,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getView = async (doc: Doc, options: FocusViewOptions) => { - if (DocListCast(this.dataDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { if (!this.SidebarShown) { this.toggleSidebar(false); options.didMove = true; @@ -1247,17 +1215,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below if (this._editorView && textAnchorId) { - const editor = this._editorView; - const ret = findAnchorFrag(editor.state.doc.content, editor); + const { state } = this._editorView; + const ret = findAnchorFrag(state.doc.content, this._editorView); - const content = (ret.frag as any)?.content; - if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { + const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined; + if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) { !options.instant && (this._focusSpeed = focusSpeed); - let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start + let selection = TextSelection.near(state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { - selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected + selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } - editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); + this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); setTimeout(() => { @@ -1333,9 +1301,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey]; const dataData = this.dataDoc[this.fieldKey]; const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey]; - const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; - const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; - const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; + const dataTime = dataData ? (DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; @@ -1494,41 +1462,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let el = elIn; while (el && el !== document.body) { if (getComputedStyle(el).display === 'none') return false; - el = el.parentNode as any; + el = el.parentElement; } return true; } - richTextMenuPlugin() { - const self = this; + static richTextMenuPlugin(props: FormattedTextBoxProps) { return new Plugin({ - view(newView) { - runInAction(() => { - self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); - }); - return new RichTextMenuPlugin({ editorProps: this._props }); - }, + view: action((newView: EditorView) => { + props?.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + return new RichTextMenuPlugin({ editorProps: props }); + }), }); } _didScroll = false; _scrollStopper: undefined | (() => void); + // eslint-disable-next-line @typescript-eslint/no-explicit-any setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { - const self = this; this._editorView?.destroy(); this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: editorView => { const docPos = editorView.coordsAtPos(editorView.state.selection.to); - const viewRect = self._ref.current!.getBoundingClientRect(); - const scrollRef = self._scrollRef; + const viewRect = this._ref.current!.getBoundingClientRect(); + const scrollRef = this._scrollRef; const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); - const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; + const scrollPos = scrollRef.scrollTop + shift * this.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { setTimeout(() => { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); @@ -1541,7 +1506,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; }, dispatchTransaction: this.dispatchTransaction, - nodeViews: FormattedTextBox.nodeViews(this), + nodeViews: FormattedTextBox._nodeViews(this), clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); @@ -1559,7 +1524,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); } } - (this._editorView as any).TextView = this; + this._editorView.TextView = this; } const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); @@ -1584,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } else if (!FormattedTextBox.DontSelectInitialText) { const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); selectAll(this._editorView.state, (tx: Transaction) => { - this._editorView?.dispatch(tx.deleteSelection().addStoredMark(mark)); + this._editorView?.dispatch(tx.addStoredMark(mark)); }); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else { @@ -1637,18 +1602,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } onPointerDown = (e: React.PointerEvent): void => { - if ((e.nativeEvent as any).handledByInnerReactInstance) { - return; // e.stopPropagation(); - } - (e.nativeEvent as any).handledByInnerReactInstance = true; - if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. - if ((e.target as any).tagName === 'AUDIOTAG') { + const target = e.target as HTMLElement; + if (target.tagName === 'AUDIOTAG') { e.preventDefault(); e.stopPropagation(); - const timecode = Number((e.target as any)?.dataset?.timecode); - DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { + const timecode = Number(target.dataset?.timecode); + DocServer.GetRefField(target.dataset?.audioid || '').then(anchor => { if (anchor instanceof Doc) { // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; @@ -1672,7 +1633,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. e.stopPropagation(); // if the text box's content is active, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); - (this.ProseRef?.children?.[0] as any).focus(); + (this.ProseRef?.children?.[0] as HTMLElement).focus(); } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { @@ -1680,7 +1641,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }; onSelectEnd = () => { - GPTPopup.Instance.setSidebarId(this.SidebarKey); + GPTPopup.Instance.setSidebarId(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; document.removeEventListener('pointerup', this.onSelectEnd); }; @@ -1688,10 +1649,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const state = this.EditorView?.state; if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - let clickTarget = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - for (let { target } = e as any; target && !target.dataset?.targethrefs; target = target.parentElement); - while (clickTarget && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, clickTarget?.dataset?.targethrefs, clickTarget?.dataset.linkdoc, clickTarget?.dataset.nopreview === 'true'); + let clickTarget: HTMLElement | Element | null = e.target as HTMLElement; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let target: HTMLElement | Element | null = clickTarget as HTMLElement; target instanceof HTMLElement && !target.dataset?.targethrefs; target = target.parentElement); + while (clickTarget instanceof HTMLElement && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; + const dataset = clickTarget instanceof HTMLElement ? clickTarget?.dataset : undefined; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true'); } }; @action @@ -1715,27 +1677,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setFocus = (ipos?: number) => { const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1); setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100); - setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200); + setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200); }; @action onFocused = (e: React.FocusEvent): void => { // applyDevTools.applyDevTools(this._editorView); - this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { if (!this._props.isContentActive()) return; - if ((e.nativeEvent as any).handledByInnerReactInstance) { - e.stopPropagation(); - return; - } - if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { + const editorView = this._editorView; + const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined; + if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. - const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) - if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2))); + const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY }); + const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) + if (pcords && node?.type === editorView.state.schema.nodes.dashComment) { + this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); e.preventDefault(); } if (!node && this.ProseRef) { @@ -1743,19 +1702,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const boundsRect = lastNode?.getBoundingClientRect(); if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document - this._editorView?.focus(); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); + editorView.focus(); + editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); } - } else if (node && [this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node.type) && node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); + } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) { + editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos))); } } - if (this._props.rootSelected?.()) { + if (editorView && this._props.rootSelected?.()) { // if text box is selected, then it consumes all click events - (e.nativeEvent as any).handledByInnerReactInstance = true; - this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, e.shiftKey); + e.stopPropagation(); + this.hitBulletTargets(e.clientX, e.clientY, !editorView.state.selection.empty || this._forceUncollapse, false, e.shiftKey); } - this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; + this._forceUncollapse = !editorRoot?.getSelection()?.isCollapsed; }; // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) { @@ -1771,9 +1730,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let $olistPos = this._editorView?.state.doc.resolve(olistPos); let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) { - if ($olistPos && ($olistPos as any).path.length > 3) { + if ($olistPos && $olistPos.depth) { olistNode = $olistPos.parent; - $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); + $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1)); } } const maxSize = this._editorView?.state.doc.content.size ?? 0; @@ -1804,7 +1763,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @action - onBlur = (e: any) => { + onBlur = (e: React.FocusEvent) => { if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) { const stordMarks = this._editorView?.state.storedMarks?.slice(); @@ -1822,7 +1781,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } - FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; // this is the markdown for @<published name> document publishing to Doc.myPublishedDocs const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); @@ -1869,7 +1827,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB switch (e.key) { case 'Escape': this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); + (document.activeElement as HTMLElement).blur?.(); DocumentView.DeselectAll(); RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; @@ -1936,14 +1894,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; fitContentsToBox = () => BoolCast(this.Document._freeform_fitContentsToBox); sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.sidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; - sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); - sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); + sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); + sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.sidebarKey); setSidebarHeight = (height: number) => { - this.dataDoc[this.SidebarKey + '_height'] = height; + this.dataDoc[this.sidebarKey + '_height'] = height; }; sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => @@ -1973,9 +1931,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get sidebarHandle() { TraceMobx(); - const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; + const annotated = DocListCast(this.dataDoc[this.sidebarKey]).filter(d => d?.author).length; const color = !annotated ? Colors.WHITE : Colors.BLACK; - const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); + const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : (this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')) as string); return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : ( <div @@ -1992,6 +1950,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get sidebarCollection() { const renderComponent = (tag: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const ComponentTag: any = tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos @@ -2026,7 +1985,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} - viewField={this.SidebarKey} + viewField={this.sidebarKey} isAnnotationOverlay={false} select={emptyFunction} isAnyChildContentActive={returnFalse} @@ -2041,14 +2000,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB fitContentsToBox={this.fitContentsToBox} noSidebar treeViewHideTitle - fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} + fieldKey={this.layoutDoc[this.sidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> ); }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} + {renderComponent(StrCast(this.layoutDoc[this.sidebarKey + '_type_collection']))} </div> ); } @@ -2119,19 +2078,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); } }; - _oldWheel: any; - @computed get fontColor() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor); - } - @computed get fontSize() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); - } - @computed get fontFamily() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); - } - @computed get fontWeight() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight); - } + render() { TraceMobx(); const scale = this._props.NativeDimScaling?.() || 1; @@ -2225,6 +2172,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, + _layout_noSidebar: true, defaultDoubleClick: 'ignore', systemIcon: 'BsFileEarmarkTextFill', }, diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 01c46edeb..6c0eac103 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,4 +1,4 @@ -import { Mark, ResolvedPos } from 'prosemirror-model'; +import { Mark, Node, ResolvedPos } from 'prosemirror-model'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { ClientUtils } from '../../../../ClientUtils'; @@ -61,8 +61,8 @@ export class FormattedTextBoxComment { tooltip.style.display = 'none'; tooltip.appendChild(tooltipText); tooltip.onpointerdown = (e: PointerEvent) => { - const { textBox, startUserMarkRegion, endUserMarkRegion, userMark } = FormattedTextBoxComment; - false && startUserMarkRegion !== undefined && textBox?.adoptAnnotation(startUserMarkRegion, endUserMarkRegion, userMark); + // const { textBox, startUserMarkRegion, endUserMarkRegion, userMark } = FormattedTextBoxComment; + // startUserMarkRegion !== undefined && textBox?.adoptAnnotation(startUserMarkRegion, endUserMarkRegion, userMark); e.stopPropagation(); e.preventDefault(); }; @@ -73,7 +73,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.textBox = undefined; FormattedTextBoxComment.tooltip.style.display = 'none'; } - public static saveMarkRegion(textBox: any, start: number, end: number, mark: Mark) { + public static saveMarkRegion(textBox: FormattedTextBox, start: number, end: number, mark: Mark) { FormattedTextBoxComment.textBox = textBox; FormattedTextBoxComment.startUserMarkRegion = start; FormattedTextBoxComment.endUserMarkRegion = end; @@ -87,7 +87,7 @@ export class FormattedTextBoxComment { const start = view.coordsAtPos(state.selection.from - nbef); const end = view.coordsAtPos(state.selection.from - nbef); // The box in which the tooltip is positioned, to use as base - const box = (document.getElementsByClassName('mainView-container') as any)[0].getBoundingClientRect(); + const box = document.getElementsByClassName('mainView-container')[0].getBoundingClientRect(); // Find a center-ish x position from the selection endpoints (when crossing lines, end may be more to the left) const left = Math.max((start.left + end.left) / 2, start.left + 3); FormattedTextBoxComment.tooltip.style.left = left - box.left + 'px'; @@ -118,8 +118,8 @@ export class FormattedTextBoxComment { const nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); const noselection = state.selection.$from === state.selection.$to; - let child: any = null; - state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any /* , pos: number, parent: any */) => { + let child: Node | undefined; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node: Node /* , pos: number, parent: any */) => { !child && node.marks.length && (child = node); }); const mark = child && findOtherUserMark(child.marks); diff --git a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts index 8799964b3..d41938698 100644 --- a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts +++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts @@ -1,18 +1,18 @@ -import { Node, DOMOutputSpec } from 'prosemirror-model'; +import { Node, DOMOutputSpec, AttributeSpec, TagParseRule } from 'prosemirror-model'; import clamp from '../../../util/clamp'; import convertToCSSPTValue from '../../../util/convertToCSSPTValue'; import toCSSLineSpacing from '../../../util/toCSSLineSpacing'; // import type { NodeSpec } from './Types'; type NodeSpec = { - attrs?: { [key: string]: any }; + attrs?: { [key: string]: AttributeSpec }; content?: string; draggable?: boolean; group?: string; inline?: boolean; name?: string; - parseDOM?: Array<any>; - toDOM?: (node: any) => DOMOutputSpec; + parseDOM?: Array<TagParseRule>; + toDOM?: (node: Node) => DOMOutputSpec; }; // This assumes that every 36pt maps to one indent level. @@ -30,7 +30,7 @@ function convertMarginLeftToIndentValue(marginLeft: string): number { return clamp(MIN_INDENT_LEVEL, Math.floor(ptValue / INDENT_MARGIN_PT_SIZE), MAX_INDENT_LEVEL); } -function getAttrs(dom: HTMLElement): Object { +export function getAttrs(dom: HTMLElement): object { const { lineHeight, textAlign, marginLeft, paddingTop, paddingBottom } = dom.style; let align = dom.getAttribute('align') || textAlign || ''; @@ -50,9 +50,31 @@ function getAttrs(dom: HTMLElement): Object { return { align, indent, lineSpacing, paddingTop, paddingBottom, id }; } -function toDOM(node: Node): DOMOutputSpec { +export function getHeadingAttrs(dom: HTMLElement): { align?: string; indent?: number; lineSpacing?: string; paddingTop?: string; paddingBottom?: string; id: string; level?: number } { + const { lineHeight, textAlign, marginLeft, paddingTop, paddingBottom } = dom.style; + + let align = dom.getAttribute('align') || textAlign || ''; + align = ALIGN_PATTERN.test(align) ? align : ''; + + let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || '', 10); + + if (!indent && marginLeft) { + indent = convertMarginLeftToIndentValue(marginLeft); + } + + indent = indent || MIN_INDENT_LEVEL; + + const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : undefined; + + const level = Number(dom.nodeName.substring(1)) || 1; + + const id = dom.getAttribute('id') || ''; + return { align, indent, lineSpacing, paddingTop, paddingBottom, id, level }; +} + +export function toDOM(node: Node): DOMOutputSpec { const { align, indent, inset, lineSpacing, paddingTop, paddingBottom, id } = node.attrs; - const attrs: { [key: string]: any } | null = {}; + const attrs: { [key: string]: unknown } | null = {}; let style = ''; if (align && align !== 'left') { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index a612f3c65..738f6d699 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,11 +1,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { lift, wrapIn } from 'prosemirror-commands'; +import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; import { wrapInList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; +import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; @@ -17,13 +17,11 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../DocumentView'; import { EquationBox } from '../EquationBox'; import { FieldViewProps } from '../FieldView'; -import { FormattedTextBox } from './FormattedTextBox'; +import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; -const { toggleMark } = require('prosemirror-commands'); - @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // eslint-disable-next-line no-use-before-define @@ -35,8 +33,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { private _linkToRef = React.createRef<HTMLInputElement>(); layoutDoc: Doc | undefined; - @observable public view?: EditorView = undefined; - public editorProps: FieldViewProps | undefined; + @observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined; + public editorProps: FieldViewProps | AntimodeMenuProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -114,17 +112,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } _disposer: IReactionDisposer | undefined; componentDidMount() { - this._disposer = reaction( - () => DocumentView.Selected().slice(), - () => this.updateMenu(undefined, undefined, undefined, undefined) - ); + // this._disposer = reaction( + // () => DocumentView.Selected().slice(), + // () => this.updateMenu(undefined, undefined, undefined, undefined) + // ); } componentWillUnmount() { this._disposer?.(); } @action - public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any, layoutDoc: Doc | undefined) { + public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, layoutDoc: Doc | undefined) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } @@ -158,7 +156,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); } - setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { + setMark = (mark: Mark, state: EditorState, dispatch: (tr: Transaction) => void, dontToggle: boolean = false) => { if (mark) { const newPos = state.selection.$anchor.node()?.type === schema.nodes.ordered_list ? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); @@ -177,25 +175,26 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { toggleMark(mark.type, mark.attrs)(state, dispatch); } } - this.updateMenu(this.view, undefined, undefined, this.layoutDoc); + // this.updateMenu(this.view, undefined, undefined, this.layoutDoc); } }; // finds font sizes and families in selection - getActiveAlignment() { + getActiveAlignment = () => { if (this.view && this.TextView?._props.rootSelected?.()) { - const { path } = this.view.state.selection.$from as any; - for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { - if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { - return path[i].attrs.align || 'left'; + const from = this.view.state.selection.$from; + for (let i = from.depth; i >= 0; i--) { + const node = from.node(i); + if (node.type === this.view.state.schema.nodes.paragraph || node.type === this.view.state.schema.nodes.heading) { + return node.attrs.align || 'left'; } } } return 'left'; - } + }; // finds font sizes and families in selection - getActiveListStyle() { + getActiveListStyle = () => { const state = this.view?.state; if (state) { const pos = state.selection.$anchor; @@ -207,7 +206,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } return ''; - } + }; // finds font sizes and families in selection getActiveFontStylesOnSelection() { @@ -321,7 +320,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); this.setMark(mark, this.view.state, this.view.dispatch, false); - this.TextView.autoLink(); + this.TextView?.autoLink(); this.view.focus(); } }; @@ -350,7 +349,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }; setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { - if (this.view) { + if (this.TextView && this.view) { const { text, paragraph } = this.view.state.schema.nodes; const selNode = this.view.state.selection.$anchor.node(); if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { @@ -360,11 +359,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); - this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.setMark(fmark, this.view.state, (tx: Transaction) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else { Doc.UserDoc()[fontField] = value; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } }; @@ -383,17 +382,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { marks && tx2.setStoredMarks([...marks]); this.view.dispatch(tx2); } else - !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { + !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); this.view!.dispatch(tx3); }); this.view.focus(); - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + // this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; - insertSummarizer(state: EditorState, dispatch: any) { + insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) { if (state.selection.empty) return false; const mark = state.schema.marks.summarize.create(); const { tr } = state; @@ -407,7 +406,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { vcenterToggle = () => { this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); }; - align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { + align = (view: EditorView, dispatch: (tr: Transaction) => void, alignment: 'left' | 'right' | 'center') => { if (this.TextView?._props.rootSelected?.()) { let { tr } = view.state; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { @@ -423,7 +422,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - paragraphSetup(state: EditorState, dispatch: any, field: 'inset' | 'indent', value?: 0 | 10 | -10) { + paragraphSetup(state: EditorState, dispatch: (tr: Transaction) => void, field: 'inset' | 'indent', value?: 0 | 10 | -10) { let { tr } = state; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { @@ -439,9 +438,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } - insertBlockquote(state: EditorState, dispatch: any) { - const { path } = state.selection.$from as any; - if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) { + insertBlockquote(state: EditorState, dispatch: (tr: Transaction) => void) { + const node = state.selection.$from.depth ? state.selection.$from.node(state.selection.$from.depth - 1) : undefined; + if (node?.type === schema.nodes.blockquote) { lift(state, dispatch); } else { wrapIn(schema.nodes.blockquote)(state, dispatch); @@ -449,7 +448,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } - insertHorizontalRule(state: EditorState, dispatch: any) { + insertHorizontalRule(state: EditorState, dispatch: (tr: Transaction) => void) { dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView()); return true; } @@ -497,7 +496,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } get TextView() { - return (this.view as any)?.TextView as FormattedTextBox; + return this.view?.TextView; } get TextViewFieldKey() { return this.TextView?._props.fieldKey; @@ -512,19 +511,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } createLinkButton() { - const self = this; - - function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) { - self.TextView?.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), 'link change'); - } + const onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.TextView?.endUndoTypingBatch(); + UndoManager.RunInBatch(() => this.setCurrentLink(e.target.value), 'link change'); + }; const link = this.currentLink ? this.currentLink : ''; const button = ( <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> { - // eslint-disable-next-line jsx-a11y/control-has-associated-label <button type="button" className="antimodeMenu-button color-preview-button"> <FontAwesomeIcon icon="link" size="lg" /> </button> @@ -589,7 +585,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // TODO: should check for valid URL @undoBatch makeLinkToURL = (target: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); + this.TextView?.makeLinkAnchor(undefined, 'onRadd:rightight', target, target); }; @undoBatch @@ -597,12 +593,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); if (linkAnchor) { - const allAnchors = linkAnchor.attrs.allAnchors.slice(); - this.TextView.RemoveAnchorFromSelection(allAnchors); + const allAnchors = (linkAnchor.attrs.allAnchors as { href: string; title: string; linkId: string; targetId: string }[]).slice(); + this.TextView?.RemoveAnchorFromSelection(allAnchors); // 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. allAnchors - .filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0) - .forEach((aref: any) => { + .filter(aref => aref?.href.indexOf(Doc.localServerPath()) === 0) + .forEach(aref => { const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0]; anchorId && DocServer.GetRefField(anchorId).then(linkDoc => Doc.DeleteLink?.(linkDoc as Doc)); }); @@ -629,7 +625,7 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; - constructor(props: any) { + constructor(props: ButtonDropdownProps) { super(props); makeObservable(this); } @@ -683,7 +679,6 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps <> {this._props.button} { - // eslint-disable-next-line jsx-a11y/control-has-associated-label <button type="button" className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> <FontAwesomeIcon icon="caret-down" size="sm" /> </button> @@ -697,12 +692,12 @@ export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps } interface RichTextMenuPluginProps { - editorProps: any; + editorProps: FormattedTextBoxProps; } export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { // eslint-disable-next-line react/no-unused-class-component-methods - update(view: EditorView, lastState: EditorState | undefined) { - RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc); + update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) { + RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc); } render() { return null; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index bf11dfe62..e0d6c7c05 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,4 +1,5 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; +import { NodeType } from 'prosemirror-model'; import { NodeSelection, TextSelection } from 'prosemirror-state'; import { ClientUtils } from '../../../../ClientUtils'; import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; @@ -6,7 +7,7 @@ import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { NumCast, StrCast } from '../../../../fields/Types'; -import { Utils } from '../../../../Utils'; +import { emptyFunction, Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocUtils } from '../../../documents/DocUtils'; @@ -35,13 +36,7 @@ export class RichTextRules { wrappingInputRule(/%>$/, schema.nodes.blockquote), // 1. create numerical ordered list - wrappingInputRule( - /^1\.\s$/, - schema.nodes.ordered_list, - () => ({ mapStyle: 'decimal', bulletStyle: 1 }), - (match: any, node: any) => node.childCount + node.attrs.order === +match[1], - ((type: any) => ({ type: type, attrs: { mapStyle: 'decimal', bulletStyle: 1 } })) as any - ), + wrappingInputRule(/^1\.\s$/, schema.nodes.ordered_list, () => ({ mapStyle: 'decimal', bulletStyle: 1 }), emptyFunction, ((type: unknown) => ({ type, attrs: { mapStyle: 'decimal', bulletStyle: 1 } })) as unknown as null), // A. create alphabetical ordered list wrappingInputRule( @@ -49,9 +44,8 @@ export class RichTextRules { schema.nodes.ordered_list, // match => { () => ({ mapStyle: 'multi', bulletStyle: 1 }), - // return ({ order: +match[1] }) - (match: any, node: any) => node.childCount + node.attrs.order === +match[1], - ((type: any) => ({ type: type, attrs: { mapStyle: 'multi', bulletStyle: 1 } })) as any + emptyFunction, + ((type: NodeType) => ({ type, attrs: { mapStyle: 'multi', bulletStyle: 1 } })) as unknown as null ), // * + - create bullet list @@ -60,8 +54,8 @@ export class RichTextRules { schema.nodes.ordered_list, // match => { () => ({ mapStyle: 'bullet' }), // ({ order: +match[1] }) - (match: any, node: any) => node.childCount + node.attrs.order === +match[1], - ((type: any) => ({ type: type, attrs: { mapStyle: 'bullet' } })) as any + emptyFunction, + ((type: NodeType) => ({ type: type, attrs: { mapStyle: 'bullet' } })) as unknown as null ), // ``` create code block @@ -93,7 +87,7 @@ export class RichTextRules { const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); textDoc.inlineTextCount = numInlines + 1; - const node = (state.doc.resolve(start) as any).nodeAfter; + const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' }); const sm = state.storedMarks || undefined; @@ -137,7 +131,7 @@ export class RichTextRules { textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation - const node = (state.doc.resolve(start) as any).nodeAfter; + const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); const sm = state.storedMarks || undefined; @@ -154,8 +148,8 @@ export class RichTextRules { // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule(/(%d|d)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; - const pos = state.doc.resolve(start) as any; - for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + const pos = state.doc.resolve(start); + for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); if (node.type === schema.nodes.paragraph) { const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 }); @@ -169,8 +163,8 @@ export class RichTextRules { // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule(/(%h|h)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; - const pos = state.doc.resolve(start) as any; - for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + const pos = state.doc.resolve(start); + for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); if (node.type === schema.nodes.paragraph) { const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 }); @@ -184,12 +178,12 @@ export class RichTextRules { // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule(/(%q|q)$/, (state, match, start, end) => { if (!match[0].startsWith('%') && !this.EnteringStyle) return null; - const pos = state.doc.resolve(start) as any; + const pos = state.doc.resolve(start); if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) { const { node } = state.selection; return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 }); } - for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) { + for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); if (node.type === schema.nodes.paragraph) { const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 }); @@ -202,9 +196,9 @@ export class RichTextRules { // center justify text new InputRule(/%\^/, (state, match, start, end) => { - const resolved = state.doc.resolve(start) as any; + const resolved = state.doc.resolve(start); if (resolved?.parent.type.name === 'paragraph') { - return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks); + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.start() - 1, schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks); } const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; @@ -214,9 +208,9 @@ export class RichTextRules { // left justify text new InputRule(/%\[/, (state, match, start, end) => { - const resolved = state.doc.resolve(start) as any; + const resolved = state.doc.resolve(start); if (resolved?.parent.type.name === 'paragraph') { - return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks); + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.start() - 1, schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks); } const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; @@ -226,9 +220,9 @@ export class RichTextRules { // right justify text new InputRule(/%\]/, (state, match, start, end) => { - const resolved = state.doc.resolve(start) as any; + const resolved = state.doc.resolve(start); if (resolved?.parent.type.name === 'paragraph') { - return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks); + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.start() - 1, schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks); } const node = resolved.nodeAfter; const sm = state.storedMarks || undefined; @@ -402,7 +396,7 @@ export class RichTextRules { }), // create an inline view of a tag stored under the '#' field - new InputRule(/#([a-zA-Z_-]+[a-zA-Z_\-0-9]*)\s$/, (state, match, start, end) => { + new InputRule(/#(@?[a-zA-Z_-]+[a-zA-Z_\-0-9]*)\s$/, (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; // this.Document[DocData]['#' + tag] = '#' + tag; @@ -410,6 +404,7 @@ export class RichTextRules { if (!tags.includes(tag)) { tags.push(tag); this.Document[DocData].tags = new List<string>(tags); + this.Document[DocData].showTags = true; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag }); return state.tr @@ -426,9 +421,9 @@ export class RichTextRules { if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??'; - const node = (state.doc.resolve(start) as any).nodeAfter; + const node = state.doc.resolve(start).nodeAfter; - if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); + if (node?.marks.findIndex(m => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); return node ? state.tr .removeMark(start, end, schema.marks.user_mark) @@ -438,7 +433,7 @@ export class RichTextRules { }), new InputRule(/%\(/, (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; + const node = state.doc.resolve(start).nodeAfter; const sm = state.storedMarks?.slice() || []; const mark = state.schema.marks.summarizeInclusive.create(); @@ -447,7 +442,7 @@ export class RichTextRules { const content = selected.selection.content(); const replaced = node ? selected.replaceRangeWith(start, end, schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]); + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...(node?.marks ?? []), ...sm]); }), new InputRule(/%\)/, (state, match, start, end) => state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create())), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 6e1f325cf..ba8e4faed 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -34,14 +34,14 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'a[href]', - getAttrs(dom: any) { + getAttrs: dom => { return { title: dom.getAttribute('title'), }; }, }, ], - toDOM(node: any) { + toDOM: node => { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /* 'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; @@ -53,7 +53,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'div', - getAttrs(dom: any) { + getAttrs: dom => { return { noAutoLink: dom.getAttribute('data-noAutoLink'), }; @@ -80,7 +80,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'a[href]', - getAttrs(dom: any) { + getAttrs: dom => { return { title: dom.getAttribute('title'), noPreview: dom.getAttribute('noPreview'), @@ -88,7 +88,7 @@ export const marks: { [index: string]: MarkSpec } = { }, }, ], - toDOM(node: any) { + toDOM: node => { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); return node.attrs.docref && node.attrs.title @@ -117,7 +117,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs(dom: any) { + getAttrs: dom => { return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : '' }; }, }, @@ -131,7 +131,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs(dom: any) { + getAttrs: dom => { const cstyle = getComputedStyle(dom); if (cstyle.font) { if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' }; @@ -154,7 +154,7 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs(dom: any) { + getAttrs: dom => { return { color: dom.getAttribute('color') }; }, }, @@ -170,12 +170,12 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs(dom: any) { + getAttrs: dom => { return { fontHighlight: dom.getAttribute('background-color') }; }, }, ], - toDOM(node: any) { + toDOM: node => { return node.attrs.fontHighlight ? ['span', { style: 'background-color:' + node.attrs.fontHighlight }] : ['span', { style: 'background-color: transparent' }]; }, }, @@ -224,7 +224,7 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { bulletType: { default: 'decimal' }, }, - toDOM(node: any) { + toDOM: node => { return [ 'span', { @@ -238,11 +238,11 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs: (p: any) => { + getAttrs: p => { if (typeof p !== 'string') { const style = getComputedStyle(p); if (style.textDecoration === 'underline') return null; - if (p.parentElement.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement.outerHTML.indexOf('text-decoration-style: solid') !== -1) { + if (p.parentElement?.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement?.outerHTML.indexOf('text-decoration-style: solid') !== -1) { return null; } } @@ -266,11 +266,11 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs: (p: any) => { + getAttrs: p => { if (typeof p !== 'string') { const style = getComputedStyle(p); if (style.textDecoration === 'underline') return null; - if (p.parentElement.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement.outerHTML.indexOf('text-decoration-style: dotted') !== -1) { + if (p.parentElement?.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement?.outerHTML.indexOf('text-decoration-style: dotted') !== -1) { return null; } } @@ -292,10 +292,10 @@ export const marks: { [index: string]: MarkSpec } = { parseDOM: [ { tag: 'span', - getAttrs: (p: any) => { + getAttrs: p => { if (typeof p !== 'string') { const style = getComputedStyle(p); - if (style.textDecoration === 'underline' || p.parentElement.outerHTML.indexOf('text-decoration-style:line') !== -1) { + if (style.textDecoration === 'underline' || p.parentElement?.outerHTML.indexOf('text-decoration-style:line') !== -1) { return null; } } @@ -317,7 +317,7 @@ export const marks: { [index: string]: MarkSpec } = { selected: { default: false }, }, parseDOM: [{ style: 'background: yellow' }], - toDOM(node: any) { + toDOM: node => { return ['span', { style: `background: ${node.attrs.selected ? 'orange' : 'yellow'}` }]; }, }, @@ -330,7 +330,7 @@ export const marks: { [index: string]: MarkSpec } = { }, excludes: 'user_mark', group: 'inline', - toDOM(node: any) { + toDOM: node => { const uid = node.attrs.userid.replace(/\./g, '').replace(/@/g, ''); const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); @@ -348,7 +348,7 @@ export const marks: { [index: string]: MarkSpec } = { }, group: 'inline', inclusive: false, - toDOM(node: any) { + toDOM: node => { const uid = node.attrs.userid.replace('.', '').replace('@', ''); return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0]; }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 5bf942218..02ded3103 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -1,6 +1,6 @@ import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; -import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; +import { ParagraphNodeSpec, toParagraphDOM, getHeadingAttrs } from './ParagraphNodeSpec'; import { DocServer } from '../../../DocServer'; import { Doc, Field, FieldType } from '../../../../fields/Doc'; import { schema } from './schema_rts'; @@ -53,7 +53,7 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [ { tag: 'audiotag', - getAttrs(dom: any) { + getAttrs: dom => { return { timeCode: dom.getAttribute('data-timecode'), audioId: dom.getAttribute('data-audioid'), @@ -123,24 +123,57 @@ export const nodes: { [index: string]: NodeSpec } = { level: { default: 1 }, }, parseDOM: [ - { tag: 'h1', attrs: { level: 1 } }, - { tag: 'h2', attrs: { level: 2 } }, - { tag: 'h3', attrs: { level: 3 } }, - { tag: 'h4', attrs: { level: 4 } }, - { tag: 'h5', attrs: { level: 5 } }, - { tag: 'h6', attrs: { level: 6 } }, + { + tag: 'h1', + attrs: { level: 1 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, + { + tag: 'h2', + attrs: { level: 2 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, + { + tag: 'h3', + attrs: { level: 3 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, + { + tag: 'h4', + attrs: { level: 4 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, + { + tag: 'h5', + attrs: { level: 5 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, + { + tag: 'h6', + attrs: { level: 6 }, + getAttrs(dom) { + return getHeadingAttrs(dom); + }, + }, ], toDOM(node) { - const dom = toParagraphDOM(node) as any; - dom[0] = `h${node.attrs.level || 1}`; + const dom = toParagraphDOM(node); + if (dom instanceof Array) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (dom as any)[0] = `h${node.attrs.level || 1}`; // [0] is readonly so cast away to any + } return dom; }, - getAttrs(dom: any) { - const attrs = getParagraphNodeAttrs(dom) as any; - const level = Number(dom.nodeName.substring(1)) || 1; - attrs.level = level; - return attrs; - }, }, // :: NodeSpec A code listing. Disallows marks or non-text inline @@ -221,7 +254,7 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [ { tag: 'img[src]', - getAttrs(dom: any) { + getAttrs: dom => { return { src: dom.getAttribute('src'), title: dom.getAttribute('title'), @@ -300,7 +333,7 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [ { tag: 'video[src]', - getAttrs(dom: any) { + getAttrs: dom => { return { src: dom.getAttribute('src'), title: dom.getAttribute('title'), @@ -341,33 +374,31 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [ { tag: 'ul', - getAttrs(dom: any) { + getAttrs: dom => { return { bulletStyle: dom.getAttribute('data-bulletStyle'), mapStyle: dom.getAttribute('data-mapStyle'), fontColor: dom.style.color, - fontSize: dom.style['font-size'], - fontFamily: dom.style['font-family'], - indent: dom.style['margin-left'], + fontSize: dom.style.fontSize, + fontFamily: dom.style.fontFamily, + indent: dom.style.marginLeft, }; }, }, { style: 'list-style-type=disc', - getAttrs() { - return { mapStyle: 'bullet' }; - }, + getAttrs: () => ({ mapStyle: 'bullet' }), }, { tag: 'ol', - getAttrs(dom: any) { + getAttrs: dom => { return { bulletStyle: dom.getAttribute('data-bulletStyle'), mapStyle: dom.getAttribute('data-mapStyle'), fontColor: dom.style.color, - fontSize: dom.style['font-size'], - fontFamily: dom.style['font-family'], - indent: dom.style['margin-left'], + fontSize: dom.style.fontSize, + fontFamily: dom.style.fontFamily, + indent: dom.style.marginLeft, }; }, }, @@ -416,7 +447,7 @@ export const nodes: { [index: string]: NodeSpec } = { parseDOM: [ { tag: 'li', - getAttrs(dom: any) { + getAttrs: dom => { return { mapStyle: dom.getAttribute('data-mapStyle'), bulletStyle: dom.getAttribute('data-bulletStyle') }; }, }, |
