diff options
Diffstat (limited to 'src/client/views/nodes/formattedText')
8 files changed, 203 insertions, 352 deletions
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 74eeb014c..d79df4272 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -26,6 +26,7 @@ display: inline-block; font-weight: normal; background: rgba(0, 0, 0, 0.1); + cursor: default; } .dashFieldView-fieldSpan { min-width: 8px; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 22a0cbe5e..439d4785e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -87,7 +87,6 @@ export class DashFieldView { hideValue={node.attrs.hideValue} editable={node.attrs.editable} nodeSelected={this.NodeSelected} - dataDoc={node.attrs.dataDoc} tbox={tbox} /> ); @@ -119,7 +118,6 @@ interface IDashFieldViewInternal { height: number; editable: boolean; nodeSelected: () => boolean; - dataDoc: boolean; node: any; getPos: any; unclickable: () => boolean; @@ -139,7 +137,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi makeObservable(this); this._fieldKey = this._props.fieldKey; this._textBoxDoc = this._props.tbox.Document; - const setDoc = action((doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc)); + const setDoc = action((doc: Doc) => (this._dashDoc = doc)); if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); @@ -168,7 +166,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi setTimeout(() => !this._props.tbox.ProseRef?.contains(document.activeElement) && this._props.tbox._props.onBlur?.()); } }); - selectedCell = (): [Doc, number] => [this._dashDoc!, 0]; + selectedCell = (): [Doc, number] | undefined => (this._dashDoc ? [this._dashDoc, 0] : undefined); columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey // set the display of the field's value (checkbox for booleans, span of text for strings) @@ -284,7 +282,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} {!this.values.length ? null : ( - <select className="dashFieldView-select" tabIndex={-1} defaultValue={Field.toKeyValueString(this._dashDoc!, this._fieldKey)} onChange={this.selectVal}> + <select className="dashFieldView-select" tabIndex={-1} defaultValue={this._dashDoc && Field.toKeyValueString(this._dashDoc, this._fieldKey)} onChange={this.selectVal}> <option value="-unset-">-unset-</option> {this.values.map(val => ( <option value={val.value}>{val.label}</option> @@ -334,13 +332,11 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { render() { return this.getElement( <> - {!this._fieldKey.startsWith('#') ? null : ( - <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.showFields}> - <FontAwesomeIcon icon="eye" size="sm" /> - </button> - </Tooltip> - )} + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="sm" /> + </button> + </Tooltip> {this._fieldKey.startsWith('#') ? null : ( <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 00d890860..38dd2e847 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -156,7 +156,6 @@ audiotag:hover { .formattedTextBox-inner, .formattedTextBox-inner-minimal { height: 100%; - overflow: auto; white-space: pre-wrap; .ProseMirror:hover { background: rgba(200, 200, 200, 0.2); @@ -274,6 +273,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; @@ -349,6 +349,8 @@ footnote::before { touch-action: none; span { font-family: inherit; + background-color: inherit; + display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span } blockquote { @@ -398,6 +400,7 @@ footnote::before { font-family: inherit; } margin-left: 0; + background-color: inherit; } .decimal2-ol { counter-reset: deci2; @@ -407,6 +410,7 @@ footnote::before { } font-size: smaller; padding-left: 2.1em; + background-color: inherit; } .decimal3-ol { counter-reset: deci3; @@ -416,6 +420,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .decimal4-ol { counter-reset: deci4; @@ -425,6 +430,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } .decimal5-ol { counter-reset: deci5; @@ -433,6 +439,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal6-ol { counter-reset: deci6; @@ -441,6 +448,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal7-ol { counter-reset: deci7; @@ -449,6 +457,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .multi1-ol { @@ -459,6 +468,7 @@ footnote::before { } margin-left: 0; padding-left: 1.2em; + background-color: inherit; } .multi2-ol { counter-reset: multi2; @@ -468,6 +478,7 @@ footnote::before { } font-size: smaller; padding-left: 2em; + background-color: inherit; } .multi3-ol { counter-reset: multi3; @@ -477,6 +488,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .multi4-ol { counter-reset: multi4; @@ -486,6 +498,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } @@ -789,6 +802,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a46b19a85..43010b2ed 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -91,7 +91,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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 _scrollRef: HTMLDivElement | null = null; private _editorView: Opt<EditorView>; public _applyingChange: string = ''; private _inDrop = false; @@ -291,7 +291,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); }; AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -478,7 +478,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( !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('@')) && + title.startsWith('-') && this._editorView && !this.dataDoc.title_custom && (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') @@ -486,14 +486,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; - const prefix = str.startsWith('@') ? '' : '-'; + const prefix = '-'; const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); 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.AddToMyPublished(this.Document); - } } } }; @@ -510,7 +507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { const editorView = this._editorView; if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { - const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); + const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, ''); var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { if ( @@ -887,7 +884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50); + setTimeout(() => (this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50), 50); }), icon: 'eye', }); @@ -1309,25 +1306,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); if (this._recordingDictation) setTimeout(this.recordDictation); } - var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { - 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/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - if (duration) { - this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper); - } else { - this._scrollRef.current.scrollTo({ top: pos }); - } + if (!this._ignoreScroll && this._scrollRef) { + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]+)(m?)s/); + const duration = Number(durationStr?.[1]) * (durationStr?.[2] ? 1 : 1000); + this._scrollStopper = smoothScroll(duration || 0, this._scrollRef, Math.abs(pos || 0), 'ease', this._scrollStopper); } }, { fireImmediately: true } ); - quickScroll = undefined; this.tryUpdateScrollHeight(); setTimeout(this.tryUpdateScrollHeight, 250); } @@ -1419,14 +1408,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB handleScrollToSelection: editorView => { const docPos = editorView.coordsAtPos(editorView.state.selection.to); const viewRect = self._ref.current!.getBoundingClientRect(); - const scrollRef = self._scrollRef.current; + const scrollRef = self._scrollRef; const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { - scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); + setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper))); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1496,6 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { @@ -1589,32 +1579,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB document.removeEventListener('pointerup', this.onSelectEnd); }; onPointerUp = (e: React.PointerEvent): void => { - const editor = this._editorView!; - const state = editor?.state; - 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() && !e.button) { - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - let xpos = pcords?.pos || 0; - while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { - xpos = xpos - 1; - } - let node: any; - try { - node = state.doc.nodeAt(xpos); - } catch (e) {} - if (node?.type !== schema.nodes.dashFieldView) { - editor.dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(xpos)))); - 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'); - } else if (node) { - try { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos)))); - } catch (e) { - editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos - 1)))); - } - } + const state = this.EditorView?.state; + if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { + if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); + let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); + while (target && !target.dataset?.targethrefs) target = target.parentElement; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); } }; @action @@ -1751,20 +1722,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; + + // this is the markdown for @<published name> document publishing to Doc.myPublishedDocs + const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); + if (match) { + this.dataDoc.title_custom = true; + this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc + this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1)); + } + this.endUndoTypingBatch(); FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; const state = this._editorView!.state; - 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.Document.title).length + 2))).deleteSelection()); - }, 'titler'); - } // if the text box blurs and none of its contents are focused(), then pass the blur along setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; @@ -1799,7 +1771,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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, undefined); + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); @@ -1826,26 +1798,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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 (!LinkInfo.Instance?.LinkInfo && this._scrollRef.current) { - if (!this._props.dontSelectOnLoad) { - this._ignoreScroll = true; - this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; - this._ignoreScroll = false; - e.stopPropagation(); - e.preventDefault(); - } + if (!LinkInfo.Instance?.LinkInfo && this._scrollRef) { + this._ignoreScroll = true; + this.layoutDoc._layout_scrollTop = this._scrollRef.scrollTop; + this._ignoreScroll = false; + e.stopPropagation(); + e.preventDefault(); } }; tryUpdateScrollHeight = () => { 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.IsDragging) { - const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); - const toHgt = (node: Element) => { + const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; + const toNum = (val: string) => Number(val.replace('px', '')); + const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); - return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); + const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); + return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; - const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); + const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children); const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && 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 @@ -2029,12 +2001,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // 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()) { 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 styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( !Number.isNaN(height) && - (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); @@ -2062,7 +2034,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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' > + 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._header_height}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" @@ -2112,9 +2084,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onDoubleClick={this.onDoubleClick}> <div className="formattedTextBox-outer" - ref={this._scrollRef} + ref={r => (this._scrollRef = r)} style={{ - width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2131,8 +2103,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }} /> </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.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || 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/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index ab49a53ea..03c902580 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -11,7 +11,6 @@ import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { OpenWhere } from '../DocumentView'; -//import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; import { Doc } from '../../../../fields/Doc'; import { EditorView } from 'prosemirror-view'; @@ -90,7 +89,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); - const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); + const marks = state.storedMarks || state.selection.$to.parentOffset ? state.selection.$from.marks() : undefined; if ( !sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); @@ -103,7 +102,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end))); if ( !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => { - const tx3 = updateBullets(tx2, schema); + const tx25 = updateBullets(tx2, schema); + const ol_node = tx25.doc.nodeAt(range!.start)!; + const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks); // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); @@ -256,7 +257,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); - const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { + const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; @@ -268,7 +269,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if ( !joinBackward(state, (tx: Transaction) => { dispatch(updateBullets(tx, schema)); - if (once && view.state.selection.$from.depth > 1 && view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === view.state.schema.nodes.list_item) backspace(view.state, view.dispatch, view, false); + if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { + // gets rid of an extra paragraph when joining two list items together. + joinBackward(view.state, (tx: Transaction) => view.dispatch(tx)); + } }) ) { if ( @@ -295,7 +299,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const depth = trange ? liftTarget(trange) : null; if ( depth !== null && - state.selection.$from.node(state.selection.$from.depth - 1)?.type === state.schema.nodes.blockquote && // + state.selection.$from.node(-1)?.type === state.schema.nodes.blockquote && // !state.selection.$from.node().content.size && trange ) { @@ -305,7 +309,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!newlineInCode(state, dispatch as any)) { - if (once && view.state.selection.$from.depth > 1 && !view.state.selection.$from.nodeBefore && !view.state.selection.$from.nodeBefore) { + const olNode = view.state.selection.$anchor.node(-2); + const liNode = view.state.selection.$anchor.node(-1); + // prettier-ignore + if (liNode?.type === schema.nodes.list_item && !liNode.textContent && + olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3) + { + // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); } else if ( !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { @@ -313,32 +323,32 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); dispatch(tx3); + // removes an extra paragraph created when selecting text across two list items or splitting an empty list item + !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2)); }) ) { - const fromattrs = state.selection.$from.node().attrs; - if ( - !splitBlockKeepMarks(state, (tx3: Transaction) => { - const tonode = tx3.selection.$to.node(); - if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); - dispatch(tx4); - if ( - view.state.selection.$from.parentOffset && // - !view.state.selection.$from.node().content.size - ) - liftListItem(schema.nodes.list_item)(view.state, view.dispatch); - else if ( - once && - view.state.selection.$from.parentOffset && - view.state.selection.$from.depth > 1 && // - view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === schema.nodes.list_item - ) - enter(view.state, view.dispatch, view, false); - else if (once && depth && !view.state.selection.$from.parentOffset) backspace(view.state, view.dispatch, view, false); - } else dispatch(tx3.insertText('\r\n')); - }) - ) { - return false; + if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') { + // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again + view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({}))); + enter(view.state, view.dispatch, view, false); + } else { + const fromattrs = state.selection.$from.node().attrs; + if ( + !splitBlockKeepMarks(state, (tx3: Transaction) => { + const tonode = tx3.selection.$to.node(); + if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + dispatch(tx4); + } + + if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. + enter(view.state, dispatch, view, false); + } + }) + ) { + return false; + } } } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index b5d0f28d8..cecf106a3 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -27,7 +27,10 @@ const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { - @observable static Instance: RichTextMenu; + static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); + static get Instance() { + return RichTextMenu._instance?.menu; + } public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); @@ -48,7 +51,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; - @observable private activeListType: string = ''; + @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set<Mark> = new Set(); @@ -67,7 +70,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu.Instance = this; + RichTextMenu._instance.menu = this; this.updateMenu(undefined, undefined, props, this.layoutDoc); this._canFade = false; this.Pinned = true; @@ -100,6 +103,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get fontSize() { return this._activeFontSize; } + @computed get listStyle() { + return this._activeListType; + } @computed get textAlign() { return this._activeAlignment; } @@ -131,11 +137,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; } - // update active marks - const activeMarks = this.getActiveMarksOnSelection(); - this.setActiveMarkButtons(activeMarks); - - // update active font family and size + this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); const activeFamilies = active.activeFamilies; const activeSizes = active.activeSizes; @@ -144,7 +146,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); - this.activeListType = this.getActiveListStyle(); + this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; @@ -161,17 +163,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); - const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from; + const fromRange = numberRange(state.selection.from).reverse(); + const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); - if (node?.type === schema.nodes.ordered_list) { - let attrs = node.attrs; - if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; - if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; - const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); - } - { + if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { + const hasMark = node.marks.some(m => m.type === mark.type); + const otherMarks = node.marks.filter(m => m.type !== mark.type); + const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); + const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); + dispatch(updateBullets(markup, state.schema)); + } else { const state = this.view?.state; const tr = this.view?.state.tr; if (tr && state) { @@ -201,16 +202,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - 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) { - return path[i].attrs.mapStyle; + const state = this.view?.state; + if (state) { + const pos = state.selection.$anchor; + for (let i = 0; i < pos.depth; i++) { + const node = pos.node(i); + if (node.type === schema.nodes.ordered_list) { + return node.attrs.mapStyle; } } - if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) { - return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle; - } } return ''; } @@ -224,11 +224,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; - const marks: Mark[] = [...(state.storedMarks ?? [])]; + var marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { } else if (state.selection.empty) { - const ref_node = this.reference_node(pos); - marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; + } } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); @@ -255,41 +256,26 @@ 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.rootSelected?.()) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - 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); - //current selection - const { empty, ranges, $to } = this.view.state.selection as TextSelection; const state = this.view.state; - if (!empty) { - activeMarks = markGroup.filter(mark => { - const has = false; - for (let i = 0; !has && i < ranges.length; i++) { - return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark); - } - return false; - }); - } else { - const pos = this.view.state.selection.$from; - const ref_node: ProsNode | null = this.reference_node(pos); - if (ref_node !== null && ref_node !== this.view.state.doc) { - if (ref_node.isText) { - } else { - return []; - } - activeMarks = markGroup.filter(mark_type => { - // if (mark_type === state.schema.marks.pFontSize) { - // return mark.isINSet - // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); - // } - const mark = state.schema.mark(mark_type); - return mark.isInSet(ref_node.marks); - }); + var marks: Mark[] = [...(state.storedMarks ?? [])]; + const pos = this.view.state.selection.$from; + if (state.storedMarks !== null) { + } else if (state.selection.empty) { + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } + } else { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); + }); } - 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]; + return markGroup.filter(mark_type => { + const mark = state.schema.mark(mark_type); + return mark.isInSet(marks); + }); } @action @@ -370,7 +356,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontSize = (fontSize: string) => { if (this.view) { if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { - this.TextView.dataDoc.fontSize = fontSize; + this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize; this.view.focus(); } else { const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); @@ -385,7 +371,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontFamily = (family: string) => { if (this.view) { - const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); + const fmark = this.view.state.schema.marks.pFontFamily.create({ family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else if (SelectionManager.Views.length) { @@ -417,40 +403,24 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { - const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle(); - const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle }); - if (!this.view || nodeType?.attrs.mapStyle === '') return; - - const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list; - let inList: any = undefined; - let fromList = -1; - const path: any = Array.from((this.view.state.selection.$from as any).path); - for (let i = 0; i < path.length; i++) { - if (path[i]?.type === schema.nodes.ordered_list) { - inList = path[i]; - fromList = path[i - 1]; - } - } + const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle(); + const newMapStyle = active === mapStyle ? '' : mapStyle; + if (!this.view || newMapStyle === '') return; + let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if ( - inList || + if (inList) { + const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + this.view.dispatch(tx2); + } else !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); + const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - - this.view!.dispatch(tx2); - }) - ) { - const tx2 = this.view.state.tr; - if (nodeType && (inList || nextIsOL)) { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - this.view.dispatch(tx3); - } - } + this.view!.dispatch(tx3); + }); this.view.focus(); this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; @@ -565,7 +535,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // todo: add brushes to brushMap to save with a style name onBrushNameKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); + RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; @@ -573,7 +543,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @action clearBrush() { - RichTextMenu.Instance.brushMarks = new Set(); + RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set()); } @action @@ -709,118 +679,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - linkExtend($start: ResolvedPos, href: string) { - const mark = this.view!.state.schema.marks.linkAnchor; - - let startIndex = $start.index(); - let endIndex = $start.indexAfter(); - - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++; - - let startPos = $start.start(); - let endPos = startPos; - for (let i = 0; i < endIndex; i++) { - const size = $start.parent.child(i).nodeSize; - if (i < startIndex) startPos += size; - endPos += size; - } - return { from: startPos, to: endPos }; - } - - reference_node(pos: ResolvedPos): ProsNode | null { - if (!this.view) return null; - - let ref_node: ProsNode = this.view.state.doc; - if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { - ref_node = pos.nodeBefore; - } - if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) { - ref_node = pos.nodeAfter; - } - } - if (!ref_node && pos.pos > 0) { - let skip = false; - for (let i: number = pos.pos - 1; i > 0; i--) { - this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { - if (node.isLeaf && !skip) { - ref_node = node; - skip = true; - } - }); - } - } - if (!ref_node.isLeaf && ref_node.childCount > 0) { - ref_node = ref_node.child(0); - } - return ref_node; - } - render() { return null; - // TraceMobx(); - // const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - // //!this.collapsed ? this.getDragger() : (null), - // // !this.Pinned ? (null) : <div key="frag1"> {[ - // // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // // <div className="richTextMenu-divider" key="divider" /> - // // ]}</div>, - // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // this.createColorButton(), - // this.createHighlighterButton(), - // this.createLinkButton(), - // this.createBrushButton(), - // <div className="collectionMenu-divider" key="divider 2" />, - // this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), - // this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), - // this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), - // this.createButton("indent", "Inset More", undefined, this.insetParagraph), - // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), - // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), - // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), - // ]}</div>; - - // const row2 = <div className="antimodeMenu-row row-2" key="row2"> - // {this.collapsed ? this.getDragger() : (null)} - // <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - // <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.Document._text_fontSize = val); - // })), - // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { - // this.activeFontFamily = 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", () => ({})), - // this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), - // this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), - // this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule) - // ]} - // </div> - // {/* <div key="collapser"> - // {<div key="collapser"> - // <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> - // <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> - // </button> - // </div> } - // <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> - // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - // </button> - // </div> */} - // </div>; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 5e53a019e..42665830f 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -246,7 +246,7 @@ export class RichTextRules { // activate a style by name using prefix '%<color name>' new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => { const color = match[0].substring(1, match[0].length); - const marks = RichTextMenu.Instance._brushMap.get(color); + const marks = RichTextMenu.Instance?._brushMap.get(color); if ( DocListCast((Doc.UserDoc().template_notes as Doc).data) @@ -295,8 +295,8 @@ export class RichTextRules { // create a hyperlink to a titled document // @(<doctitle>) - new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => { - const docTitle = match[2]; + new InputRule(new RegExp(/@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => { + const docTitle = match[1]; const prefixLength = '@('.length; if (docTitle) { const linkToDoc = (target: Doc) => { @@ -342,12 +342,7 @@ export class RichTextRules { const assign = match[4] === ':' ? (match[4] = '') : match[4]; const value = match[5]; const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('='); - const getTitledDoc = (docTitle: string) => { - if (!DocServer.FindDocByTitle(docTitle)) { - Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); - } - return DocServer.FindDocByTitle(docTitle); - }; + const getTitledDoc = (docTitle: string) => DocServer.FindDocByTitle(docTitle); // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); @@ -359,7 +354,7 @@ export class RichTextRules { if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr; } const target = docTitle ? getTitledDoc(docTitle) : undefined; - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, hideValue: false, dataDoc }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, hideValue: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }, { inCode: true } @@ -372,7 +367,7 @@ export class RichTextRules { if (count) { const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); - RichTextMenu.Instance.elideSelection(this.TextBox.EditorView?.state, true); + RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true); } count++; }); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index e335044ea..62b8b03d6 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -24,6 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = { // :: NodeSpec The top level document node. doc: { content: 'block+', + marks: '_', }, paragraph: ParagraphNodeSpec, @@ -120,7 +121,6 @@ export const nodes: { [index: string]: NodeSpec } = { ...ParagraphNodeSpec.attrs, level: { default: 1 }, }, - defining: true, parseDOM: [ { tag: 'h1', attrs: { level: 1 } }, { tag: 'h2', attrs: { level: 2 } }, @@ -131,8 +131,7 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node) { const dom = toParagraphDOM(node) as any; - const level = node.attrs.level || 1; - dom[0] = 'h' + level; + dom[0] = `h${node.attrs.level || 1}`; return dom; }, getAttrs(dom: any) { @@ -266,7 +265,6 @@ export const nodes: { [index: string]: NodeSpec } = { hideKey: { default: false }, hideValue: { default: false }, editable: { default: true }, - dataDoc: { default: false }, }, leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), group: 'inline', @@ -332,12 +330,10 @@ export const nodes: { [index: string]: NodeSpec } = { ...orderedList, content: 'list_item+', group: 'block', + marks: '_', attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet" - fontColor: { default: 'inherit' }, - fontSize: { default: undefined }, - fontFamily: { default: undefined }, + mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet", visibility: { default: true }, indent: { default: undefined }, }, @@ -377,9 +373,10 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; - const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : ''; - const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : ''; - const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : ''; + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ @@ -387,7 +384,7 @@ export const nodes: { [index: string]: NodeSpec } = { { 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `${fsize} ${ffam} ${fcol} ${marg}`, + style: `${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ]; @@ -399,7 +396,7 @@ export const nodes: { [index: string]: NodeSpec } = { class: `${map}-ol`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`, + style: `list-style: none; ${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ] @@ -423,16 +420,22 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, ], - toDOM(node: any) { + toDOM(node: Node) { + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', - { class: `${map}`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, + { class: `${map}`, style: `${fhigh} ${fsize} ${ffam} ${fcol} `, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, node.attrs.visibility ? 0 : [ 'span', - { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre` }, + { + style: `${fhigh} ${fsize} ${ffam} ${fcol} position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre`, + }, `${node.firstChild?.textContent}...`, ], ]; |
