diff options
Diffstat (limited to 'src/client/views/nodes/formattedText')
8 files changed, 130 insertions, 218 deletions
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 2e2e1d41c..3734ad9cc 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); + align-content: center; cursor: default; } .dashFieldView-fieldSpan { @@ -42,6 +43,9 @@ user-select: all; min-width: 100%; display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } } } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index bb0efa917..7ea5d1fcf 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -184,7 +184,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi getFinfo={this.finfo} setColumnValues={returnFalse} allowCRs - oneLine={!this._expanded} + oneLine={!this._expanded && this._props.editable} finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} @@ -275,7 +275,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi </span> )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} - {!this.values.length ? null : ( + {!this.values.length || !this.props.editable ? null : ( <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 => ( @@ -308,6 +308,7 @@ export class DashFieldView { this.dom.style.height = node.attrs.height; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-flex'; + this.dom.style.maxWidth = '100%'; this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bef999e6d..dc23a695d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,5 +1,4 @@ /* eslint-disable no-use-before-define */ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Property } from 'csstype'; @@ -14,14 +13,13 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -35,7 +33,6 @@ import { DocUtils } from '../../../documents/DocUtils'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; -import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -104,12 +101,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; - private static _globalHighlightsCache: string = ''; - private static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); - private static _highlightStyleSheet = addStyleSheet(); - private static _bulletStyleSheet = addStyleSheet(); - private static _userStyleSheet = addStyleSheet(); + private _curHighlights = new ObservableSet<string>(['Audio Tags']); + private static _highlightStyleSheet = addStyleSheet().sheet; + private static _bulletStyleSheet = addStyleSheet().sheet; + private _userStyleSheetElement: HTMLStyleElement | undefined; + private _enteringStyle = false; private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); @@ -217,9 +214,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : this.rootDoc; - if (!pinProps && this.EditorView?.state.selection.empty) return rootDoc; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); + if (!pinProps && this.EditorView?.state.selection.empty) return this.rootDoc; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc?.title), annotationOn: this.rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); @@ -248,7 +244,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB anchor.followLinkAudio = true; let stopFunc: () => void = emptyFunction; target.$mediaState = mediaState.Recording; - DictationManager.recordAudioAnnotation(target, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + DictationManager.recordAudioAnnotation(target, Doc.LayoutDataKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, @@ -430,8 +426,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.isTemplateForField(this.Document) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore @@ -453,18 +449,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB updateTitle = () => { 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 + !this._props.dontRegisterView && // only update the title if the data document's data field is changing title.startsWith('-') && this.EditorView && !this.dataDoc.title_custom && - (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') + (Doc.LayoutDataKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this.EditorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; const prefix = '-'; - const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); + const cfield = ComputedField.DisableCompute(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); } @@ -591,7 +587,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const dragData = de.complete.docDragData; if (dragData) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); const draggedDoc = dragData.droppedDocuments.lastElement(); let added: Opt<boolean>; @@ -599,7 +596,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) { // replace text contents when dragging with Alt if (de.altKey) { - const fieldKey = Doc.LayoutFieldKey(draggedDoc); + const fieldKey = Doc.LayoutDataKey(draggedDoc); if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } @@ -699,45 +696,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } updateHighlights = (highlights: string[]) => { - if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; - setTimeout(() => { - FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''); - }); - clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (!highlights.includes('Audio Tags')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); - } - if (highlights.includes('Text from Others')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); - } - if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); - } - if (highlights.includes('Todo Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); - } - if (highlights.includes('Important Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' }); - } - if (highlights.includes('Bold Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, ''); - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, ''); - } - if (highlights.includes('Disagree Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' }); - } - if (highlights.includes('Ignore Items')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' }); - } + const userStyleSheet = () => { + if (!this._userStyleSheetElement) { + this._userStyleSheetElement = addStyleSheet(); + } + return this._userStyleSheetElement.sheet; + }; + const viewId = this.DocumentView?.().ViewGuid ?? 1; + const userId = ClientUtils.CurrentUserEmail().replace(/\./g, '').replace('@', ''); // must match marks_rts -> user_mark's uid + highlights.filter(f => f !== 'Audio Tags').length && clearStyleSheetRules(userStyleSheet()); + if (!highlights.includes('Audio Tags')) addStyleSheetRule(userStyleSheet(), `#${viewId} .audiotag`, { display: 'none' }, ''); // prettier-ignore + if (highlights.includes('Text from Others')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-remote`, { background: 'yellow' }, ''); // prettier-ignore + if (highlights.includes('My Text')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { background: 'moccasin' }, ''); // prettier-ignore + if (highlights.includes('Todo Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-todo`, { outline: 'black solid 1px' }, ''); // prettier-ignore + if (highlights.includes('Important Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-important`, { 'font-size': 'larger' }, ''); // prettier-ignore + if (highlights.includes('Disagree Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-disagree`, { 'text-decoration': 'line-through' }, ''); // prettier-ignore + if (highlights.includes('Ignore Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-ignore`, { 'font-size': '1' }, ''); // prettier-ignore + if (highlights.includes('Bold Text')) { addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong))`, { 'font-size': '0px' }, ''); + addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong)) ::after`, { content: '...', 'font-size': '5px' }, '')} // prettier-ignore if (highlights.includes('By Recent Minute')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const min = Math.round(Date.now() / 1000 / 60); - numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); + numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-min-` + (min - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const hr = Math.round(Date.now() / 1000 / 60 / 60); - numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); + numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-hr-` + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @@ -855,57 +840,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return; } - const changeItems: ContextMenuProps[] = []; - changeItems.push({ - description: 'plain', - event: undoable(() => { - Doc.setNativeView(this.Document); - this.layoutDoc.layout_autoHeightMargins = undefined; - }, 'set plain view'), - icon: 'eye', - }); - changeItems.push({ - description: 'metadata', - event: undoable(() => { - this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; - this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => { - this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; - }, 50); - }, 'set metadata view'), - icon: 'eye', - }); - const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null); - DocListCast(noteTypesDoc?.data).forEach(note => { - const icon: IconProp = StrCast(note.icon) as IconProp; - changeItems.push({ - description: StrCast(note.title), - event: undoable( - () => { - this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.Document); - DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); - }, - `set ${StrCast(note.title)} view}` - ), - icon: icon, - }); - }); const highlighting: ContextMenuProps[] = []; const noviceHighlighting = ['Audio Tags', 'My Text', 'Text from Others', 'Bold Text']; const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour']; (Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ - description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, + description: (!this._curHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, event: action(() => { e.stopPropagation(); - if (!FormattedTextBox._globalHighlights.has(option)) { - FormattedTextBox._globalHighlights.add(option); + if (!this._curHighlights.has(option)) { + this._curHighlights.add(option); } else { - FormattedTextBox._globalHighlights.delete(option); + this._curHighlights.delete(option); } }), - icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format', + icon: !this._curHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); const appearance = cm.findByDescription('Appearance...'); @@ -952,20 +901,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB icon: 'expand-arrows-alt', }); - appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); - - !Doc.noviceMode && - appearanceItems.push({ - description: 'Make Default Layout', - event: () => { - if (!this.layoutDoc.isTemplateDoc) { - MakeTemplate(this.Document); - } - Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); - Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); - }, - icon: 'eye', - }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); @@ -988,14 +923,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); - !Doc.noviceMode && - optionItems.push({ - description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, - event: () => { - this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; - }, - icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', - }); optionItems.push({ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), @@ -1076,16 +1003,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { link.$isDictation = true; - const audioanchor = Cast(link.link_anchor_2, Doc, null); - const textanchor = Cast(link.link_anchor_1, Doc, null); + const audioanchor = DocCast(link.link_anchor_2); + const textanchor = DocCast(link.link_anchor_1); if (audioanchor) { audioanchor.backgroundColor = 'tan'; const audiotag = this.EditorView.state.schema.nodes.audiotag.create({ timeCode: NumCast(audioanchor._timecodeToShow), audioId: audioanchor[Id], - textId: textanchor[Id], + textId: textanchor?.[Id] ?? '', }); - textanchor.$title = 'dictation:' + audiotag.attrs.timeCode; + textanchor && (textanchor.$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))); @@ -1220,11 +1147,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. // 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); + resetNativeHeight = action((scrollHeight: number) => { this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; - if (nh) this.layoutDoc._nativeHeight = scrollHeight; - }; + if (!this.layoutDoc.isTemplateForField) this.layoutDoc._nativeHeight = scrollHeight; + }); addPlugin = (plugin: Plugin) => { const editorView = this.EditorView; @@ -1252,15 +1178,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }), autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); - this._disposers.highlights = reaction( - () => Array.from(FormattedTextBox._globalHighlights).slice(), - highlights => this.updateHighlights(highlights), - { fireImmediately: true } - ); - this._disposers.width = reaction( - () => this._props.PanelWidth(), - () => this.tryUpdateScrollHeight() - ); + this._disposers.highlights = reaction(() => Array.from(this._curHighlights).slice(), this.updateHighlights, { fireImmediately: true }); + this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight), @@ -1272,7 +1191,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( - (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // + (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && // layoutAutoHeight && newHeight && (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) && @@ -1281,7 +1200,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._props.setHeight?.(newHeight); } }, - { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } + { fireImmediately: !Array.from(this._curHighlights).includes('Bold Text') } ); this._disposers.links = reaction( () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks @@ -1296,8 +1215,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const dataData = this.dataDoc[this.fieldKey]; const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey]; const dataTime = dataData ? (DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; - const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; @@ -1336,7 +1255,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); } this.prepareForTyping(); - if (FormattedTextBox._globalHighlights.has('Bold Text')) { + if (this._curHighlights.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 && this.EditorView) || this.isLabel) && !selected) { @@ -1393,7 +1312,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // } catch (err) { // console.log('Drop failed', err); // } - this.addDocument?.(DocCast(this.Document.image)); + DocCast(this.Document.image) && this.addDocument?.(DocCast(this.Document.image)!); } //if (this.Document.image) this.addDocument?.(DocCast(this.Document.image)); @@ -1506,8 +1425,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { this.EditorView?.destroy(); + const edState = () => { + try { + return rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config); + } catch { + return EditorState.create(config); + } + }; this._editorView = new EditorView(this.ProseRef, { - state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), + state: edState(), handleScrollToSelection: this.scrollToSelection, dispatchTransaction: this.dispatchTransaction, nodeViews: FormattedTextBox._nodeViews(this), @@ -1519,7 +1445,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB (this.EditorView as unknown as { scrollToSelection: unknown }).scrollToSelection = this.scrollToSelection; const { state } = this._editorView; if (!rtfField) { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const layoutProto = DocCast(this.layoutDoc.proto); + const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left'; if (textAlign !== 'left') { @@ -1581,6 +1508,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this.recordingDictation) { this.recordingDictation = !this.recordingDictation; } + removeStyleSheet(this._userStyleSheetElement); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); FormattedTextBox.LiveTextUndo?.end(); @@ -1827,13 +1755,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const { state } = _editorView; if (!state.selection.empty && e.key === '%') { - this._rules!.EnteringStyle = true; + this._enteringStyle = true; StopEvent(e); return; } + if (this._enteringStyle && 'tix!'.includes(e.key)) { + const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??'; + const node = state.selection.$from.nodeAfter; + const start = state.selection.from; + const end = state.selection.to; + + if (node) { + StopEvent(e); + _editorView.dispatch( + state.tr + .removeMark(start, end, schema.marks.user_mark) + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) })) + ); + return; + } + } - if (state.selection.empty || !this._rules!.EnteringStyle) { - this._rules!.EnteringStyle = false; + if (state.selection.empty || !this._enteringStyle) { + this._enteringStyle = false; } for (let i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 4fa1fb13b..c7731e812 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -148,11 +148,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._activeAlignment = this.getActiveAlignment(); this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox)); this._activeFontFamily = !activeFamilies.length - ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) + ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutFieldKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; @@ -382,7 +382,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } } else if (this.dataDoc) { - this.dataDoc[`${Doc.LayoutFieldKey(this.dataDoc)}_${fontField}`] = value; + this.dataDoc[`${Doc.LayoutDataKey(this.dataDoc)}_${fontField}`] = value; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } else { Doc.UserDoc()[fontField] = value; @@ -414,17 +414,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); }; - insertSummarizer(state: EditorState, dispatch: (tr: Transaction) => void) { - if (state.selection.empty) return false; - const mark = state.schema.marks.summarize.create(); - const { tr } = state; - tr.addMark(state.selection.from, state.selection.to, mark); - const content = tr.selection.content(); - const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); - dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); - return true; - } - vcenterToggle = () => { if (this.dataDoc) this.dataDoc.text_centered = !this.dataDoc.text_centered; else Doc.UserDoc().textCentered = !Doc.UserDoc().textCentered; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 77c00537b..26ccf6931 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,7 +1,6 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeType } from 'prosemirror-model'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { ClientUtils } from '../../../../ClientUtils'; import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; @@ -21,7 +20,6 @@ import { schema } from './schema_rts'; export class RichTextRules { public Document: Doc; public TextBox: FormattedTextBox; - public EnteringStyle: boolean = false; constructor(doc: Doc, textBox: FormattedTextBox) { this.Document = doc; this.TextBox = textBox; @@ -110,25 +108,23 @@ export class RichTextRules { new InputRule(/>>$/, (state, match, start, end) => { const numInlines = NumCast(this.Document.$inlineTextCount); this.Document.$inlineTextCount = numInlines + 1; - const inlineFieldKey = '$inline' + numInlines; // which field on the text document this annotation will write to - const inlineLayoutKey = '$layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation + const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to + const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation const textDocInline = Docs.Create.TextDocument('', { - _layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, - annotationOn: this.Document[DocData], _layout_fitWidth: true, _layout_autoHeight: true, - text_fontSize: '9px', - title: 'inline comment', }); + textDocInline.layout_fieldKey = inlineLayoutKey; + textDocInline.annotationOn = this.Document[DocData]; textDocInline.title = inlineFieldKey; // give the annotation its own title 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.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.isDataDoc = true; textDocInline.proto = this.Document[DocData]; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] - this.Document[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text - this.Document[inlineFieldKey] = ''; // set a default value for the annotation + this.Document['$' + inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text + this.Document['$' + inlineFieldKey] = ''; // set a default value for the annotation const node = state.doc.resolve(start).nodeAfter; const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); @@ -143,9 +139,8 @@ export class RichTextRules { return replaced; }), - // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%d|d)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the First-line indent node type for the selection's paragraph + new InputRule(/%d$/, (state, match, start, end) => { const pos = state.doc.resolve(start); for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); @@ -158,9 +153,8 @@ export class RichTextRules { return null; }), - // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%h|h)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the Hanging indent node type for the current selection's paragraph + new InputRule(/%h$/, (state, match, start, end) => { const pos = state.doc.resolve(start); for (let depth = pos.depth; depth >= 0; depth--) { const node = pos.node(depth); @@ -173,9 +167,8 @@ export class RichTextRules { return null; }), - // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/(%q|q)$/, (state, match, start, end) => { - if (!match[0].startsWith('%') && !this.EnteringStyle) return null; + // set the Quoted indent node type for the current selection's paragraph + new InputRule(/%q$/, (state, match, start, end) => { const pos = state.doc.resolve(start); if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) { const { node } = state.selection; @@ -292,7 +285,8 @@ export class RichTextRules { editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength)))); } - DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); + const tanchor = this.TextBox.getAnchor(true); + tanchor && DocUtils.MakeLink(tanchor, target, { link_relationship: 'portal to:portal from' }); const teditor = this.TextBox.EditorView; if (teditor && selection) { @@ -410,22 +404,6 @@ export class RichTextRules { // # heading textblockTypeInputRule(/^(#{1,6})\s$/, schema.nodes.heading, match => ({ level: match[1].length })), - // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) - new InputRule(/[ti!x]$/, (state, match, start, end) => { - if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; - - const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??'; - const node = state.doc.resolve(start).nodeAfter; - - if (node?.marks.findIndex(m => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - return node - ? state.tr - .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) - : state.tr; - }), - new InputRule(/%\(/, (state, match, start, end) => { const node = state.doc.resolve(start).nodeAfter; const sm = state.storedMarks?.slice() || []; diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 6dea891a0..eeb604b57 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -59,12 +59,15 @@ export class SummaryView { let endPos = start; const visited = new Set(); - for (let i: number = start + 1; i < view.state.doc.nodeSize - 1; i++) { + const summarized = new Set(); + const isSummary = (node: Node) => summarized.has(node) || node.marks.find(m => m.type === mtype || m.type === mtypeInc); + for (let i = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; // eslint-disable-next-line no-loop-func view.state.doc.nodesBetween(start, i, (node: Node /* , pos: number, parent: Node, index: number */) => { + isSummary(node) && Array.from(node.children).forEach(child => summarized.add(child)); if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find(m => m.type === mtype || m.type === mtypeInc)) { + if (summarized.has(node) || isSummary(node)) { visited.add(node); endPos = i + node.nodeSize - 1; } else skip = true; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index dc1e4772e..b7dae1ca3 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -118,6 +118,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.style.fontSize) return false; return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : '' }; }, }, @@ -132,16 +133,9 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { - const cstyle = getComputedStyle(dom); - if (cstyle.font) { - if (cstyle.font.indexOf('Times New Roman') !== -1) return { fontFamily: 'Times New Roman' }; - if (cstyle.font.indexOf('Arial') !== -1) return { fontFamily: 'Arial' }; - if (cstyle.font.indexOf('Georgia') !== -1) return { fontFamily: 'Georgia' }; - if (cstyle.font.indexOf('Comic Sans') !== -1) return { fontFamily: 'Comic Sans MS' }; - if (cstyle.font.indexOf('Tahoma') !== -1) return { fontFamily: 'Tahoma' }; - if (cstyle.font.indexOf('Crimson') !== -1) return { fontFamily: 'Crimson Text' }; - } - return { fontFamily: '' }; + const cstyle = dom.style.fontFamily; + if (!cstyle) return false; + return { fontFamily: cstyle }; }, }, ], @@ -155,6 +149,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.style.color) return false; return { color: dom.getAttribute('color') }; }, }, @@ -171,6 +166,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: dom => { + if (!dom.getAttribute('background-color')) return false; return { fontHighlight: dom.getAttribute('background-color') }; }, }, @@ -239,13 +235,7 @@ export const marks: { [index: string]: MarkSpec } = { { tag: 'span', getAttrs: p => { - if (typeof p !== 'string') { - const style = getComputedStyle(p); - if (style.textDecoration === 'underline') return null; - if (p.parentElement?.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement?.outerHTML.indexOf('text-decoration-style: solid') !== -1) { - return null; - } - } + if (p.getAttribute('data-summarizeInclusive')) return [[{ style: 'data-summarizeInclusive: true' }]]; return false; }, }, @@ -255,7 +245,8 @@ export const marks: { [index: string]: MarkSpec } = { return [ 'span', { - style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)', + 'data-summarizeInclusive': 'true', + style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210)', }, ]; }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index fe7b77e74..5d34afc8a 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -353,6 +353,7 @@ export const nodes: { [index: string]: NodeSpec } = { hard_break: { inline: true, group: 'inline', + marks: '_', selectable: false, parseDOM: [{ tag: 'br' }], toDOM() { |
