diff options
Diffstat (limited to 'src/client/views/nodes/formattedText')
18 files changed, 1035 insertions, 579 deletions
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index aa269d8d6..b7d2a24c2 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -2,7 +2,7 @@ import { TextSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; -import React = require('react'); +import * as React from 'react'; // creates an inline comment in a note when '>>' is typed. // the comment sits on the right side of the note and vertically aligns with its anchor in the text. @@ -54,7 +54,7 @@ interface IDashDocCommentViewInternal { } export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> { - constructor(props: IDashDocCommentViewInternal) { + constructor(props: any) { super(props); this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); this.onPointerEnterCollapsed = this.onPointerEnterCollapsed.bind(this); diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index c5167461b..7335c9286 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,18 +1,21 @@ -import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { Height, Width } from '../../../../fields/DocSymbols'; -import { NumCast, StrCast } from '../../../../fields/Types'; +import { NumCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { Transform } from '../../../util/Transform'; -import { DocFocusOptions, DocumentView } from '../DocumentView'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { DocumentView } from '../DocumentView'; +import { FocusViewOptions } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); +var horizPadding = 3; // horizontal padding to container to allow cursor to show up on either side. export class DashDocView { dom: HTMLSpanElement; // container for label and value root: any; @@ -21,8 +24,7 @@ export class DashDocView { this.dom = document.createElement('span'); this.dom.style.position = 'relative'; this.dom.style.textIndent = '0'; - this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, 'lightGray'); - this.dom.style.width = node.attrs.width; + 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; @@ -62,7 +64,12 @@ export class DashDocView { } catch {} }); } - selectNode() {} + deselectNode() { + this.dom.style.backgroundColor = ''; + } + selectNode() { + this.dom.style.backgroundColor = 'rgb(141, 182, 247)'; + } } interface IDashDocViewInternal { @@ -78,27 +85,27 @@ interface IDashDocViewInternal { getPos: any; } @observer -export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { +export class DashDocViewInternal extends ObservableReactComponent<IDashDocViewInternal> { _spanRef = React.createRef<HTMLDivElement>(); _disposers: { [name: string]: IReactionDisposer } = {}; _textBox: FormattedTextBox; - @observable _dashDoc: Doc | undefined; - @observable _finalLayout: any; - @observable _width: number = 0; - @observable _height: number = 0; + @observable _dashDoc: Doc | undefined = undefined; + @computed get _width() { + return NumCast(this._dashDoc?._width); + } + @computed get _height() { + return NumCast(this._dashDoc?._height); + } updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; - this._finalLayout = dashDoc; - if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') { + if (this._props.width !== (this._dashDoc?._width ?? '') + 'px' || this._props.height !== (this._dashDoc?._height ?? '') + 'px') { try { - this._width = NumCast(this._dashDoc?._width); - this._height = NumCast(this._dashDoc?._height); // bcz: an exception will be thrown if two embeddings are open at the same time when a doc view comment is made - this.props.view.dispatch( - this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { - ...this.props.node.attrs, + this._props.view.dispatch( + this._props.view.state.tr.setNodeMarkup(this._props.getPos(), null, { + ...this._props.node.attrs, width: this._width + 'px', height: this._height + 'px', }) @@ -111,16 +118,17 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { constructor(props: IDashDocViewInternal) { super(props); - this._textBox = this.props.tbox; + makeObservable(this); + this._textBox = this._props.tbox; - DocServer.GetRefField(this.props.docId + this.props.embedding).then(async dashDoc => { + DocServer.GetRefField(this._props.docId + this._props.embedding).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { - this.props.embedding && - DocServer.GetRefField(this.props.docId).then(async dashDocBase => { + this._props.embedding && + DocServer.GetRefField(this._props.docId).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const embedding = Doc.MakeEmbedding(dashDocBase, this.props.docId + this.props.embedding); + const embedding = Doc.MakeEmbedding(dashDocBase, this._props.docId + this._props.embedding); embedding.layout_fieldKey = 'layout'; - this.props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this.props.fieldKey, undefined); + this._props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this._props.fieldKey, undefined); this.updateDoc(embedding); } }); @@ -132,23 +140,18 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { componentDidMount() { this._disposers.upater = reaction( - () => this._dashDoc && NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width), - action(() => { - if (this._dashDoc) { - this._width = NumCast(this._dashDoc._width); - this._height = NumCast(this._dashDoc._height); - const parent = this._spanRef.current?.parentNode as HTMLElement; - if (parent) { - parent.style.width = this._width + 'px'; - parent.style.height = this._height + 'px'; - } + () => ({ width: this._width, height: this._height, parent: this._spanRef.current?.parentNode as HTMLElement }), + action(({ width, height, parent }) => { + if (parent) { + parent.style.width = width + 'px'; + parent.style.height = height + 'px'; } }) ); } removeDoc = () => { - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos()))).deleteSelection()); + this._props.view.dispatch(this._props.view.state.tr.setSelection(new NodeSelection(this._props.view.state.doc.resolve(this._props.getPos()))).deleteSelection()); return true; }; @@ -157,7 +160,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current); return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; - outerFocus = (target: Doc, options: DocFocusOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target + outerFocus = (target: Doc, options: FocusViewOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target onKeyDown = (e: any) => { e.stopPropagation(); @@ -167,29 +170,30 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { }; onPointerLeave = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this._props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = ''); }; onPointerEnter = () => { - const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement; + const ele = document.getElementById('DashDocCommentView-' + this._props.docId) as HTMLDivElement; ele && (ele.style.backgroundColor = 'orange'); }; componentWillUnmount = () => Object.values(this._disposers).forEach(disposer => disposer?.()); + isContentActive = () => this._props.tbox._props.isContentActive() || this._props.tbox.isAnyChildContentActive?.(); render() { - return !this._dashDoc || !this._finalLayout || this.props.hidden ? null : ( + return !this._dashDoc || this._props.hidden ? null : ( <div ref={this._spanRef} className="dash-span" style={{ - width: this._width, + width: `calc(100% - ${horizPadding}px)`, height: this._height, - position: 'absolute', - display: 'inline-block', - left: 0, - top: 0, + position: 'relative', + display: 'flex', + margin: 'auto', + pointerEvents: this.isContentActive() ? undefined : 'none', }} onPointerLeave={this.onPointerLeave} onPointerEnter={this.onPointerEnter} @@ -198,27 +202,25 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { onKeyUp={e => e.stopPropagation()} onWheel={e => e.preventDefault()}> <DocumentView - Document={this._finalLayout} + Document={this._dashDoc} addDocument={returnFalse} - rootSelected={returnFalse} //{this._textBox.props.isSelected} removeDocument={this.removeDoc} isDocumentActive={returnFalse} - isContentActive={emptyFunction} - styleProvider={this._textBox.props.styleProvider} - docViewPath={this._textBox.props.docViewPath} + isContentActive={this.isContentActive} + styleProvider={this._textBox._props.styleProvider} + containerViewPath={this._textBox.DocumentView?.().docViewPath} ScreenToLocalTransform={this.getDocTransform} - addDocTab={this._textBox.props.addDocTab} + addDocTab={this._textBox._props.addDocTab} pinToPres={returnFalse} - renderDepth={this._textBox.props.renderDepth + 1} - PanelWidth={this._finalLayout[Width]} - PanelHeight={this._finalLayout[Height]} + renderDepth={this._textBox._props.renderDepth + 1} + PanelWidth={this._dashDoc[Width]} + PanelHeight={this._dashDoc[Height]} focus={this.outerFocus} - whenChildContentsActiveChanged={returnFalse} - bringToFront={emptyFunction} + whenChildContentsActiveChanged={this._props.tbox.whenChildContentsActiveChanged} dontRegisterView={false} - childFilters={this.props.tbox?.props.childFilters} - childFiltersByRanges={this.props.tbox?.props.childFiltersByRanges} - searchFilterDocs={this.props.tbox?.props.searchFilterDocs} + childFilters={this._props.tbox?._props.childFilters} + childFiltersByRanges={this._props.tbox?._props.childFiltersByRanges} + searchFilterDocs={this._props.tbox?._props.searchFilterDocs} /> </div> ); diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index ad315acc8..3426ba1a7 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .dashFieldView { position: relative; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index d5ad128fe..dc9914637 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,23 +1,24 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast } from '../../../../fields/Types'; +import { Cast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { Transform } from '../../../util/Transform'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); -import { Transform } from '../../../util/Transform'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -25,7 +26,7 @@ export class DashFieldView { node: any; tbox: FormattedTextBox; - unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + unclickable = () => !this.tbox._props.isSelected() && 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) { this.node = node; this.tbox = tbox; @@ -70,10 +71,10 @@ export class DashFieldView { } catch {} }); } - @action deselectNode() { + deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } - @action selectNode() { + selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } } @@ -92,25 +93,27 @@ interface IDashFieldViewInternal { } @observer -export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> { +export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldViewInternal> { _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); - @observable _dashDoc: Doc | undefined; + @observable _dashDoc: Doc | undefined = undefined; @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); - this._fieldKey = this.props.fieldKey; - this._textBoxDoc = this.props.tbox.props.Document; + makeObservable(this); + this._fieldKey = this._props.fieldKey; + this._textBoxDoc = this._props.tbox.Document; - if (this.props.docId) { - DocServer.GetRefField(this.props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + if (this._props.docId) { + DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { - this._dashDoc = this.props.tbox.rootDoc; + this._dashDoc = this._props.tbox.Document; } } + componentWillUnmount() { this._reactionDisposer?.(); } @@ -119,18 +122,18 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { return !this._dashDoc ? null : ( - <div onClick={action(e => (this._expanded = !this.props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}> + <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this._props.hideKey ? this._props.tbox._props.PanelWidth() - 20 : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this.props.hideKey ? undefined : this.return100} - columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + maxWidth={this._props.hideKey ? undefined : this.return100} + columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero} selectedCell={() => [this._dashDoc!, 0]} fieldKey={this._fieldKey} rowHeight={returnZero} - isRowActive={() => this._expanded && this.props.editable} + isRowActive={() => this._expanded && this._props.editable} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} @@ -145,9 +148,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } createPivotForField = (e: React.MouseEvent) => { - let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); + let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { - const embedding = Doc.MakeEmbedding(container.rootDoc); + const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders'; let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField)); @@ -157,7 +160,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; - this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight); + this._props.tbox._props.addDocTab(embedding, OpenWhere.addRight); } }; @@ -175,16 +178,17 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna <div className="dashFieldView" style={{ - width: this.props.width, - height: this.props.height, + width: this._props.width, + height: this._props.height, + pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> - {this.props.hideKey ? null : ( + {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> {this._fieldKey} </span> )} - {this.props.fieldKey.startsWith('#') ? null : this.fieldValueContent} + {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} </div> ); } @@ -197,7 +201,7 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { super(props); DashFieldViewMenu.Instance = this; } - @action + showFields = (e: React.MouseEvent) => { DashFieldViewMenu.createFieldView(e); DashFieldViewMenu.Instance.fadeOut(true); diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss new file mode 100644 index 000000000..b0c17e56e --- /dev/null +++ b/src/client/views/nodes/formattedText/EquationEditor.scss @@ -0,0 +1,468 @@ +// using this import, we get runtime errors when trying to load the specified font-faces +// so we copy the .css and remove the @font-face imports + +// @import 'mathquill/build/mathquill.css' +/* + * MathQuill v0.10.1 http://mathquill.com + * by Han, Jeanine, and Mary maintainers@mathquill.com + * + * This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL + * was not distributed with this file, You can obtain + * one at http://mozilla.org/MPL/2.0/. + */ +// @font-face { +// font-family: Symbola; +// src: url(font/Symbola.eot); +// src: +// local('Symbola Regular'), +// local('Symbola'), +// url(font/Symbola.woff2) format('woff2'), +// url(font/Symbola.woff) format('woff'), +// url(font/Symbola.ttf) format('truetype'), +// url(font/Symbola.otf) format('opentype'), +// url(font/Symbola.svg#Symbola) format('svg'); +// } +.mq-editable-field { + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor { + border-left: 1px solid black; + margin-left: -1px; + position: relative; + z-index: 1; + padding: 0; + display: -moz-inline-box; + display: inline-block; +} +.mq-editable-field .mq-cursor.mq-blink { + visibility: hidden; +} +.mq-editable-field, +.mq-math-mode .mq-editable-field { + border: 1px solid gray; +} +.mq-editable-field.mq-focused, +.mq-math-mode .mq-editable-field.mq-focused { + -webkit-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + -moz-box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + box-shadow: + #8bd 0 0 1px 2px, + inset #6ae 0 0 2px 0; + border-color: #709ac0; + border-radius: 1px; +} +.mq-math-mode .mq-editable-field { + margin: 1px; +} +.mq-editable-field .mq-latex-command-input { + color: inherit; + font-family: 'Courier New', monospace; + border: 1px solid gray; + padding-right: 1px; + margin-right: 1px; + margin-left: 2px; +} +.mq-editable-field .mq-latex-command-input.mq-empty { + background: transparent; +} +.mq-editable-field .mq-latex-command-input.mq-hasCursor { + border-color: ActiveBorder; +} +.mq-editable-field.mq-empty:after, +.mq-editable-field.mq-text-mode:after, +.mq-math-mode .mq-empty:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-cursor:only-child:after, +.mq-editable-field .mq-textarea + .mq-cursor:last-child:after { + visibility: hidden; + content: 'c'; +} +.mq-editable-field .mq-text-mode .mq-cursor:only-child:after { + content: ''; +} +.mq-editable-field.mq-text-mode { + overflow-x: auto; + overflow-y: hidden; +} +.mq-root-block, +.mq-math-mode .mq-root-block { + display: -moz-inline-box; + display: inline-block; + width: 100%; + padding: 2px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + vertical-align: middle; +} +.mq-math-mode { + font-variant: normal; + font-weight: normal; + font-style: normal; + font-size: 115%; + line-height: 1; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-non-leaf, +.mq-math-mode .mq-scaled { + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode var, +.mq-math-mode .mq-text-mode, +.mq-math-mode .mq-nonSymbola { + font-family: 'Times New Roman', Symbola, serif; + line-height: 0.9; +} +.mq-math-mode * { + font-size: inherit; + line-height: inherit; + margin: 0; + padding: 0; + border-color: black; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + box-sizing: border-box; +} +.mq-math-mode .mq-empty { + background: #ccc; +} +.mq-math-mode .mq-empty.mq-root-block { + background: transparent; +} +.mq-math-mode.mq-empty { + background: transparent; +} +.mq-math-mode .mq-text-mode { + display: inline-block; +} +.mq-math-mode .mq-text-mode.mq-hasCursor { + box-shadow: inset darkgray 0 0.1em 0.2em; + padding: 0 0.1em; + margin: 0 -0.1em; + min-width: 1ex; +} +.mq-math-mode .mq-font { + font: + 1em 'Times New Roman', + Symbola, + serif; +} +.mq-math-mode .mq-font * { + font-family: inherit; + font-style: inherit; +} +.mq-math-mode b, +.mq-math-mode b.mq-font { + font-weight: bolder; +} +.mq-math-mode var, +.mq-math-mode i, +.mq-math-mode i.mq-font { + font-style: italic; +} +.mq-math-mode var.mq-f { + margin-right: 0.2em; + margin-left: 0.1em; +} +.mq-math-mode .mq-roman var.mq-f { + margin: 0; +} +.mq-math-mode big { + font-size: 200%; +} +.mq-math-mode .mq-int > big { + display: inline-block; + -webkit-transform: scaleX(0.7); + -moz-transform: scaleX(0.7); + -ms-transform: scaleX(0.7); + -o-transform: scaleX(0.7); + transform: scaleX(0.7); + vertical-align: -0.16em; +} +.mq-math-mode .mq-int > .mq-supsub { + font-size: 80%; + vertical-align: -1.1em; + padding-right: 0.2em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sup > .mq-sup-inner { + vertical-align: 1.3em; +} +.mq-math-mode .mq-int > .mq-supsub > .mq-sub { + margin-left: -0.35em; +} +.mq-math-mode .mq-roman { + font-style: normal; +} +.mq-math-mode .mq-sans-serif { + font-family: sans-serif, Symbola, serif; +} +.mq-math-mode .mq-monospace { + font-family: monospace, Symbola, serif; +} +.mq-math-mode .mq-overline { + border-top: 1px solid black; + margin-top: 1px; +} +.mq-math-mode .mq-underline { + border-bottom: 1px solid black; + margin-bottom: 1px; +} +.mq-math-mode .mq-binary-operator { + padding: 0 0.2em; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-supsub { + text-align: left; + font-size: 90%; + vertical-align: -0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only { + vertical-align: 0.5em; +} +.mq-math-mode .mq-supsub.mq-sup-only .mq-sup { + display: inline-block; + vertical-align: text-bottom; +} +.mq-math-mode .mq-supsub .mq-sup { + display: block; +} +.mq-math-mode .mq-supsub .mq-sub { + display: block; + float: left; +} +.mq-math-mode .mq-supsub .mq-binary-operator { + padding: 0 0.1em; +} +.mq-math-mode .mq-supsub .mq-fraction { + font-size: 70%; +} +.mq-math-mode sup.mq-nthroot { + font-size: 80%; + vertical-align: 0.8em; + margin-right: -0.6em; + margin-left: 0.2em; + min-width: 0.5em; +} +.mq-math-mode .mq-paren { + padding: 0 0.1em; + vertical-align: top; + -webkit-transform-origin: center 0.06em; + -moz-transform-origin: center 0.06em; + -ms-transform-origin: center 0.06em; + -o-transform-origin: center 0.06em; + transform-origin: center 0.06em; +} +.mq-math-mode .mq-paren.mq-ghost { + color: silver; +} +.mq-math-mode .mq-paren + span { + margin-top: 0.1em; + margin-bottom: 0.1em; +} +.mq-math-mode .mq-array { + vertical-align: middle; + text-align: center; +} +.mq-math-mode .mq-array > span { + display: block; +} +.mq-math-mode .mq-operator-name { + font-family: Symbola, 'Times New Roman', serif; + line-height: 0.9; + font-style: normal; +} +.mq-math-mode var.mq-operator-name.mq-first { + padding-left: 0.2em; +} +.mq-math-mode var.mq-operator-name.mq-last, +.mq-math-mode .mq-supsub.mq-after-operator-name { + padding-right: 0.2em; +} +.mq-math-mode .mq-fraction { + font-size: 90%; + text-align: center; + vertical-align: -0.4em; + padding: 0 0.2em; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link { + display: -moz-groupbox; +} +.mq-math-mode .mq-fraction, +.mq-math-mode .mq-large-operator, +.mq-math-mode x:-moz-any-link, +.mq-math-mode x:default { + display: inline-block; +} +.mq-math-mode .mq-numerator, +.mq-math-mode .mq-denominator { + display: block; +} +.mq-math-mode .mq-numerator { + padding: 0 0.1em; +} +.mq-math-mode .mq-denominator { + border-top: 1px solid; + float: right; + width: 100%; + padding: 0.1em; +} +.mq-math-mode .mq-sqrt-prefix { + padding-top: 0; + position: relative; + top: 0.1em; + vertical-align: top; + -webkit-transform-origin: top; + -moz-transform-origin: top; + -ms-transform-origin: top; + -o-transform-origin: top; + transform-origin: top; +} +.mq-math-mode .mq-sqrt-stem { + border-top: 1px solid; + margin-top: 1px; + padding-left: 0.15em; + padding-right: 0.2em; + margin-right: 0.1em; + padding-top: 1px; +} +.mq-math-mode .mq-vector-prefix { + display: block; + text-align: center; + line-height: 0.25em; + margin-bottom: -0.1em; + font-size: 0.75em; +} +.mq-math-mode .mq-vector-stem { + display: block; +} +.mq-math-mode .mq-large-operator { + vertical-align: -0.2em; + padding: 0.2em; + text-align: center; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator big, +.mq-math-mode .mq-large-operator .mq-to { + display: block; +} +.mq-math-mode .mq-large-operator .mq-from, +.mq-math-mode .mq-large-operator .mq-to { + font-size: 80%; +} +.mq-math-mode .mq-large-operator .mq-from { + float: right; + /* take out of normal flow to manipulate baseline */ + width: 100%; +} +.mq-math-mode, +.mq-math-mode .mq-editable-field { + cursor: text; + font-family: Symbola, 'Times New Roman', serif; +} +.mq-math-mode .mq-overarrow { + border-top: 1px solid black; + margin-top: 1px; + padding-top: 0.2em; +} +.mq-math-mode .mq-overarrow:before { + display: block; + position: relative; + top: -0.34em; + font-size: 0.5em; + line-height: 0em; + content: '\27A4'; + text-align: right; +} +.mq-math-mode .mq-overarrow.mq-arrow-left:before { + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: 'FlipH'; +} +.mq-math-mode .mq-selection, +.mq-editable-field .mq-selection, +.mq-math-mode .mq-selection .mq-non-leaf, +.mq-editable-field .mq-selection .mq-non-leaf, +.mq-math-mode .mq-selection .mq-scaled, +.mq-editable-field .mq-selection .mq-scaled { + background: #b4d5fe !important; + background: Highlight !important; + color: HighlightText; + border-color: HighlightText; +} +.mq-math-mode .mq-selection .mq-matrixed, +.mq-editable-field .mq-selection .mq-matrixed { + background: #39f !important; +} +.mq-math-mode .mq-selection .mq-matrixed-container, +.mq-editable-field .mq-selection .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; +} +.mq-math-mode .mq-selection.mq-blur, +.mq-editable-field .mq-selection.mq-blur, +.mq-math-mode .mq-selection.mq-blur .mq-non-leaf, +.mq-editable-field .mq-selection.mq-blur .mq-non-leaf, +.mq-math-mode .mq-selection.mq-blur .mq-scaled, +.mq-editable-field .mq-selection.mq-blur .mq-scaled, +.mq-math-mode .mq-selection.mq-blur .mq-matrixed, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed { + background: #d4d4d4 !important; + color: black; + border-color: black; +} +.mq-math-mode .mq-selection.mq-blur .mq-matrixed-container, +.mq-editable-field .mq-selection.mq-blur .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; +} +.mq-editable-field .mq-textarea, +.mq-math-mode .mq-textarea { + position: relative; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; +} +.mq-editable-field .mq-textarea *, +.mq-math-mode .mq-textarea *, +.mq-editable-field .mq-selectable, +.mq-math-mode .mq-selectable { + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + position: absolute; + clip: rect(1em 1em 1em 1em); + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + -o-transform: scale(0); + transform: scale(0); + resize: none; + width: 1px; + height: 1px; +} +.mq-math-mode .mq-matrixed { + background: white; + display: -moz-inline-box; + display: inline-block; +} +.mq-math-mode .mq-matrixed-container { + filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); + margin-top: -0.1em; +} diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx new file mode 100644 index 000000000..b4102e08e --- /dev/null +++ b/src/client/views/nodes/formattedText/EquationEditor.tsx @@ -0,0 +1,87 @@ +import React, { Component, createRef } from 'react'; + +// Import JQuery, required for the functioning of the equation editor +import $ from 'jquery'; + +import './EquationEditor.scss'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +window.jQuery = $; + +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore +require('mathquill/build/mathquill'); + +(window as any).MathQuill = (window as any).MathQuill.getInterface(1); + +type EquationEditorProps = { + onChange(latex: string): void; + value: string; + spaceBehavesLikeTab?: boolean; + autoCommands: string; + autoOperatorNames: string; + onEnter?(): void; +}; + +/** + * @typedef {EquationEditorProps} props + * @prop {Function} onChange Triggered when content of the equation editor changes + * @prop {string} value Content of the equation handler + * @prop {boolean}[false] spaceBehavesLikeTab Whether spacebar should simulate tab behavior + * @prop {string} autoCommands List of commands for which you only have to type the name of the + * command with a \ in front of it. Examples: pi theta rho sum + * @prop {string} autoOperatorNames List of operators for which you only have to type the name of the + * operator with a \ in front of it. Examples: sin cos tan + * @prop {Function} onEnter Triggered when enter is pressed in the equation editor + * @extends {Component<EquationEditorProps>} + */ +class EquationEditor extends Component<EquationEditorProps> { + element: 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) { + super(props); + + this.element = createRef(); + this.mathField = null; + + // MathJax apparently fire 2 edit events on startup. + this.ignoreEditEvents = 2; + } + + componentDidMount() { + const { onChange, value, spaceBehavesLikeTab, autoCommands, autoOperatorNames, onEnter } = this.props; + + const config = { + handlers: { + edit: () => { + if (this.ignoreEditEvents > 0) { + this.ignoreEditEvents -= 1; + return; + } + if (this.mathField.latex() !== value) { + onChange(this.mathField.latex()); + } + }, + enter: onEnter, + }, + spaceBehavesLikeTab, + autoCommands, + autoOperatorNames, + }; + + this.mathField = (window as any).MathQuill.MathField(this.element.current, config); + this.mathField.latex(value || ''); + } + + render() { + return <span ref={this.element} style={{ border: '0px', boxShadow: 'None' }} />; + } +} + +export default EquationEditor; diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 5e62d94c2..b786c5ffb 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,13 +1,13 @@ -import EquationEditor from 'equation-editor-react'; -import { IReactionDisposer, trace } from 'mobx'; +import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { TextSelection } from 'prosemirror-state'; +import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; +import EquationEditor from './EquationEditor'; import { FormattedTextBox } from './FormattedTextBox'; -import React = require('react'); export class EquationView { dom: HTMLDivElement; // container for label and value @@ -63,10 +63,10 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> _fieldKey: string; _ref: React.RefObject<EquationEditor> = React.createRef(); - constructor(props: IEquationViewInternal) { + constructor(props: any) { super(props); - this._fieldKey = this.props.fieldKey; - this._textBoxDoc = this.props.tbox.props.Document; + this._fieldKey = props.fieldKey; + this._textBoxDoc = props.tbox.Document; } componentWillUnmount() { @@ -101,7 +101,7 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> <EquationEditor ref={this._ref} value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')} - onChange={str => (this._textBoxDoc[this._fieldKey] = str)} + onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" spaceBehavesLikeTab={true} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 818c0cbe7..03ff0436b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables'; +@import '../../global/globalCssVariables.module.scss'; .ProseMirror { width: 100%; @@ -49,6 +49,7 @@ audiotag:hover { transition: opacity 1s; width: 100%; position: relative; + transform-origin: left top; top: 0; left: 0; } @@ -89,6 +90,7 @@ audiotag:hover { bottom: 0; width: 11; height: 11; + cursor: default; } .formattedTextBox-outer { @@ -590,7 +592,7 @@ footnote::before { } @media only screen and (max-width: 1000px) { - @import '../../global/globalCssVariables'; + @import '../../global/globalCssVariables.module.scss'; .ProseMirror { width: 100%; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 563b6a25d..f9cef1a60 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,8 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -11,21 +10,20 @@ import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; -import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; -import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -44,17 +42,17 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; -import { ViewBoxAnnotatableComponent } from '../../DocComponent'; -import { DocumentButtonBar } from '../../DocumentButtonBar'; +import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; -import { FieldView, FieldViewProps } from '../FieldView'; -import { LinkDocPreview } from '../LinkDocPreview'; +import { media_state } from '../AudioBox'; +import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; +import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; +import { LinkInfo } from '../LinkDocPreview'; import { PinProps, PresBox } from '../trails'; import { DashDocCommentView } from './DashDocCommentView'; import { DashDocView } from './DashDocView'; @@ -69,19 +67,9 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; -import applyDevTools = require('prosemirror-dev-tools'); -import React = require('react'); -import { media_state } from '../AudioBox'; -import { setCORS } from 'google-translate-api-browser'; -import { isDarkMode } from '../../../util/reportManager/reportManagerUtils'; -// setting up cors-anywhere server address -const translate = setCORS('http://cors-anywhere.herokuapp.com/'); -export const GoogleRef = 'googleDocId'; -type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; - -export interface FormattedTextBoxProps {} +// import * as applyDevTools from 'prosemirror-dev-tools'; @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -94,12 +82,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); static _hadSelection: boolean = false; + private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; public _applyingChange: string = ''; + private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; private _lastTimedMark: Mark | undefined = undefined; @@ -110,6 +100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _recordingStart: number = 0; private _ignoreScroll = false; private _lastText = ''; + private _hadDownFocus = false; private _focusSpeed: Opt<number>; private _keymap: any = undefined; private _rules: RichTextRules | undefined; @@ -117,6 +108,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _forceDownNode: Node | undefined; private _downX = 0; private _downY = 0; + private _downTime = 0; private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { @@ -130,7 +122,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get noSidebar() { - return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_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%'); @@ -139,19 +131,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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; + return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } @computed get textHeight() { - return NumCast(this.rootDoc[this.fieldKey + '_height']); + return NumCast(this.dataDoc[this.fieldKey + '_height']); } @computed get scrollHeight() { - return NumCast(this.rootDoc[this.fieldKey + '_scrollHeight']); + return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } @computed get sidebarHeight() { - return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '_height']); + return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); } @computed get titleHeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; } @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); @@ -163,8 +155,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { - this._keymap = buildKeymap(schema, this.props); - this._rules = new RichTextRules(this.rootDoc, this); + this._keymap = buildKeymap(schema, this._props); + this._rules = new RichTextRules(this.Document, this); return { schema, plugins: [ @@ -188,7 +180,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private gptRes: string = ''; public static PasteOnLoad: ClipboardEvent | undefined; - public static SelectOnLoad = ''; + private static SelectOnLoad: Doc | undefined; + public static SetSelectOnLoad(doc: Doc) { + FormattedTextBox.SelectOnLoad = doc; + } public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; public static IsFragment(html: string) { @@ -206,8 +201,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); + makeObservable(this); FormattedTextBox.Instance = this; this._recordingStart = Date.now(); } @@ -247,13 +243,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc }); + if (!pinProps && this._editorView?.state.selection.empty) return this.Document; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); this._finishingLink = false; - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); return anchor; }; @@ -274,14 +270,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (target) { anchor.followLinkAudio = true; let stopFunc: any; - Doc.GetProto(target).mediaState = media_state.Recording; - Doc.GetProto(target).audioAnnoState = 'recording'; - DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + const targetData = target[DocData]; + targetData.mediaState = media_state.Recording; + targetData.audioAnnoState = 'recording'; + DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); let reactionDisposer = reaction( () => target.mediaState, action(dictation => { if (!dictation) { - Doc.GetProto(target).audioAnnoState = 'stopped'; + targetData.audioAnnoState = 'stopped'; stopFunc(); reactionDisposer(); } @@ -308,15 +305,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn); - FormattedTextBox.SelectOnLoad = target[Id]; + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn); + FormattedTextBox.SetSelectOnLoad(target); return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); - this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); + let ele: Opt<HTMLDivElement> = undefined; + try { + const contents = window.getSelection()?.getRangeAt(0).cloneContents(); + if (contents) { + ele = document.createElement('div'); + ele.append(contents); + } + this._selectionHTML = ele?.innerHTML; + } catch (e) {} }; dispatchTransaction = (tx: Transaction) => { @@ -328,7 +334,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box - const templateData = this.rootDoc !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template + const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); @@ -350,11 +356,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited - textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); + textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); unchanged = false; } } else { @@ -362,7 +368,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps dataDoc[this.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have - ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } this._applyingChange = ''; @@ -379,7 +385,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.updateState(EditorState.fromJSON(this.config, json)); } } - if (window.getSelection()?.isCollapsed && this.props.isSelected()) { + if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) { AnchorMenu.Instance.fadeOut(true); } } @@ -427,7 +433,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; @@ -435,22 +441,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); - DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); + Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); - // this.prepareForTyping(); } - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); }; updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( - !this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing + !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing (title.startsWith('-') || title.startsWith('@')) && this._editorView && !this.dataDoc.title_custom && - (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text') + (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; @@ -461,7 +466,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); if (str.startsWith('@') && str.length > 1) { - Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc); + Doc.AddToMyPublished(this.Document); } } } @@ -470,7 +475,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@' hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { const editorView = this._editorView; - if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) { + if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { @@ -481,16 +486,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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 ?? - (LinkManager.Links(this.rootDoc).find( + (LinkManager.Links(this.Document).find( link => - Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && // + Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.Document) && // Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) ) || - DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!); + DocUtils.MakeLink(this.Document, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); - const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; + // DocCast(alink.link_anchor_1).followLinkLocation = 'add:right'; + const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); - const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' }); + const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term' }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); @@ -557,7 +563,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @undoBatch - @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); @@ -567,45 +572,51 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (dragData) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); - let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl); - const draggedDoc = dragData.draggedDocuments.lastElement(); - if (added) { + const draggedDoc = dragData.droppedDocuments.lastElement(); + let added: Opt<boolean>; + const dropAction = dragData.dropAction || dragData.userDropAction; + if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) { // replace text contents when dragging with Alt if (de.altKey) { const fieldKey = Doc.LayoutFieldKey(draggedDoc); - if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } // embed document when drag marked as embed - } else if (de.embedKey) { + } else if (de.embedKey || dropAction) { const node = schema.nodes.dashDoc.create({ - width: draggedDoc[Width](), - height: draggedDoc[Height](), + width: NumCast(draggedDoc._width), + height: NumCast(draggedDoc._height), title: 'dashDoc', docId: draggedDoc[Id], float: 'unset', }); - if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { + if (!['embed', 'copy'].includes((dropAction ?? '') as any)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; + } else { + added = true; } if (added) { draggedDoc._freeform_fitContentsToBox = true; - Doc.SetContainer(draggedDoc, this.rootDoc); + Doc.SetContainer(draggedDoc, this.Document); const view = this._editorView!; try { + this._inDrop = true; const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; pos && view.dispatch(view.state.tr.insert(pos, node)); added = pos ? true : false; // pos will be null if you don't drop onto an actual text location } catch (e) { console.log('Drop failed', e); added = false; + } finally { + this._inDrop = false; } } } } // otherwise, fall through to outer collection to handle drop - !added && e.preventDefault(); - e.stopPropagation(); + added === false && e.preventDefault(); + added === true && e.stopPropagation(); return added; } return false; @@ -705,14 +716,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action toggleSidebar = (preview: boolean = false) => { + const defaultSidebar = 250; const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; else { this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; - this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + this.layoutDoc._layout_showSidebar = + (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; } - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); + this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); @@ -729,12 +742,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const localDelta = this.props + const localDelta = this._props .ScreenToLocalTransform() - .scale(this.props.NativeDimScaling?.() || 1) + .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; - const width = this.layoutDoc[Width]() + localDelta[0]; + const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; @@ -745,15 +758,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); - // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); - // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); + // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]); + // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); - this.props.select(false); + this._props.select(false); setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch - pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {}); + pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); @@ -763,7 +776,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; - this.props.addDocTab(trail, OpenWhere.replaceRight); + this._props.addDocTab(trail, OpenWhere.replaceRight); } }; @@ -781,7 +794,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ?.trim() .split(' ') .filter(h => h); - const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0]; + const anchorDoc = Array.from(hrefs ?? []) + .lastElement() + .replace(Doc.localServerPath(), '') + .split('?')[0]; const deleteMarkups = undoBatch(() => { const sel = editor.state.selection; editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); @@ -810,7 +826,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps changeItems.push({ description: 'plain', event: undoBatch(() => { - Doc.setNativeView(this.rootDoc); + Doc.setNativeView(this.Document); this.layoutDoc.layout_autoHeightMargins = undefined; }), icon: 'eye', @@ -819,8 +835,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'metadata', event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; - this.rootDoc.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._layout_autoHeightMargins = 50), 50); + this.Document.layout_fieldKey = 'layout_meta'; + setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50); }), icon: 'eye', }); @@ -831,8 +847,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: StrCast(note.title), event: undoBatch(() => { this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.rootDoc); - DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + Doc.setNativeView(this.Document); + DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); }), icon: icon, }); @@ -854,69 +870,50 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); + const appearance = cm.findByDescription('Appearance...'); + const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; - const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ + appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); - uicontrols.push({ + appearanceItems.push({ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); - !Doc.noviceMode && uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); + !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && - uicontrols.push({ + appearanceItems.push({ description: 'Broadcast Message', - event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), + event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text)), icon: 'expand-arrows-alt', }); - cm.addItem({ description: 'UI Controls...', subitems: uicontrols, icon: 'asterisk' }); - const appearance = cm.findByDescription('Appearance...'); - const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); - // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); !Doc.noviceMode && appearanceItems.push({ description: 'Make Default Layout', event: () => { if (!this.layoutDoc.isTemplateDoc) { - const title = StrCast(this.rootDoc.title); - this.rootDoc.title = 'text'; - MakeTemplate(this.rootDoc, true, title); - } else if (!this.rootDoc.isTemplateDoc) { - const title = StrCast(this.rootDoc.title); - this.rootDoc.title = 'text'; - this.rootDoc.layout = this.layoutDoc.layout as string; - this.rootDoc.title = this.layoutDoc.isTemplateForField as string; - this.rootDoc.isTemplateDoc = false; - this.rootDoc.isTemplateForField = ''; - this.rootDoc.layout_fieldKey = 'layout'; - MakeTemplate(this.rootDoc, true, title); - setTimeout(() => { - this.rootDoc._layout_autoHeight = this.layoutDoc._layout_autoHeight; // layout_autoHeight, width and height - this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template - this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields - this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, 'string', null); - this.rootDoc.backgroundColor = Cast(this.layoutDoc.backgroundColor, 'string', null); - }, 10); + const title = StrCast(this.Document.title); + this.Document.title = 'text'; + MakeTemplate(this.Document, true, title); } - Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.rootDoc); + Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); + Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); }, icon: 'eye', }); - cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); + !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); - this.props.renderDepth && + this._props.renderDepth && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), @@ -959,8 +956,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps generateImage = async () => { GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); - GPTPopup.Instance?.setImgTargetDoc(this.rootDoc); - GPTPopup.Instance.addToCollection = this.props.addDocument; + GPTPopup.Instance?.setImgTargetDoc(this.Document); + GPTPopup.Instance.addToCollection = this._props.addDocument; GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text); GPTPopup.Instance.generateImage(); }; @@ -1000,7 +997,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { - Doc.GetProto(link).isDictation = true; + link[DocData].isDictation = true; const audioanchor = Cast(link.link_anchor_2, Doc, null); const textanchor = Cast(link.link_anchor_1, Doc, null); if (audioanchor) { @@ -1010,7 +1007,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps audioId: audioanchor[Id], textId: textanchor[Id], }); - Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode; + textanchor[DocData].title = 'dictation:' + audiotag.attrs.timeCode; const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); @@ -1055,22 +1052,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; + anchor.text_html = this._selectionHTML ?? selectedText; anchor.title = selectedText.substring(0, 30); + anchor.presentation_zoomText = true; return anchor; } - return anchorDoc ?? this.rootDoc; + return anchorDoc ?? this.Document; } - return anchorDoc ?? this.rootDoc; + return anchorDoc ?? this.Document; } - getView = async (doc: Doc) => { - if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - !this.SidebarShown && this.toggleSidebar(false); + 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 (!this.SidebarShown) { + this.toggleSidebar(false); + options.didMove = true; + } setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; - focus = (textAnchor: Doc, options: DocFocusOptions) => { + focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; const textAnchorId = textAnchor[Id]; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { @@ -1128,7 +1130,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; } else { - return this.props.focus(this.rootDoc, options); + return this._props.focus(this.Document, options); } } }; @@ -1137,12 +1139,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = (scrollHeight: number) => { const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.rootDoc[this.fieldKey + '_height'] = scrollHeight; + this.dataDoc[this.fieldKey + '_height'] = scrollHeight; if (nh) this.layoutDoc._nativeHeight = scrollHeight; }; @computed get contentScaling() { - return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.NativeDimScaling?.() || 1 : 1; + return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1; } @action @@ -1164,7 +1166,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } componentDidMount() { - !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( @@ -1177,7 +1179,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps { fireImmediately: true } ); this._disposers.width = reaction( - () => this.props.PanelWidth(), + () => this._props.PanelWidth(), width => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( @@ -1195,13 +1197,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( - (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this.props.isSelected()) && // + (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // layout_autoHeight && newHeight && - newHeight !== this.rootDoc.height && - !this.props.dontRegisterView + newHeight !== this.layoutDoc.height && + !this._props.dontRegisterView ) { - this.props.setHeight?.(newHeight); + this._props.setHeight?.(newHeight); } }, { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } @@ -1213,20 +1215,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._cachedLinks = newLinks; } ); - this._disposers.buttonBar = reaction( - () => DocumentButtonBar.Instance, - instance => { - if (instance) { - this.pullFromGoogleDoc(this.checkState); - this.dataDoc[GoogleRef] && this.dataDoc.googleDocUnchanged && runInAction(() => (instance.isAnimatingFetch = true)); - } - } - ); this._disposers.editorState = reaction( () => { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey])) }; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1242,50 +1235,32 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } ); - this._disposers.pullDoc = reaction( - () => this.props.Document[Pulls], - () => { - if (!DocumentButtonBar.hasPulledHack) { - DocumentButtonBar.hasPulledHack = true; - this.pullFromGoogleDoc(this.dataDoc.googleDocUnchanged ? this.checkState : this.updateState); - } - } - ); - this._disposers.pushDoc = reaction( - () => this.props.Document[Pushes], - () => { - if (!DocumentButtonBar.hasPushedHack) { - DocumentButtonBar.hasPushedHack = true; - this.pushToGoogleDoc(); - } - } - ); this._disposers.search = reaction( - () => Doc.IsSearchMatch(this.rootDoc), + () => Doc.IsSearchMatch(this.Document), search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()), - { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false } + { fireImmediately: Doc.IsSearchMatchUnmemoized(this.Document) ? true : false } ); this._disposers.selected = reaction( - () => this.props.isSelected(), + () => this._props.rootSelected?.(), action(selected => { //selected && setTimeout(() => this.prepareForTyping()); if (FormattedTextBox._globalHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { - RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } if (this._editorView && selected) { - RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); setTimeout(this.autoLink, 20); } }), { fireImmediately: true } ); - if (!this.props.dontRegisterView) { + if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this._recordingDictation, () => { @@ -1300,7 +1275,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { - if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) { + if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) { const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); @@ -1319,80 +1294,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(this.tryUpdateScrollHeight, 250); } - pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - const modes = GoogleApiClientUtils.Docs.WriteMode; - let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], 'string'); - if (!reference) { - mode = modes.Insert; - reference = { title: StrCast(this.dataDoc.title) }; - } - const redo = async () => { - if (this._editorView && reference) { - const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); - const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); - response && (this.dataDoc[GoogleRef] = response.documentId); - const pushSuccess = response !== undefined && !('errors' in response); - dataDoc.googleDocUnchanged = pushSuccess; - DocumentButtonBar.Instance.startPushOutcome(pushSuccess); - } - }; - const undo = () => { - if (exportState && reference) { - const content: GoogleApiClientUtils.Docs.Content = { - text: exportState.text, - requests: [], - }; - GoogleApiClientUtils.Docs.write({ reference, content, mode }); - } - }; - UndoManager.AddEvent({ undo, redo, prop: '' }); - redo(); - }); - }; - - pullFromGoogleDoc = async (handler: PullHandler) => { - const dataDoc = this.dataDoc; - const documentId = StrCast(dataDoc[GoogleRef]); - let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; - if (documentId) { - exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); - } - exportState && UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); - }; - - updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - let pullSuccess = false; - if (exportState !== undefined) { - pullSuccess = true; - dataDoc[this.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON())); - setTimeout(() => { - if (this._editorView) { - const state = this._editorView.state; - const end = state.doc.content.size - 1; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); - } - }, 0); - dataDoc.title = exportState.title; - this.dataDoc.title_custom = true; - dataDoc.googleDocUnchanged = true; - } else { - delete dataDoc[GoogleRef]; - } - DocumentButtonBar.Instance.startPullOutcome(pullSuccess); - }; - - checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - if (exportState && this._editorView) { - const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc); - const equalTitles = dataDoc.title === exportState.title; - const unchanged = equalContent && equalTitles; - dataDoc.googleDocUnchanged = unchanged; - DocumentButtonBar.Instance.setPullState(unchanged); - } - }; - clipboardTextSerializer = (slice: Slice): string => { let text = '', separated = true; @@ -1430,8 +1331,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const dashField = view.state.schema.nodes.paragraph.create({}, [ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ - allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], - location: 'add:right', + allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }], title: `from: ${DocCast(pdfAnchor.embedContainer).title}`, noPreview: true, docref: false, @@ -1441,7 +1341,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ]), ]); - const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { link_relationship: 'PDF pasted' }); + const link = DocUtils.MakeLink(pdfAnchor, this.Document, { link_relationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } @@ -1464,8 +1364,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const self = this; return new Plugin({ view(newView) { - runInAction(() => self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); - return new RichTextMenuPlugin({ editorProps: this.props }); + runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); + return new RichTextMenuPlugin({ editorProps: this._props }); }, }); } @@ -1487,7 +1387,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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.props.ScreenToLocalTransform().Scale; + const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); } else { @@ -1539,11 +1439,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps (this._editorView as any).TextView = this; } - const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); - if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); + if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const selLoadChar = FormattedTextBox.SelectOnLoadChar; - FormattedTextBox.SelectOnLoad = ''; - this.props.select(false); + FormattedTextBox.SelectOnLoad = undefined; + this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); @@ -1559,7 +1459,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } selectOnLoad && this._editorView!.focus(); - if (this.props.isContentActive()) this.prepareForTyping(); + if (this._props.isContentActive()) this.prepareForTyping(); if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; @@ -1582,15 +1482,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], ]; this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks)); }; - @action componentWillUnmount() { if (this._recordingDictation) { this._recordingDictation = !this._recordingDictation; @@ -1601,7 +1500,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.LiveTextUndo = undefined; this.unhighlightSearchTerms(); this._editorView?.destroy(); - RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none'); } @@ -1623,7 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); if (!docView) { - this.props.addDocTab(audiodoc, OpenWhere.addBottom); + this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; @@ -1636,8 +1535,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } this._downX = e.clientX; this._downY = e.clientY; + this._downTime = Date.now(); + this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; FormattedTextBoxComment.textBox = this; - if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // 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 @@ -1654,9 +1555,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; + if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime) && !this._hadDownFocus) { + (this.ProseRef?.children[0] as HTMLElement)?.blur?.(); + } if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - else if (this.props.isContentActive(true)) { + else if (this._props.isContentActive()) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let xpos = pcords?.pos || 0; while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { @@ -1666,15 +1570,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); - if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) { - return; - } } }; @action onDoubleClick = (e: React.MouseEvent): void => { FormattedTextBoxComment.textBox = this; - if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar e.stopPropagation(); // if the text box is selected, then it consumes all click events @@ -1685,7 +1586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } FormattedTextBoxComment.Hide(); - if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { + if (e.buttons === 1 && this._props.rootSelected?.() && !e.altKey) { e.stopPropagation(); } }; @@ -1697,12 +1598,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @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.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 (!this._props.isContentActive()) return; if ((e.nativeEvent as any).handledByInnerReactInstance) { e.stopPropagation(); return; @@ -1731,7 +1632,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); } } - if (this.props.isSelected(true)) { + if (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, this._forceDownNode, e.shiftKey); @@ -1745,7 +1646,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); let olistPos = clickPos?.pos; - if (clickPos && olistPos && this.props.isSelected(true)) { + if (clickPos && olistPos && this._props.rootSelected?.()) { const clickNode = this._editorView?.state.doc.nodeAt(olistPos); const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; @@ -1773,7 +1674,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } startUndoTypingBatch() { - !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title)); + !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.Document.title)); } public endUndoTypingBatch() { this._undoTyping?.end(); @@ -1794,8 +1695,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tr && this._editorView.dispatch(tr); } } - if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { - RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); + if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) { + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; this.endUndoTypingBatch(); @@ -1805,29 +1706,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView!.state; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { - try { - translate(curText, { from: 'en', to: 'es' }).then((result1: any) => { - setTimeout( - () => - translate(result1.text, { from: 'es', to: 'en' }).then((result: any) => { - const tb = this._sidebarTagRef.current as FormattedTextBox; - tb._editorView?.dispatch(tb._editorView!.state.tr.insertText(result1.text + '\r\n\r\n' + result.text)); - }), - 1000 - ); - }); - } catch (e: any) { - console.log(e.message); - } - this._lastText = curText; - } - if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc.title_custom) { + if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) { UndoManager.RunInBatch(() => { this.dataDoc.title_custom = true; this.dataDoc.layout_showTitle = 'title'; const tr = this._editorView!.state.tr; - this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection()); + this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection()); }, 'titler'); } }; @@ -1836,7 +1720,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); e.stopPropagation(); - this.props.setTitleFocus?.(); + this._props.setTitleFocus?.(); return; } const state = this._editorView!.state; @@ -1853,7 +1737,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let stopPropagation = true; for (var i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); - if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } @@ -1862,7 +1746,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); - RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); + RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); @@ -1876,7 +1760,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': if (e.code !== 'Space') { - [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.rootDoc)) && + [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) && this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } break; @@ -1889,8 +1773,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { - if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { - if (!this.props.dontSelectOnLoad) { + if (!LinkInfo.Instance?.LinkInfo && this._scrollRef.current) { + if (!this._props.dontSelectOnLoad) { this._ignoreScroll = true; this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; @@ -1900,21 +1784,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }; tryUpdateScrollHeight = () => { - const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children && !SnappingManager.GetIsDragging()) { + if (children && !SnappingManager.IsDragging) { const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); const toHgt = (node: Element) => { const { height, marginTop, marginBottom } = getComputedStyle(node); return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight); - if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { + const scrollHeight = this.ProseRef && proseHeight; + if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); - if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { + if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... @@ -1922,21 +1806,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + 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) => { 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); - setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '_height'] = height); - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + setSidebarHeight = (height: number) => (this.dataDoc[this.SidebarKey + '_height'] = height); + sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => - this.props + this._props .ScreenToLocalTransform() - .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) + .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); @computed get audioHandle() { return !this._recordingDictation ? null : ( @@ -1959,9 +1843,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps TraceMobx(); 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.rootDoc, 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' : '')); - return !annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging()) ? null : ( + return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : ( <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} @@ -1976,12 +1860,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get sidebarCollection() { const renderComponent = (tag: string) => { - const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} - {...this.props} - rootDoc={this.rootDoc} + {...this._props} + Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} usePanelWidth={true} @@ -1997,14 +1881,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag - {...this.props} + {...this._props} ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - PanelHeight={this.props.PanelHeight} + PanelHeight={this._props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} @@ -2018,7 +1902,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} + renderDepth={this._props.renderDepth + 1} setHeight={this.setSidebarHeight} fitContentsToBox={this.fitContentsToBox} noSidebar={true} @@ -2036,12 +1920,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } cycleAlternateText = () => { if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; - this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; } }; @computed get overlayAlternateIcon() { - const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`]; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -2063,7 +1947,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} style={{ - display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none', + display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', color: usePath === undefined ? 'black' : 'white', }}> @@ -2072,14 +1956,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps </Tooltip> ); } - @computed get fieldKey() { - const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}_usePath`]); - return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : ''); + get fieldKey() { + const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { - if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { + if (this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { // if the scrolled freeform is a child of the sidebar component, we need to let the event go through // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling e.preventDefault(); @@ -2088,11 +1972,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - if (this.props.isContentActive()) { + if (this._props.isContentActive()) { + const scale = this._props.NativeDimScaling?.() || 1; + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(this.props.PanelHeight()) && // - (this.props.isSelected() || this.isAnyChildContentActive()) + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); } @@ -2101,25 +1988,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; _oldWheel: any; @computed get fontColor() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); } @computed get fontSize() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); } @computed get fontFamily() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); } @computed get fontWeight() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight); } render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; - setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); - const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); + const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); + const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" @@ -2131,7 +2018,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} style={{ - ...(this.props.dontScale + ...(this._props.dontScale ? {} : { transform: `scale(${scale})`, @@ -2150,9 +2037,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-cont" ref={this._ref} style={{ - cursor: this.props.isContentActive() ? 'text' : undefined, - height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', + cursor: this._props.isContentActive() ? 'text' : undefined, + height: this._props.height ? 'max-content' : undefined, + overflow: this.layout_autoHeight ? 'hidden' : undefined, + pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2167,7 +2055,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2184,8 +2072,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} + {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index e7ca26d5c..ce17af6ca 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -3,7 +3,7 @@ import { EditorState, NodeSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; -import { LinkDocPreview } from '../LinkDocPreview'; +import { LinkDocPreview, LinkInfo } from '../LinkDocPreview'; import { FormattedTextBox } from './FormattedTextBox'; import './FormattedTextBoxComment.scss'; import { schema } from './schema_rts'; @@ -133,9 +133,10 @@ export class FormattedTextBoxComment { const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; //nbef && naft && - LinkDocPreview.SetLinkInfo({ - docProps: textBox.props, - linkSrc: textBox.rootDoc, + LinkInfo.SetLinkInfo({ + DocumentView: textBox.DocumentView, + styleProvider: textBox._props.styleProvider, + linkSrc: textBox.Document, linkDoc: linkDoc ? (DocServer.GetCachedRefField(linkDoc) as Doc) : undefined, location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), hrefs, diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index ec11079b4..08bad2d57 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -47,7 +47,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey } const canEdit = (state: any) => { - switch (GetEffectiveAcl(props.DataDoc)) { + switch (GetEffectiveAcl(props.TemplateDataDocument)) { case AclAugment: const prevNode = state.selection.$cursor.nodeBefore; const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; @@ -334,7 +334,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a blank space bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (props.DataDoc && GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true; + if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index 8afa0f6b5..d6ed5ebee 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -1,4 +1,4 @@ -@import "../../global/globalCssVariables"; +@import '../../global/globalCssVariables.module.scss'; .button-dropdown-wrapper { position: relative; @@ -55,7 +55,6 @@ input { color: black; } - } .richTextMenu { @@ -68,12 +67,12 @@ font-size: 12px; height: 100%; margin-right: 3px; - + &:focus, &:hover { background-color: black; } - + &::-ms-expand { color: white; } @@ -126,7 +125,7 @@ display: flex; justify-content: space-between; - >div { + > div { display: flex; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index e3ac4fb9d..5858c3b11 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,27 +1,28 @@ -import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model'; import { wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; -import { Cast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; +import { numberRange } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { EquationBox } from '../EquationBox'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; -import { EquationBox } from '../EquationBox'; -import { numberRange } from '../../../../Utils'; const { toggleMark } = require('prosemirror-commands'); @observer @@ -30,7 +31,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); - @observable public view?: EditorView; + layoutDoc: Doc | undefined; + @observable public view?: EditorView = undefined; public editorProps: FieldViewProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -62,14 +64,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private showLinkDropdown: boolean = false; _reaction: IReactionDisposer | undefined; - constructor(props: Readonly<{}>) { + constructor(props: AntimodeMenuProps) { super(props); - runInAction(() => { - RichTextMenu.Instance = this; - this.updateMenu(undefined, undefined, props); - this._canFade = false; - this.Pinned = true; - }); + makeObservable(this); + RichTextMenu.Instance = this; + this.updateMenu(undefined, undefined, props, this.layoutDoc); + this._canFade = false; + this.Pinned = true; } @computed get noAutoLink() { @@ -102,11 +103,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get textAlign() { return this._activeAlignment; } + @computed get textVcenter() { + return BoolCast(this.layoutDoc?.layout_centered); + } _disposer: IReactionDisposer | undefined; componentDidMount() { this._disposer = reaction( - () => SelectionManager.Views().slice(), - views => this.updateMenu(undefined, undefined, undefined) + () => SelectionManager.Views.slice(), + views => this.updateMenu(undefined, undefined, undefined, undefined) ); } componentWillUnmount() { @@ -114,11 +118,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } @action - public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) { + public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any, layoutDoc: Doc | undefined) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; + this.layoutDoc = layoutDoc; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change @@ -181,7 +186,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveAlignment() { - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const path = (this.view.state.selection.$from as any).path; 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) { @@ -194,7 +199,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const path = (this.view.state.selection.$from as any).path; for (let i = 0; i < path.length; i += 3) { if (path[i].type === this.view.state.schema.nodes.ordered_list) { @@ -214,7 +219,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const activeSizes = new Set<string>(); const activeColors = new Set<string>(); const activeHighlights = new Set<string>(); - if (this.view && this.TextView?.props.isSelected(true)) { + if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; const marks: Mark[] = [...(state.storedMarks ?? [])]; @@ -233,8 +238,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); - } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._text_fontSize) && activeSizes.add(StrCast(dv.rootDoc._text_fontSize))); + } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { + SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } @@ -249,7 +254,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //finds all active marks on selection in given group getActiveMarksOnSelection() { let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView?.props.isSelected(true)) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks; const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); @@ -285,10 +290,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return activeMarks; } - destroy() { - !this.TextView?.props.isSelected(true) && this.fadeOut(true); - } - @action setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; @@ -357,10 +358,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } - } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize)); + } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { + SelectionManager.Views.forEach(dv => (dv.Document._text_fontSize = fontSize)); } else Doc.UserDoc().fontSize = fontSize; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; setFontFamily = (family: string) => { @@ -369,7 +370,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else Doc.UserDoc().fontFamily = family; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; setHighlight(color: string) { @@ -378,7 +379,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true); this.view.focus(); } else Doc.UserDoc()._fontHighlight = color; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } setColor(color: string) { @@ -387,7 +388,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); this.view.focus(); } else Doc.UserDoc().fontColor = color; - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } // TODO: remove doesn't work @@ -428,7 +429,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } this.view.focus(); - this.updateMenu(this.view, undefined, this.props); + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; insertSummarizer(state: EditorState, dispatch: any) { @@ -442,8 +443,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return true; } + vcenterToggle = (view: EditorView, dispatch: any) => { + this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered); + }; align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { - if (this.TextView?.props.isSelected(true)) { + if (this.TextView?._props.rootSelected?.()) { var tr = view.state.tr; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { @@ -577,7 +581,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return (this.view as any)?.TextView as FormattedTextBox; } get TextViewFieldKey() { - return this.TextView?.props.fieldKey; + return this.TextView?._props.fieldKey; } @action setActiveHighlight(color: string) { @@ -638,7 +642,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (linkDoc instanceof Doc) { const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc); const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc); - const currentDoc = SelectionManager.Docs().lastElement(); + const currentDoc = SelectionManager.Docs.lastElement(); if (currentDoc && link_anchor_1 && link_anchor_2) { if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) { return StrCast(link_anchor_2.title); @@ -665,7 +669,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }; @undoBatch - @action deleteLink = () => { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); @@ -771,11 +774,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // <div className="collectionMenu-divider" key="divider 3" /> // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { // this.activeFontSize = val; - // SelectionManager.Views().map(dv => dv.props.Document._text_fontSize = val); + // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val); // })), // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { // this.activeFontFamily = val; - // SelectionManager.Views().map(dv => dv.props.Document._text_fontFamily = val); + // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val); // })), // <div className="collectionMenu-divider" key="divider 4" />, // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), @@ -808,10 +811,15 @@ interface ButtonDropdownProps { } @observer -export class ButtonDropdown extends React.Component<ButtonDropdownProps> { +export class ButtonDropdown extends ObservableReactComponent<ButtonDropdownProps> { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; + constructor(props: any) { + super(props); + makeObservable(this); + } + componentDidMount() { document.addEventListener('pointerdown', this.onBlur); } @@ -846,22 +854,22 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { render() { return ( <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}> - {!this.props.pdf ? ( - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> - {this.props.button} - <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {!this._props.pdf ? ( + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> + {this._props.button} + <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this._props.openDropdownOnButton ? this.onDropdownClick : undefined}> <FontAwesomeIcon icon="caret-down" size="sm" /> </div> </div> ) : ( <> - {this.props.button} + {this._props.button} <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> <FontAwesomeIcon icon="caret-down" size="sm" /> </button> </> )} - {this.showDropdown ? this.props.dropdownContent : null} + {this.showDropdown ? this._props.dropdownContent : null} </div> ); } @@ -875,6 +883,6 @@ export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> return null; } update(view: EditorView, lastState: EditorState | undefined) { - RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps); + RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, (view as any).TextView?.layoutDoc); } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 3e2afd2ce..456ed4732 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -95,7 +95,6 @@ export class RichTextRules { textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`); 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; @@ -265,9 +264,16 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); } }; + const getTitledDoc = (docTitle: string) => { + if (!DocServer.FindDocByTitle(docTitle)) { + Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + } + const titledDoc = DocServer.FindDocByTitle(docTitle); + return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; + }; if (!fieldKey) { if (docTitle) { - const target = DocServer.FindDocByTitle(docTitle); + const target = getTitledDoc(docTitle); if (target) { setTimeout(() => linkToDoc(target)); return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); @@ -279,7 +285,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const target = DocServer.FindDocByTitle(docTitle); + const target = getTitledDoc(docTitle); const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 3355e4529..7ec296ed2 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,7 +1,7 @@ import { TextSelection } from 'prosemirror-state'; import { Fragment, Node, Slice } from 'prosemirror-model'; import * as ReactDOM from 'react-dom/client'; -import React = require('react'); +import * as React from 'react'; // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. // this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss index 8c4d77da9..87320943d 100644 --- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss +++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss @@ -1,9 +1,9 @@ -@import "../views/global/globalCssVariables"; +@import '../views/global/globalCssVariables.module.scss'; .ProseMirror-menu-dropdown-wrap { display: inline-block; position: relative; } - + .ProseMirror-menu-dropdown { vertical-align: 1px; cursor: pointer; @@ -17,11 +17,11 @@ margin-right: 4px; &:after { - content: ""; + content: ''; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 2px); @@ -33,7 +33,7 @@ margin-right: -4px; } -.ProseMirror-menu-dropdown-menu, +.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { font-size: 12px; background: white; @@ -55,19 +55,18 @@ } } - .ProseMirror-menu-submenu-label:after { - content: ""; + content: ''; border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid currentColor; - opacity: .6; + opacity: 0.6; position: absolute; right: 4px; top: calc(50% - 4px); } - - .ProseMirror-icon { + +.ProseMirror-icon { display: inline-block; // line-height: .8; // vertical-align: -2px; /* Compensate for padding */ @@ -79,14 +78,14 @@ } svg { - fill:white; + fill: white; height: 1em; } span { vertical-align: text-top; - } - } + } +} .wrapper { position: absolute; @@ -99,10 +98,10 @@ background: #323232; border-radius: 6px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - } -.tooltipMenu, .basic-tools { +.tooltipMenu, +.basic-tools { z-index: 3000; pointer-events: all; padding: 3px; @@ -111,17 +110,17 @@ align-items: center; .ProseMirror-example-setup-style hr { - padding: 2px 10px; - border: none; - margin: 1em 0; + padding: 2px 10px; + border: none; + margin: 1em 0; } - + .ProseMirror-example-setup-style hr:after { - content: ""; - display: block; - height: 1px; - background-color: silver; - line-height: 2px; + content: ''; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; } } @@ -142,7 +141,7 @@ } } - &> * { + & > * { margin-top: 50%; margin-left: 50%; transform: translate(-50%, -50%); @@ -168,7 +167,7 @@ background-color: black; } - &> * { + & > * { margin-top: 50%; margin-left: 50%; transform: translate(-50%, -50%); @@ -208,18 +207,17 @@ margin-top: 13px; } - .font-size-indicator { - font-size: 12px; - padding-right: 0px; - } - .summarize{ - color: white; - height: 20px; - text-align: center; - } - - -.brush{ +.font-size-indicator { + font-size: 12px; + padding-right: 0px; +} +.summarize { + color: white; + height: 20px; + text-align: center; +} + +.brush { display: inline-block; width: 1em; height: 1em; @@ -229,7 +227,7 @@ margin-right: 15px; } -.brush-active{ +.brush-active { display: inline-block; width: 1em; height: 1em; @@ -269,7 +267,6 @@ } .buttonSettings-dropdown { - &.ProseMirror-menu-dropdown { width: 10px; height: 25px; @@ -301,7 +298,7 @@ padding: 3px; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - .ProseMirror-menu-dropdown-item{ + .ProseMirror-menu-dropdown-item { cursor: default; &:last-child { @@ -312,7 +309,8 @@ background-color: #323232; } - .button-setting, .button-setting-disabled { + .button-setting, + .button-setting-disabled { padding: 2px; border-radius: 2px; } @@ -328,25 +326,23 @@ } input { - color: black; - border: none; - border-radius: 1px; - padding: 3px; + color: black; + border: none; + border-radius: 1px; + padding: 3px; } button { - padding: 6px; - background-color: #323232; - border: 1px solid black; - border-radius: 1px; + padding: 6px; + background-color: #323232; + border: 1px solid black; + border-radius: 1px; &:hover { - background-color: black; + background-color: black; } } } - - } } diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 6f07588b3..a342285b0 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model'; import { Doc } from '../../../../fields/Doc'; @@ -28,7 +28,6 @@ export const marks: { [index: string]: MarkSpec } = { autoLinkAnchor: { attrs: { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, - location: { default: null }, title: { default: null }, }, inclusive: false, @@ -37,7 +36,6 @@ export const marks: { [index: string]: MarkSpec } = { tag: 'a[href]', getAttrs(dom: any) { return { - location: dom.getAttribute('location'), title: dom.getAttribute('title'), }; }, @@ -46,7 +44,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; + return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -73,7 +71,6 @@ export const marks: { [index: string]: MarkSpec } = { linkAnchor: { attrs: { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, - location: { default: null }, title: { default: null }, noPreview: { default: false }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text @@ -84,7 +81,6 @@ export const marks: { [index: string]: MarkSpec } = { tag: 'a[href]', getAttrs(dom: any) { return { - location: dom.getAttribute('location'), title: dom.getAttribute('title'), noPreview: dom.getAttribute('noPreview'), }; @@ -108,7 +104,7 @@ export const marks: { [index: string]: MarkSpec } = { node.attrs.title, ], ] - : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline; cursor: default` }, 0]; + : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index f27fb18e2..d023020e1 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -1,4 +1,4 @@ -import React = require('react'); +import * as React from 'react'; import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; @@ -212,7 +212,6 @@ export const nodes: { [index: string]: NodeSpec } = { alt: { default: null }, title: { default: null }, float: { default: 'left' }, - location: { default: 'add:right' }, docId: { default: '' }, }, group: 'inline', |
