diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 304 |
1 files changed, 222 insertions, 82 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a76f9623a..b5aa34a29 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,7 +1,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -11,7 +12,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -20,15 +21,18 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; +import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { Networking } from '../../../Network'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; +import { IsFollowLinkScript } from '../../../util/LinkFollower'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -64,10 +68,10 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); +import { RTFMarkup } from '../../../util/RTFMarkup'; +import { List } from '../../../../fields/List'; const translateGoogleApi = require('translate-google-api'); - export const GoogleRef = 'googleDocId'; - type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer @@ -78,7 +82,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; - static _globalHighlights: string[] = ['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']; + static _globalHighlightsCache: string = ''; + static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); static _highlightStyleSheet: any = addStyleSheet(); static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); @@ -151,7 +156,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @computed get config() { this._keymap = buildKeymap(schema, this.props); - this._rules = new RichTextRules(this.props.Document, this); + this._rules = new RichTextRules(this.rootDoc, this); return { schema, plugins: [ @@ -170,6 +175,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; } + // State for GPT + @observable + private gptRes: string = ''; + public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoad = ''; public static DontSelectInitialText = false; // whether initial text should be selected or not @@ -186,13 +195,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return ''; } public static GetDocFromUrl(url: string) { - return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid + return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } constructor(props: any) { super(props); FormattedTextBox.Instance = this; - this.updateHighlights(); this._recordingStart = Date.now(); } @@ -306,11 +314,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps accumTags.push(node.attrs.fieldKey); } }); - const curTags = Object.keys(dataDoc).filter(key => key.startsWith('#')); - const added = accumTags.filter(tag => !curTags.includes(tag)); - const removed = curTags.filter(tag => !accumTags.includes(tag)); - removed.forEach(r => (dataDoc[r] = undefined)); - added.forEach(a => (dataDoc[a] = a)); + dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined; let unchanged = true; if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { @@ -454,7 +458,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps alink = alink ?? (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || - DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); + DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); @@ -540,17 +544,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // embed document when dragg marked as embed } else if (de.embedKey) { const target = dragData.droppedDocuments[0]; - target._fitContentsToBox = true; const node = schema.nodes.dashDoc.create({ width: target[WidthSym](), height: target[HeightSym](), title: 'dashDoc', - docid: target[Id], + docId: target[Id], float: 'unset', }); if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) { dragData.removeDocument?.(dragData.draggedDocuments[0]); } + target._fitContentsToBox = true; + target.context = this.rootDoc; const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); e.stopPropagation(); @@ -603,45 +608,46 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return ret; } - updateHighlights = () => { - const highlights = FormattedTextBox._globalHighlights; + updateHighlights = (highlights: string[]) => { + if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; + setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''))); clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (highlights.indexOf('Audio Tags') === -1) { + if (highlights.includes('Audio Tags')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); } - if (highlights.indexOf('Text from Others') !== -1) { + if (highlights.includes('Text from Others')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } - if (highlights.indexOf('My Text') !== -1) { + if (highlights.includes('My Text')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' }); } - if (highlights.indexOf('Todo Items') !== -1) { + if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); } - if (highlights.indexOf('Important Items') !== -1) { + if (highlights.includes('Important Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' }); } - if (highlights.indexOf('Bold Text') !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror strong > span', { 'font-size': 'large' }, ''); - addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror :not(strong > span)', { 'font-size': '0px' }, ''); + 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.indexOf('Disagree Items') !== -1) { + if (highlights.includes('Disagree Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' }); } - if (highlights.indexOf('Ignore Items') !== -1) { + if (highlights.includes('Ignore Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' }); } - if (highlights.indexOf('By Recent Minute') !== -1) { + if (highlights.includes('By Recent Minute')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { 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() })); - setTimeout(this.updateHighlights); } - if (highlights.indexOf('By Recent Hour') !== -1) { + if (highlights.includes('By Recent Hour')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { 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() })); } + this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @@ -779,23 +785,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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.indexOf(option) === -1 ? 'Highlight ' : 'Unhighlight ') + option, - event: () => { + description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, + event: action(() => { e.stopPropagation(); - if (FormattedTextBox._globalHighlights.indexOf(option) === -1) { - FormattedTextBox._globalHighlights.push(option); + if (!FormattedTextBox._globalHighlights.has(option)) { + FormattedTextBox._globalHighlights.add(option); } else { - FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1); + FormattedTextBox._globalHighlights.delete(option); } - runInAction(() => (this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join(''))); - this.updateHighlights(); - }, - icon: FormattedTextBox._globalHighlights.indexOf(option) === -1 ? 'highlighter' : 'remove-format', + }), + icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); const uicontrols: ContextMenuProps[] = []; uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: !this.Document._noSidebar ? 'eye-slash' : 'eye' }); + uicontrols.push({ + description: (this.Document._showAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', + event: () => (this.layoutDoc._showAltContentUI = !this.layoutDoc._showAltContentUI), + icon: !this.Document._showAltContentUI ? 'eye-slash' : 'eye', + }); uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ @@ -844,12 +853,72 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: !this.Document._singleLine ? 'grip-lines' : 'bars' }); - optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' }); + optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); + optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); + optionItems.push({ + description: !this.Document._singleLine ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', + event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), + icon: !this.Document._singleLine ? 'grip-lines' : 'bars', + }); + !Doc.noviceMode && optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' }); + optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; }; + animateRes = (resIndex: number) => { + if (resIndex < this.gptRes.length) { + this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex]; + setTimeout(() => { + this.animateRes(resIndex + 1); + }, 20); + } + }; + + askGPT = action(async () => { + try { + let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); + if (res) { + this.gptRes = res; + this.animateRes(0); + } + } catch (err) { + console.log(err); + this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong'; + } + }); + + generateImage = async () => { + console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text); + try { + let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text); + if (image_url) { + const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] }); + const source = Utils.prepend(result.accessPaths.agnostic.client); + const newDoc = Docs.Create.ImageDocument(source, { + x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10, + y: NumCast(this.rootDoc.y), + _height: 200, + _width: 200, + 'data-nativeWidth': result.nativeWidth, + 'data-nativeHeight': result.nativeHeight, + }); + if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + newDoc.overlayX = this.rootDoc.x; + newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + } else { + this.props.addDocument?.(newDoc); + } + // Create link between prompt and image + DocUtils.MakeLink(this.rootDoc, newDoc, { linkRelationship: 'Image Prompt' }); + } + } catch (err) { + console.log(err); + return ''; + } + }; + breakupDictation = () => { if (this._editorView && this._recording) { this.stopDictation(true); @@ -879,7 +948,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._editorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { - const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' }); + const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor', unrendered: true }); return this.addDocument(tanch) ? tanch : undefined; }; const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); @@ -955,7 +1024,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let hadStart = start !== 0; frag.forEach((node, index) => { const examinedNode = findAnchorNode(node, editor); - if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { + if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); hadStart = true; @@ -970,9 +1039,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } return undefined; } + if (node.type === this._editorView?.state.schema.nodes.dashDoc) { + if (node.attrs.docId === textAnchorId) { + return { node, start: 0 }; + } + return undefined; + } if (!node.isText) { const content = findAnchorFrag(node.content, editor); - return { node: node.copy(content.frag), start: content.start }; + if (content.frag.childCount) return { node: content.frag.childCount ? content.frag.child(0) : node, start: content.start }; } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); @@ -986,7 +1061,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const ret = findAnchorFrag(editor.state.doc.content, editor); const content = (ret.frag as any)?.content; - if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { + if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { !options.instant && (this._focusSpeed = focusSpeed); let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { @@ -997,6 +1072,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed); setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); + return focusSpeed; + } else { + return this.props.focus(this.rootDoc, options); } } }; @@ -1020,6 +1098,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps () => this.autoHeight, autoHeight => 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(), width => this.tryUpdateScrollHeight() @@ -1104,7 +1187,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.selected = reaction( () => this.props.isSelected(), action(selected => { - this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join('') : ''; + if (FormattedTextBox._globalHighlights.has('Bold Text')) { + this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed + } if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -1112,6 +1197,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); this.autoLink(); } + // Accessing editor and text doc for gpt assisted text edits + if (this._editorView && selected) { + AnchorMenu.Instance?.setEditorView(this._editorView); + AnchorMenu.Instance?.setTextDoc(this.dataDoc); + } }), { fireImmediately: true } ); @@ -1260,7 +1350,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ - view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ + view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], location: 'add:right', @@ -1273,7 +1363,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ]), ]); - const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted'); + const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } @@ -1420,7 +1510,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } componentWillUnmount() { - FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); this.unhighlightSearchTerms(); @@ -1470,22 +1559,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // but that's changed, so this shouldn't be needed. //e.stopPropagation(); // if the text box is selected, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); - document.addEventListener('pointermove', this.onSelectMove); } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } }; - onSelectMove = (e: PointerEvent) => e.stopPropagation(); onSelectEnd = (e: PointerEvent) => { document.removeEventListener('pointerup', this.onSelectEnd); - document.removeEventListener('pointermove', this.onSelectMove); }; onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; - if (!state || !editor) return; + if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1497,10 +1583,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return; } } - - if (e.button === 0 && this.props.isSelected(true) && !e.altKey) { - e.stopPropagation(); - } }; @action onDoubleClick = (e: React.MouseEvent): void => { @@ -1528,13 +1610,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); - FormattedTextBox.Focused = this; this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); this.startUndoTypingBatch(); }; - @observable public static Focused: FormattedTextBox | undefined; onClick = (e: React.MouseEvent): void => { + if (!this.props.isContentActive()) return; if ((e.nativeEvent as any).handledByInnerReactInstance) { e.stopPropagation(); return; @@ -1566,8 +1647,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events (e.nativeEvent as any).handledByInnerReactInstance = true; - if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it. - // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment) this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; @@ -1630,7 +1709,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tr && this._editorView.dispatch(tr); } } - FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -1669,8 +1747,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey) { + if ((e.altKey || e.ctrlKey) && e.key === 't') { e.preventDefault(); + e.stopPropagation(); + this.props.setTitleFocus?.(); return; } const state = this._editorView!.state; @@ -1721,6 +1801,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._ignoreScroll = true; this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; + e.stopPropagation(); + e.preventDefault(); } } }; @@ -1743,12 +1825,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => this.props.Document._fitContentsToBox; + fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox); sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); - // console.log("printting allSideBarDocs"); - // console.log(this.allSidebarDocs); return this.addDocument(doc, sidebarKey); }; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); @@ -1789,8 +1869,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ - backgroundColor: backgroundColor, - color: color, + backgroundColor, + color, opacity: annotated ? 1 : undefined, }}> <FontAwesomeIcon icon={'comment-alt'} /> @@ -1804,39 +1884,41 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps <SidebarAnnos ref={this._sidebarRef} {...this.props} - fieldKey={this.fieldKey} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - ScreenToLocalTransform={this.sidebarScreenToLocal} + usePanelWidth={true} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} showSidebar={this.SidebarShown} - PanelWidth={this.sidebarWidth} - setHeight={this.setSidebarHeight} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} removeDocument={this.removeDocument} + ScreenToLocalTransform={this.sidebarScreenToLocal} + fieldKey={this.fieldKey} + PanelWidth={this.sidebarWidth} + setHeight={this.setSidebarHeight} /> ) : ( <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> <ComponentTag - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} PanelHeight={this.props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} - scaleField={this.SidebarKey + '-scale'} + viewField={this.SidebarKey} isAnnotationOverlay={false} select={emptyFunction} + isAnyChildContentActive={returnFalse} NativeDimScaling={this.sidebarContentScaling} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.sidebarRemDocument} moveDocument={this.sidebarMoveDocument} addDocument={this.sidebarAddDocument} - CollectionView={undefined} ScreenToLocalTransform={this.sidebarScreenToLocal} renderDepth={this.props.renderDepth + 1} setHeight={this.setSidebarHeight} @@ -1854,6 +1936,49 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps </div> ); } + cycleAlternateText = () => { + if (this.layoutDoc._showAltContentUI) { + const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`]; + this.rootDoc[`_${this.props.fieldKey}-usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + } + }; + @computed get overlayAlternateIcon() { + const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`]; + return ( + <Tooltip + title={ + <div className="dash-tooltip"> + toggle (%/) between + <span style={{ color: usePath === undefined ? 'black' : undefined }}> + <em> primary, </em> + </span> + <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}> + <em>alternate, </em> + </span> + and show + <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}> + <em> alternate on hover</em> + </span> + </div> + }> + <div + className="formattedTextBox-alternateButton" + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} + style={{ + display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none', + background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', + color: usePath === undefined ? 'black' : 'white', + }}> + <FontAwesomeIcon icon="turn-up" size="sm" /> + </div> + </Tooltip> + ); + } + @computed get fieldKey() { + const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}-usePath`]); + return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `-${usePath.replace(':hover', '')}` : ''); + } + @observable _isHovering = false; render() { TraceMobx(); const active = this.props.isContentActive() || this.props.isSelected(); @@ -1870,8 +1995,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div - className="formattedTextBox-cont" - onWheel={e => this.props.isContentActive() && e.stopPropagation()} + className="formattedTextBox" + onPointerEnter={action(() => (this._isHovering = true))} + onPointerLeave={action(() => (this._isHovering = false))} + ref={r => + r?.addEventListener( + 'wheel', // 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) + (e: WheelEvent) => { + if (this.props.isContentActive()) { + if (!NumCast(this.layoutDoc._scrollTop) && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + } + }, + { passive: false } + ) + } style={{ ...(this.props.dontScale ? {} @@ -1881,6 +2019,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps width: `${100 / scale}%`, height: `${100 / scale}%`, }), + display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, transition: 'inherit', // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), @@ -1890,7 +2029,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...styleFromLayoutString, }}> <div - className={`formattedTextBox-cont`} + className="formattedTextBox-cont" ref={this._ref} style={{ overflow: this.autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an autoHeight doc in its own tab, or in the lightbox @@ -1925,13 +2064,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined, + pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined, }} /> </div> {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} {this.audioHandle} + {this.layoutDoc._showAltContentUI ? this.overlayAlternateIcon : null} </div> </div> ); |