diff options
author | srichman333 <sarah_n_richman@brown.edu> | 2023-10-10 16:21:41 -0400 |
---|---|---|
committer | srichman333 <sarah_n_richman@brown.edu> | 2023-10-10 16:21:41 -0400 |
commit | 9e91e6065333f03d3f3bf2c0d43b822d85344c78 (patch) | |
tree | 4c923fc8257b597d69700bee4c1a4e69d3cbe21a /src/client/views/nodes/formattedText/FormattedTextBox.tsx | |
parent | 368e33c076085b1b73f522ac88f548a2ad081c80 (diff) | |
parent | d929c0511cae863412a398f426d9e5b7ca64e6d9 (diff) |
merge?
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 147 |
1 files changed, 95 insertions, 52 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bd969b527..90ebf5206 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -38,7 +38,7 @@ import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; @@ -71,13 +71,14 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -const translateGoogleApi = require('translate-google-api'); +import { media_state } from '../AudioBox'; +import { setCORS } from 'google-translate-api-browser'; +// setting up cors-anywhere server address +const translate = setCORS('http://cors-anywhere.herokuapp.com/'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; -export interface FormattedTextBoxProps { - allowScroll?: boolean; -} +export interface FormattedTextBoxProps {} @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { public static LayoutString(fieldStr: string) { @@ -93,6 +94,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps static _userStyleSheet: any = addStyleSheet(); static _hadSelection: boolean = false; private _sidebarRef = React.createRef<SidebarAnnos>(); + private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; @@ -153,11 +155,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } - @computed get _recording() { - return this.dataDoc?.mediaState === 'recording'; + @computed get _recordingDictation() { + return this.dataDoc?.mediaState === media_state.Recording; } - set _recording(value) { - !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? 'recording' : undefined); + set _recordingDictation(value) { + !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); } @computed get config() { this._keymap = buildKeymap(schema, this.props); @@ -265,19 +267,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); + setTimeout(() => { const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; - DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); + let stopFunc: any; + Doc.GetProto(target).mediaState = media_state.Recording; + Doc.GetProto(target).audioAnnoState = 'recording'; + DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + let reactionDisposer = reaction( + () => target.mediaState, + action(dictation => { + if (!dictation) { + Doc.GetProto(target).audioAnnoState = 'stopped'; + stopFunc(); + reactionDisposer(); + } + }) + ); target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); } }); }; - AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); - return undefined; - }); + AnchorMenu.Instance.Highlight = undoable( + action((color: string, isLinkButton: boolean) => { + this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + return undefined; + }), + 'highlght text' + ); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -372,7 +391,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let link; LinkManager.Links(this.dataDoc).forEach((l, i) => { const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') { + if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; link = l; @@ -439,7 +458,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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 ? '...' : ''); + this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); if (str.startsWith('@') && str.length > 1) { Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc); } @@ -541,7 +560,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); - e.stopPropagation(); return true; } const dragData = de.complete.docDragData; @@ -648,7 +666,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); } if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); @@ -688,7 +706,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps toggleSidebar = (preview: boolean = false) => { const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; - else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + else { + this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + } this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; @@ -844,7 +865,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); - uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); + !Doc.noviceMode && uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && uicontrols.push({ description: 'Broadcast Message', @@ -945,14 +966,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; breakupDictation = () => { - if (this._editorView && this._recording) { + if (this._editorView && this._recordingDictation) { this.stopDictation(true); this._break = true; const state = this._editorView.state; const to = state.selection.to; const updated = TextSelection.create(state.doc, to, to); this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to)); - if (this._recording) { + if (this._recordingDictation) { this.recordDictation(); } } @@ -1242,13 +1263,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!this.props.dontRegisterView) { this._disposers.record = reaction( - () => this._recording, + () => this._recordingDictation, () => { this.stopDictation(true); - this._recording && this.recordDictation(); - } + this._recordingDictation && this.recordDictation(); + }, + { fireImmediately: true } ); - if (this._recording) setTimeout(this.recordDictation); + if (this._recordingDictation) setTimeout(this.recordDictation); } var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( @@ -1546,8 +1568,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action componentWillUnmount() { - if (this._recording) { - this._recording = !this._recording; + if (this._recordingDictation) { + this._recordingDictation = !this._recordingDictation; } Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); @@ -1561,7 +1583,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown = (e: React.PointerEvent): void => { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return e.stopPropagation(); + return; //e.stopPropagation(); } else (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); @@ -1585,7 +1607,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }); } - if (this._recording && !e.ctrlKey && e.button === 0) { + if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } this._downX = e.clientX; @@ -1593,10 +1615,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBoxComment.textBox = this; if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { - // stop propagation if not in sidebar - // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView, - // but that's changed, so this shouldn't be needed. - //e.stopPropagation(); // if the text box is selected, then it consumes all down events + // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. + e.stopPropagation(); // if the text box's content is active, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); } } @@ -1614,8 +1634,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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 }); - // !this.props.isSelected(true) && - editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + let xpos = pcords?.pos || 0; + while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { + xpos = xpos - 1; + } + editor.dispatch(state.tr.setSelection(new TextSelection(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'); @@ -1758,13 +1781,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView!.state; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - if (this.layoutDoc.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { + if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { try { - translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => { + translate(curText, { from: 'en', to: 'es' }).then((result1: any) => { setTimeout( () => - translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => { - this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0]; + translate(result1.text, { from: 'es', to: 'en' }).then((result: any) => { + const tb = this._sidebarTagRef.current as FormattedTextBox; + tb._editorView?.dispatch(tb._editorView!.state.tr.insertText(result1.text + '\r\n\r\n' + result.text)); }), 1000 ); @@ -1802,7 +1826,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - e.stopPropagation(); + let stopPropagation = true; for (var i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) { @@ -1821,6 +1845,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps case 'Tab': e.preventDefault(); break; + case 'c': + this._editorView?.state.selection.empty && (stopPropagation = false); + break; default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': @@ -1830,6 +1857,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } break; } + if (stopPropagation) e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1887,7 +1915,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); @computed get audioHandle() { - return !this._recording ? null : ( + return !this._recordingDictation ? null : ( <div className="formattedTextBox-dictation" onPointerDown={e => @@ -1896,7 +1924,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e, returnFalse, emptyFunction, - action(e => (this._recording = !this._recording)) + action(e => (this._recordingDictation = !this._recordingDictation)) ) }> <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> @@ -1948,6 +1976,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> <ComponentTag {...this.props} + ref={this._sidebarTagRef as any} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} @@ -1970,14 +1999,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps fitContentsToBox={this.fitContentsToBox} noSidebar={true} treeViewHideTitle={true} - fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} + fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> ); }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} + {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))} </div> ); } @@ -2025,9 +2054,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { + if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { + if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { + // if the scrolled freeform is a child of the sidebar component, we need to let the event go through + // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling + e.preventDefault(); + } + return; + } + // 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() && !this.props.allowScroll) { - if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault(); + if (this.props.isContentActive()) { + // prevent default if selected || child is active but this doc isn't scrollable + if ( + (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(this.props.PanelHeight()) && // + (this.props.isSelected() || this.isAnyChildContentActive()) + ) { + e.preventDefault(); + } e.stopPropagation(); } }; @@ -2084,7 +2128,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ref={this._ref} style={{ cursor: this.props.isContentActive() ? 'text' : undefined, - overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', }} @@ -2098,10 +2141,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer`} + className="formattedTextBox-outer" ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2119,9 +2162,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps /> </div> {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? null : this.sidebarHandle} + {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} - {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null} + {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> </div> ); |