diff options
Diffstat (limited to 'src/client/views/nodes')
37 files changed, 1196 insertions, 1218 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 8a38ef663..c685ec66f 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -22,6 +22,7 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent'; import './AudioBox.scss'; import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; +import { OpenWhere } from './DocumentView'; /** * AudioBox @@ -383,7 +384,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { newDoc.overlayY = NumCast(this.Document.y) + NumCast(this.layoutDoc._height); Doc.AddToMyOverlay(newDoc); } else { - this._props.addDocument?.(newDoc); + this._props.addDocTab(newDoc, OpenWhere.addRight); } }), false @@ -606,7 +607,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <div className="audiobox-file" style={{ - pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none', flexDirection: this.miniPlayer ? 'row' : 'column', justifyContent: this.miniPlayer ? 'flex-start' : 'space-between', }}> @@ -662,9 +662,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { max="1" value={this._muted ? 0 : this._volume} className="toolbar-slider volume" - onPointerDown={(e: React.PointerEvent) => { - e.stopPropagation(); - }} + onPointerDown={e => e.stopPropagation()} onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} /> </div> @@ -691,8 +689,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { value={this.timeline?._zoomFactor ?? 1} className="toolbar-slider" id="zoom-slider" - onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} - onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))} + onPointerDown={e => e.stopPropagation()} + onChange={e => this.zoom(Number(e.target.value))} /> </div> )} @@ -752,7 +750,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { render() { return ( - <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined }}> + <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none' }}> {!this.path ? this.recordingControls : this.playbackControls} </div> ); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 2800ea200..0d0a7c623 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { OmitKeys, numberRange } from '../../../Utils'; @@ -17,6 +17,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldViewProps } from './FieldView'; +import { TransitionTimer } from '../../../fields/DocSymbols'; /// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface @@ -91,6 +92,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @observable AutoDim = this.props.autoDim; @observable Transition = this.props.transition; + componentDidMount(): void { + if (this.props.transition && !this.Document[TransitionTimer]) { + const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]); + this.Document[TransitionTimer] = setTimeout( + action(() => (this.Document[TransitionTimer] = this.Transition = undefined)), + num + ); + } + } + componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) { super.componentDidUpdate(prevProps); this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); @@ -98,14 +109,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes - DataTransition = () => this._props.transition; // prettier-ignore + DataTransition = () => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (doc === this.layoutDoc) { - switch (property) { + switch (property.split(':')[0]) { case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.BackgroundColor; case StyleProp.Color: return this.Color; @@ -224,7 +235,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF // undefined - this is not activated by a group isGroupActive = () => { if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; - const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + const backColor = this.BackgroundColor; + const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent'); return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; render() { @@ -237,7 +249,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this.PanelWidth(), height: this.PanelHeight(), transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`, - transition: this.Transition || StrCast(this.Document.dataTransition), + transition: this.DataTransition(), zIndex: this.ZIndex, display: this.Width ? undefined : 'none', }}> @@ -251,6 +263,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} /> )} </div> diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 715b23fb6..9ffdc350d 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -69,8 +69,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() action((e, doubleTap) => { if (doubleTap) { this._isAnyChildContentActive = true; - if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); - if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); } }), false, @@ -131,8 +131,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return false; }; - whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); - closeDown = (e: React.PointerEvent, which: string) => { setupMoveUpEvents( this, @@ -159,6 +157,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); + + /** + * Tests for whether a comparison box slot (ie, before or after) has renderable text content + * @param whichSlot field key for start or end slot + * @returns a JSX layout string if a text field is found, othwerise undefined + */ + testForTextFields = (whichSlot: string) => { + const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string'; + const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); + const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim(); + const layoutTemplateString = + slotHasText ? FormattedTextBox.LayoutString(whichSlot): + whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) : + altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore + + // A bit hacky to try out the concept of using GPT to fill in flashcards + // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) + // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>). + // eg., this.text_alternate is + // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" + // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field + // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2) + if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) { + var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... + if (queryText && queryText.match(/\(\(.*\)\)/)) { + KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt + } + } + return layoutTemplateString; + }; + _closeRef = React.createRef<HTMLDivElement>(); render() { const clearButton = (which: string) => { @@ -176,48 +205,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() /** * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case * where if there are no Docs in the slots, but the main fieldKey contains text, then - * @param which + * @param whichSlot * @returns */ - const displayDoc = (which: string) => { - const whichDoc = DocCast(this.dataDoc[which]); + const displayDoc = (whichSlot: string) => { + const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim(); - // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot - // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot - const layoutTemplateString = !targetDoc - ? which.endsWith('1') && subjectText !== undefined - ? FormattedTextBox.LayoutString(this.fieldKey) - : which.endsWith('2') && (this.Document[which] instanceof RichTextField || typeof this.Document[which] === 'string') - ? FormattedTextBox.LayoutString(which) - : undefined - : undefined; - - // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic. - // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string) - // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally) - // by the field references in the text (eg., this.text_alternate is - // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))" - // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field - // A GPT call will put the "answer" in the second slot of the comparison (eg., text_2) - if (which.endsWith('2') && !layoutTemplateString && !targetDoc) { - var queryText = RTFCast(this.Document[this.fieldKey + '_alternate']) - ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... - .trim(); - if (subjectText && queryText.match(/\(\(.*\)\)/)) { - KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt - } - } + const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot); return targetDoc || layoutTemplateString ? ( <> <DocumentView {...this._props} + ignoreUsePath={layoutTemplateString ? true : undefined} renderDepth={this.props.renderDepth + 1} LayoutTemplateString={layoutTemplateString} Document={layoutTemplateString ? this.Document : targetDoc} containerViewPath={this.DocumentView?.().docViewPath} - moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2} - removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2} + moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2} + removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={returnZero} NativeHeight={returnZero} isContentActive={emptyFunction} @@ -227,7 +232,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() hideLinkButton={true} pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> - {layoutTemplateString ? null : clearButton(which)} + {layoutTemplateString ? null : clearButton(whichSlot)} </> // placeholder image if doc is missing ) : ( <div className="placeholder"> diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index b4da41fa0..60c5fdba2 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -120,8 +120,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { - const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); - const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes))); + const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); + const changedAxes = data.config_dataVizAxes && this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes))); this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows); this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; @@ -268,7 +268,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); this._disposers.datavis = reaction( () => { - if (this.layoutDoc.dataViz_schemaLive==undefined) this.layoutDoc.dataViz_schemaLive = true; + if (this.layoutDoc.dataViz_schemaLive == undefined) this.layoutDoc.dataViz_schemaLive = true; const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); if (!keys) return; @@ -285,42 +285,43 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im }); current.push(row); }); - if (!this.layoutDoc._dataViz_schemaOG){ // makes a copy of the original table for the "live" toggle - let csvRows = []; - csvRows.push(keys.join(',')); - for (let i = 0; i < children.length-1; i++) { - let eachRow = []; - for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); - eachRow.push(cell); - } - csvRows.push(eachRow); + if (!this.layoutDoc._dataViz_schemaOG) { + // makes a copy of the original table for the "live" toggle + let csvRows = []; + csvRows.push(keys.join(',')); + for (let i = 0; i < children.length - 1; i++) { + let eachRow = []; + for (let j = 0; j < keys.length; j++) { + var cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + eachRow.push(cell); } - const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); - const options = { x: 0, y: 0, title: 'schemaTable for static dataviz', _width: 300, _height: 100, type: 'text/csv' }; - const file = new File([blob], 'schemaTable for static dataviz', options); - const loading = Docs.Create.LoadingDocument(file, options); - DocUtils.uploadFileToDoc(file, {}, loading); - this.layoutDoc._dataViz_schemaOG = loading; - } - const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc - const ogHref = CsvCast(ogDoc[this.fieldKey])? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; - const href = CsvCast(this.Document[this.fieldKey]).url.href - if (ogHref && !DataVizBox.datasetSchemaOG.has(href)){ // sets original dataset to the var - const lastRow = current.pop(); - DataVizBox.datasetSchemaOG.set(href, current); - current.push(lastRow!); - fetch('/csvData?uri=' + ogHref) - .then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res)))); + csvRows.push(eachRow); } + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x: 0, y: 0, title: 'schemaTable for static dataviz', _width: 300, _height: 100, type: 'text/csv' }; + const file = new File([blob], 'schemaTable for static dataviz', options); + const loading = Docs.Create.LoadingDocument(file, options); + DocUtils.uploadFileToDoc(file, {}, loading); + this.layoutDoc._dataViz_schemaOG = loading; + } + const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc; + const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; + const href = CsvCast(this.Document[this.fieldKey]).url.href; + if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) { + // sets original dataset to the var + const lastRow = current.pop(); + DataVizBox.datasetSchemaOG.set(href, current); + current.push(lastRow!); + fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res)))); + } return current; }, current => { if (current) { - const href = CsvCast(this.Document[this.fieldKey]).url.href; - if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); - else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); + const href = CsvCast(this.Document[this.fieldKey]).url.href; + if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); + else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); } }, { fireImmediately: true } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 07e179246..e729e2fa2 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -7,7 +7,7 @@ import { OmitKeys, Without, emptyPath } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; -import { Cast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; @@ -128,15 +128,15 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString; if (!this.layoutDoc) return '<p>awaiting layout</p>'; if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString()); - const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); + const tempLayout = DocCast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')]); + const layoutDoc = tempLayout ?? this.layoutDoc; + const layout = Cast(layoutDoc[layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(layoutDoc.layout_fieldKey, 'layout')], 'string'); if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); if (typeof layout === 'string') return layout; return '<p>Loading layout</p>'; } get layoutDoc() { - // bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script - // const template: Doc = this._props.LayoutTemplate?.() || Doc.Layout(this._props.Document, this._props.fieldKey ? Cast(this._props.Document[this._props.fieldKey], Doc, null) : undefined); const template: Doc = this._props.LayoutTemplate?.() || (this._props.LayoutTemplateString && this._props.Document) || @@ -156,6 +156,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte 'LayoutTemplate', 'layoutFieldKey', 'dontCenter', + 'DataTransition', 'contextMenuItems', //'onClick', // don't need to omit this since it will be set 'onDoubleClickScript', diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 5421c1b50..23dada260 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -234,6 +234,12 @@ } } +.documntViewInternal-dropdown { + > div { + transform-origin: top left !important; + } +} + .contentFittingDocumentView { position: relative; display: flex; @@ -256,6 +262,5 @@ .documentView-node:first-child { position: relative; - background: '#B59B66'; //$white; } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9848f18e0..fc2da18d9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -10,6 +10,7 @@ import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fi import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { PrefetchProxy } from '../../../fields/Proxy'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -19,10 +20,11 @@ import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocOptions, DocUtils, Docs } from '../../documents/Documents'; +import { DocUtils, Docs } from '../../documents/Documents'; import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; +import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -36,6 +38,7 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent, ViewBoxInterface } from '../DocComponent'; import { EditableView } from '../EditableView'; +import { FieldsDropdown } from '../FieldsDropdown'; import { GestureOverlay } from '../GestureOverlay'; import { LightboxView } from '../LightboxView'; import { AudioAnnoState, StyleProp } from '../StyleProvider'; @@ -47,7 +50,6 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; -import { FieldsDropdown } from '../FieldsDropdown'; interface Window { MediaRecorder: MediaRecorder; } @@ -102,12 +104,11 @@ export interface DocumentViewProps extends FieldViewSharedProps { childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. dragWhenActive?: boolean; dontHideOnDrag?: boolean; - suppressSetHeight?: boolean; onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected DataTransition?: () => string | undefined; NativeWidth?: () => number; NativeHeight?: () => number; - contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; @@ -199,6 +200,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document ((!this.disableClickScriptFunc && // this.onClickHandler && !this._props.onBrowseClickScript?.() && + !this.layoutDoc.layout_isSvg && this.isContentActive() !== true) || this.isContentActive() === false) ? 'none' @@ -221,7 +223,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document)) ); } - @computed get _allLinks() { + @computed get _allLinks(): Doc[] { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document); } @@ -558,7 +560,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); - this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp })); + this._props + .contextMenuItems?.() + .forEach( + item => + item.label && + cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp }) + ); if (!this.Document.isFolder) { const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null); @@ -595,7 +603,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.Document.annotationOn) { onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); @@ -808,8 +816,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; /** * displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by - * setting layout_showTitle using the format: field1[;field2[...][:hover]] - * from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] + * setting layout_showTitle using the format: field1[:hover] **/ @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; @@ -836,7 +843,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> - {!dropdownWidth ? null : <div style={{ width: dropdownWidth }}>{this.fieldsDropdown(showTitle)}</div>} + {!dropdownWidth ? null : ( + <div className="documntViewInternal-dropdown" style={{ width: dropdownWidth }}> + {this.fieldsDropdown(showTitle)} + </div> + )} <div style={{ width: `calc(100% - ${dropdownWidth}px)`, @@ -849,21 +860,26 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document contents={ showTitle .split(';') - .map(field => Field.toString(targetDoc[field.trim()] as Field)) + .map(field => Field.toJavascriptString(this.Document[field] as Field)) .join(' \\ ') || '-unset-' } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} - GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))} + GetValue={() => + showTitle + .split(';') + .map(field => Field.toKeyValueString(this.Document, field)) + .join('\\') + } SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { + if (input?.startsWith('$')) { if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = input?.substring(1); + this.layoutDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; + Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title'; } - } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { + } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') { KeyValueBox.SetField(targetDoc, showTitle, input); } return true; @@ -890,6 +906,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document fieldKey={this.layout_showCaption} styleProvider={this.captionStyleProvider} dontRegisterView={true} + rootSelected={this.rootSelected} noSidebar={true} dontScale={true} renderDepth={this._props.renderDepth} @@ -907,7 +924,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document : this.docContents ?? ( <div className="documentView-node" - id={this.Document[Id]} + id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined} style={{ ...style, background: this.backgroundBoxColor, @@ -1051,8 +1068,18 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; private _animEffectTimer: NodeJS.Timeout | undefined; - public Guid = Utils.GenerateGuid(); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. - + /** + * This is used to create an id for tracking a Doc. Since the Doc can be in a regular view and in the lightbox at + * the same time, this creates a different version of the id depending on whether the search scope will be in the lightbox or not. + * @param inLightbox is the id scoped to the lightbox + * @param id the id + * @returns + */ + public static UniquifyId(inLightbox: boolean | undefined, id: string) { + return (inLightbox ? 'lightbox-' : '') + id; + } + public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. + public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]); @computed public static get exploreMode() { return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); } @@ -1271,6 +1298,32 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined); }, 'set custom view'); + public static setDefaultTemplate(checkResult?: boolean) { + if (checkResult) { + return Doc.UserDoc().defaultTextLayout; + } + const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined; + undoable(() => { + var tempDoc: Opt<Doc>; + if (view) { + if (!view.layoutDoc.isTemplateDoc) { + tempDoc = view.Document; + MakeTemplate(tempDoc); + Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); + Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc)); + tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc); + } else { + tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]); + if (!tempDoc) { + tempDoc = view.Document; + while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto); + } + } + } + Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; + }, 'set default template')(); + } + /** * This switches between the current view of a Doc and a specified alternate layout view. * The current view of the Doc is stored in the layout_default field so that it can be restored. @@ -1382,7 +1435,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 50d4c7c78..a557cff4f 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -1,17 +1,17 @@ import { action, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { DivHeight, DivWidth } from '../../../Utils'; import { Id } from '../../../fields/FieldSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { Docs } from '../../documents/Documents'; +import { DocUtils, Docs } from '../../documents/Documents'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import EquationEditor from './formattedText/EquationEditor'; -import { DivHeight, DivWidth } from '../../../Utils'; @observer export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -80,7 +80,9 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { _height: 300, backgroundColor: 'white', }); + const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' }); this._props.addDocument?.(graph); + link && this._props.addDocument?.(link); e.stopPropagation(); } if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document); @@ -94,11 +96,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc._nativeWidth) { // if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio const prevNwidth = NumCast(this.layoutDoc._nativeWidth); - const prevNheight = NumCast(this.layoutDoc._nativeHeight); - this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', ''))); - this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', ''))); + const newNwidth = (this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', '')))); + const newNheight = (this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', '')))); this.layoutDoc._width = (NumCast(this.layoutDoc._width) * NumCast(this.layoutDoc._nativeWidth)) / prevNwidth; - this.layoutDoc._height = (NumCast(this.layoutDoc._height) * NumCast(this.layoutDoc._nativeHeight)) / prevNheight; + this.layoutDoc._height = (NumCast(this.layoutDoc._width) * newNheight) / newNwidth; } else { this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', ''))); this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', ''))); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 4ecaaa283..771856788 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -55,6 +55,7 @@ export interface FieldViewSharedProps { ignoreAutoHeight?: boolean; disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors + ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs) CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document @@ -91,6 +92,7 @@ export interface FieldViewSharedProps { waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; + suppressSetHeight?: boolean; } /** diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f02ad7300..57ae92359 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -5,8 +5,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; -import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -61,23 +60,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { } @observable noTooltip = false; - showTemplate = (): void => { - const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight); - }; - dragAsTemplate = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - }; - useAsPrototype = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); - }; + showTemplate = (dragFactory: Doc) => this._props.addDocTab(dragFactory, OpenWhere.addRight); specificContextMenu = (): void => { - if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) { - const cm = ContextMenu.Instance; - cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' }); - cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' }); + const dragFactory = DocCast(this.layoutDoc.dragFactory); + if (!Doc.noviceMode && dragFactory) { + ContextMenu.Instance.addItem({ description: 'Show Template', event: () => this.showTemplate(dragFactory), icon: 'tag' }); } }; diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 2e7a2120e..a86bdbd79 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -7,12 +7,13 @@ import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { Docs } from '../../documents/Documents'; +import { DocUtils, Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; +import { LinkManager } from '../../util/LinkManager'; @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -33,7 +34,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> componentDidMount() { this._props.setContentViewBox?.(this); reaction( - () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange], + () => [this.graphFuncs, this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange], () => this.createGraph() ); } @@ -45,11 +46,26 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> if (addAsAnnotation) this.addDocument(anchor); return anchor; }; + @computed get graphFuncs() { + const links = LinkManager.Instance.getAllRelatedLinks(this.Document) + .map(d => LinkManager.getOppositeAnchor(d, this.Document)) + .filter(d => d) + .map(d => d!); + const funcs = links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc => + StrCast(doc.text, 'x^2') + .replace(/\\sqrt/g, 'sqrt') + .replace(/\\frac\{(.*)\}\{(.*)\}/g, '($1/$2)') + .replace(/\\left/g, '') + .replace(/\\right/g, '') + .replace(/\{/g, '') + .replace(/\}/g, '') + ); + return funcs; + } createGraph = (ele?: HTMLDivElement) => { this._plotEle = ele || this._plotEle; const width = this._props.PanelWidth(); const height = this._props.PanelHeight(); - const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)')); try { this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]); this._plot = functionPlot({ @@ -59,7 +75,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) }, yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) }, grid: true, - data: fns.map(fn => ({ + data: this.graphFuncs.map(fn => ({ fn, // derivative: { fn: "2 * x", updateOnMouseMove: true } })), @@ -72,7 +88,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { - const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true); + const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => { + ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc); + if (res) { + const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' }); + link && this._props.addDocument?.(link); + } + return res; + }, true); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; @@ -104,7 +127,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> {this.theGraph} <div style={{ - display: this._props.isSelected() ? 'none' : undefined, + display: this._props.isContentActive() ? 'none' : undefined, position: 'absolute', width: '100%', height: '100%', diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 251235b93..469869e21 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -9,7 +9,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; @@ -29,6 +29,12 @@ import { OpenWhere } from './DocumentView'; import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; +import { Colors } from 'browndash-components'; +import { listSpec } from '../../../fields/Schema'; +import { List } from '../../../fields/List'; +import { url } from 'inspector'; +import { OverlayView } from '../OverlayView'; +import { Networking } from '../../Network'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -134,7 +140,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); }, true); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; @@ -159,8 +165,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @undoBatch setNativeSize = action(() => { - const scaling = (this.DocumentView?.().screenToViewTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1); - const nscale = NumCast(this._props.PanelWidth()) / scaling; + const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1); const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; @@ -259,7 +264,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -297,7 +302,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -311,13 +316,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts - .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) - .filter(url => url) - .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images + const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png')); + const altpaths = + alts + ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl)) + .filter(url => url) + .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')]; + return paths.length ? paths : [defaultUrl.href]; } @observable _error = ''; @@ -326,7 +333,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get content() { TraceMobx(); - const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)); + const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) ?? Colors.WHITE); const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1; const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); @@ -370,6 +377,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { + if (!this.dataDoc[this.fieldKey]) return this.chooseImage(); if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, @@ -468,4 +476,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl </div> ); } + + public chooseImage = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*'; + input.onchange = async _e => { + const file = input.files?.[0]; + if (file) { + const disposer = OverlayView.ShowSpinner(); + const [{ result }] = await Networking.UploadFilesToServer({ file }); + if (result instanceof Error) { + alert('Error uploading files - possibly due to unsupported file types'); + } else { + this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); + !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); + } + disposer(); + } else { + console.log('No file selected'); + } + }; + input.click(); + }; } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 2bcad806f..31a2367fc 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -55,7 +55,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { @observable _splitPercentage = 50; get fieldDocToLayout() { - return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document; + return DocCast(this._props.Document); } @action @@ -105,17 +105,18 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { default: { const _setCacheResult_ = (value: FieldResult) => { field = value as Field; - setResult?.(value); + if (setResult) setResult?.(value); + else target[key] = field; }; const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log); if (!res.success) { if (key) target[key] = script.originalScript; return false; } - field === undefined && (field = res.result); + field === undefined && (field = res.result instanceof Array ? new List<any>(res.result) : res.result); } } - if (!key) return field; + if (!key) return false; if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) { target[key] = field; return true; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index d59489a78..f9e8ce4f3 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)} + SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} /> </div> </td> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index be20b5934..74e78c671 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get Title() { - return Field.toString(this.dataDoc[this.fieldKey] as Field); + return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title); } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 6e4d0e92a..3a2509c3d 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,18 +2,20 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; +import { FieldResult } from '../../../fields/Doc'; import { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; -import { LinkManager } from '../../util/LinkManager'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; +import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; @@ -22,8 +24,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } - disposer: IReactionDisposer | undefined; - @observable _forceAnimate = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor + _disposers: { [name: string]: IReactionDisposer } = {}; + @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor constructor(props: FieldViewProps) { @@ -38,45 +40,48 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; + _hackToSeeIfDeleted: any; componentWillUnmount() { - this.disposer?.(); + this._hackToSeeIfDeleted && clearTimeout(this._hackToSeeIfDeleted); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); } componentDidMount() { this._props.setContentViewBox?.(this); - this.disposer = reaction( - () => ({ drag: SnappingManager.IsDragging }), - ({ drag }) => { - !LightboxView.Contains(this.DocumentView?.()) && - setTimeout( - // need to wait for drag manager to set 'hidden' flag on dragged DOM elements - action(() => { - const a = this.anchor1, - b = this.anchor2; - let a1 = a && document.getElementById(a.Guid); - let a2 = b && document.getElementById(b.Guid); - // test whether the anchors themselves are hidden,... - if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; - else { - // .. or whether and of their DOM parents are hidden - for (; a1 && !a1.hidden; a1 = a1.parentElement); - for (; a2 && !a2.hidden; a2 = a2.parentElement); - this._hide = a1 || a2 ? true : false; - } - }) - ); - }, - { fireImmediately: true } + this._disposers.deleting = reaction( + () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())), + empty => empty && ((this._hackToSeeIfDeleted = setTimeout(() => + (!this.anchor1 && !this.anchor2) && this._props.removeDocument?.(this.Document) + )), 1000) // prettier-ignore + ); + this._disposers.dragging = reaction( + () => SnappingManager.IsDragging, + () => setTimeout( action(() => {// need to wait for drag manager to set 'hidden' flag on dragged DOM elements + const a = this.anchor1, + b = this.anchor2; + let a1 = a && document.getElementById(a.ViewGuid); + let a2 = b && document.getElementById(b.ViewGuid); + // test whether the anchors themselves are hidden,... + if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; + else { + // .. or whether any of their DOM parents are hidden + for (; a1 && !a1.hidden; a1 = a1.parentElement); + for (; a2 && !a2.hidden; a2 = a2.parentElement); + this._hide = a1 || a2 ? true : false; + } + })) // prettier-ignore ); } render() { + TraceMobx(); + if (this._hide) return null; const a = this.anchor1; const b = this.anchor2; this._forceAnimate; const docView = this._props.docViewPath().lastElement(); - if (a && b && !LightboxView.Contains(docView)) { + if (a && b) { // text selection bounds are not directly observable, so we have to // force an update when anything that could affect them changes (text edits causing reflow, scrolling) a.Document[a.LayoutFieldKey]; @@ -92,18 +97,39 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) + var foundParent = false; + const getAnchor = (field: FieldResult): Element[] => { + const docField = DocCast(field); + const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField; + const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id])); + if (ele?.className === 'linkBox-label') foundParent = true; + if (ele?.getBoundingClientRect().width) return [ele]; + const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width); + const annoOn = DocCast(doc.annotationOn); + if (eles.length || !annoOn) return eles; + const pareles = getAnchor(annoOn); + foundParent = pareles.length ? true : false; + return pareles; + }; // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>), // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement(); - const targetBhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement(); + const targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1); + const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2); - const aid = targetAhyperlink?.id || a.Document[Id]; - const bid = targetBhyperlink?.id || b.Document[Id]; - if (!document.getElementById(aid) || !document.getElementById(bid)) { + const container = this.DocumentView?.().containerViewPath?.().lastElement()?.ContentDiv; + const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id; + const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id; + if (!aid || !bid) { setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); return null; } + if (foundParent) { + setTimeout( + action(() => (this._forceAnimate = this._forceAnimate + 0.01)), + 1 + ); + } if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); @@ -149,6 +175,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { color={color} labels={ <div + id={this.DocumentView?.().DocUniqueId} + className={'linkBox-label'} style={{ borderRadius: '8px', pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, @@ -192,6 +220,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { </> ); } + + setTimeout( + action(() => (this._forceAnimate = this._forceAnimate + 1)), + 2 + ); return ( <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 1645d0813..2a96ce458 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -69,8 +69,10 @@ export class LinkDescriptionPopup extends React.Component<{}> { }}> <input className="linkDescriptionPopup-input" - onKeyDown={e => e.stopPropagation()} - onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)} + onKeyDown={e => { + e.key === 'Enter' && this.onDismiss(true); + e.stopPropagation(); + }} value={this.description} placeholder={this.description || '(Optional) Enter link description...'} onChange={e => this.descriptionChanged(e)} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index ae25ff179..c9c8f9260 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -172,6 +172,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps if (nextHrefInd !== this._hrefInd) { this._linkDoc = undefined; this._hrefInd = nextHrefInd; + this.updateHref(); } }), true @@ -184,8 +185,9 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this._props.hrefs?.length) { const webDoc = - Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()).lastElement() ?? - Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); + Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()) + .filter(doc => doc.type === DocumentType.WEB) + .lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); DocumentManager.Instance.showDocument(webDoc, { openLocation: OpenWhere.lightbox, willPan: true, diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 927e6fad4..b73898f59 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -6,7 +6,7 @@ import { IconButton, Size, Type } from 'browndash-components'; import * as d3 from 'd3'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl'; -import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { CirclePicker, ColorResult } from 'react-color'; @@ -14,7 +14,6 @@ import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc'; -import { DocCss, Highlight } from '../../../../fields/DocSymbols'; import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { DocUtils, Docs } from '../../../documents/Documents'; @@ -28,7 +27,7 @@ import { SidebarAnnos } from '../../SidebarAnnos'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; @@ -53,7 +52,6 @@ import { MarkerIcons } from './MarkerIcons'; * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps */ -const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ'; const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/'; @@ -66,72 +64,69 @@ type PopupInfo = { description: string; }; -// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { -// mapboxAccessToken: string; -// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; -// position: ControlPosition; - -// onResult: (...args: any[]) => void; -// }; - -type MapMarker = { - longitude: number; - latitude: number; -}; - -/** - * Consider integrating later: allows for drawing, circling, making shapes on map - */ -// const drawingManager = new window.google.maps.drawing.DrawingManager({ -// drawingControl: true, -// drawingControlOptions: { -// position: google.maps.ControlPosition.TOP_RIGHT, -// drawingModes: [ -// google.maps.drawing.OverlayType.MARKER, -// // currently we are not supporting the following drawing mode on map, a thought for future development -// google.maps.drawing.OverlayType.CIRCLE, -// google.maps.drawing.OverlayType.POLYLINE, -// ], -// }, -// }); - @observer export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } - private _dragRef = React.createRef<HTMLDivElement>(); + private _unmounting = false; private _sidebarRef = React.createRef<SidebarAnnos>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _mapRef: React.RefObject<MapRef> = React.createRef(); private _disposers: { [key: string]: IReactionDisposer } = {}; - private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); constructor(props: FieldViewProps) { super(props); makeObservable(this); } - @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } + @observable _featuresFromGeocodeResults: any[] = []; + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @observable _selectedPinOrRoute: Doc | undefined = undefined; // The pin that is selected + @observable _mapReady = false; + @observable _isAnimating: boolean = false; + @observable _routeToAnimate: Doc | undefined = undefined; + @observable _animationPhase: number = 0; + @observable _finishedFlyTo: boolean = false; + @observable _frameId: number | null = null; + @observable _animationUtility: AnimationUtility | null = null; + @observable _settingsOpen: boolean = false; + @observable _mapStyle: string = 'mapbox://styles/mapbox/standard'; + @observable _showTerrain: boolean = true; + @observable _currentPopup: PopupInfo | undefined = undefined; + @observable _isStreetViewAnimation: boolean = false; + @observable _animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; + @observable _animationLineColor: string = '#ffff00'; + @observable _temporaryRouteSource: FeatureCollection = { type: 'FeatureCollection', features: [] }; + @observable _dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = { + type: 'Feature', + properties: {}, + geometry: { type: 'LineString', coordinates: [] }, + }; + + @observable path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { + type: 'Feature', + geometry: { type: 'LineString', coordinates: [] }, + properties: {}, + }; + // this list contains pushpins and configs - @computed get allAnnotations() { - return DocListCast(this.dataDoc[this.annotationKey]); - } - @computed get allPushpins() { - return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); - } - @computed get allRoutes() { - return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } //prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } //prettier-ignore + @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } //prettier-ignore + @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } //prettier-ignore + @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } //prettier-ignore + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } //prettier-ignore + @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } //prettier-ignore + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> { - if (this.routeToAnimate?.routeCoordinates) { - const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); + if (this._routeToAnimate?.routeCoordinates) { + const originalCoordinates: Position[] = JSON.parse(StrCast(this._routeToAnimate.routeCoordinates)); // const index = Math.floor(this.animationPhase * originalCoordinates.length); - const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index - console.log('Animation phase', this.animationPhase); + const index = this._animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index + console.log('Animation phase', this._animationPhase); const startIndex = Math.floor(index); const endIndex = Math.ceil(index); let feature: Feature<Geometry, GeoJsonProperties>; @@ -147,7 +142,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem feature = { type: 'Feature', properties: { - routeTitle: StrCast(this.routeToAnimate.title), + routeTitle: StrCast(this._routeToAnimate.title), }, geometry: geometry, }; @@ -158,9 +153,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const fraction = index - startIndex; const interpolator = d3.interpolateArray(startCoord, endCoord); - const interpolatedCoord = interpolator(fraction); - const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]); geometry = { @@ -170,14 +163,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem feature = { type: 'Feature', properties: { - routeTitle: StrCast(this.routeToAnimate.title), + routeTitle: StrCast(this._routeToAnimate.title), }, geometry: geometry, }; } autorun(() => { - const animationUtil = this.animationUtility; + const animationUtil = this._animationUtility; const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex)); const newFeature: Feature<LineString, turf.Properties> = { type: 'Feature', @@ -204,11 +197,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; } @computed get selectedRouteCoordinates(): Position[] { - let coordinates: Position[] = []; - if (this.routeToAnimate?.routeCoordinates) { - coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates)); - } - return coordinates; + return !this._routeToAnimate?.routeCoordinates ? [] : JSON.parse(StrCast(this._routeToAnimate.routeCoordinates)); } @computed get allRoutesGeoJson(): FeatureCollection { @@ -233,29 +222,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; } - @computed get SidebarShown() { - return this.layoutDoc._layout_showSidebar ? true : false; - } - @computed get sidebarWidthPercent() { - return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - @computed get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - componentDidMount() { this._unmounting = false; this._props.setContentViewBox?.(this); } - _unmounting = false; - componentWillUnmount(): void { + componentWillUnmount() { this._unmounting = true; this.deselectPinOrRoute(); - this._rerenderTimeout && clearTimeout(this._rerenderTimeout); Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); } @@ -269,7 +243,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPinOrRoute; + let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this._selectedPinOrRoute; if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); } @@ -370,10 +344,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const sourceAnchorCreator = action(() => { const note = this.getAnchor(true); - if (note && this.selectedPinOrRoute) { - note.latitude = this.selectedPinOrRoute.latitude; - note.longitude = this.selectedPinOrRoute.longitude; - note.map = this.selectedPinOrRoute.map; + if (note && this._selectedPinOrRoute) { + note.latitude = this._selectedPinOrRoute.latitude; + note.longitude = this._selectedPinOrRoute.longitude; + note.map = this._selectedPinOrRoute.map; } return note as Doc; }); @@ -399,10 +373,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const createFunc = undoable( action(() => { const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); - if (note && this.selectedPinOrRoute) { - note.latitude = this.selectedPinOrRoute.latitude; - note.longitude = this.selectedPinOrRoute.longitude; - note.map = this.selectedPinOrRoute.map; + if (note && this._selectedPinOrRoute) { + note.latitude = this._selectedPinOrRoute.latitude; + note.longitude = this._selectedPinOrRoute.longitude; + note.map = this._selectedPinOrRoute.map; } }), 'create note annotation' @@ -423,12 +397,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return false; }; - setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); - pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); @@ -439,52 +408,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; savedAnnotations = () => this._savedAnnotations; - _bingSearchManager: any; - _bingMap: any; - get MicrosoftMaps() { - return (window as any).Microsoft.Maps; - } - // uses Bing Search to retrieve lat/lng for a location. eg., - // const results = this.geocodeQuery(map.map, 'Philadelphia, PA'); - // to move the map to that location: - // const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA'); - // this._bingMap.current.setView({ - // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, - // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), - // }); - // - bingGeocode = (map: any, query: string) => { - return new Promise<{ latitude: number; longitude: number }>((res, reject) => { - //If search manager is not defined, load the search module. - if (!this._bingSearchManager) { - //Create an instance of the search manager and call the geocodeQuery function again. - this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => { - this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current); - res(this.bingGeocode(map, query)); - }); - } else { - this._bingSearchManager.geocode({ - where: query, - callback: action((r: any) => res(r.results[0].location)), - errorCallback: (e: any) => reject(), - }); - } - }); - }; - - @observable - bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string) - - geoDataRequestOptions = { - entityType: 'PopulatedPlace', - }; - - // The pin that is selected - @observable selectedPinOrRoute: Doc | undefined = undefined; - @action deselectPinOrRoute = () => { - if (this.selectedPinOrRoute) { + if (this._selectedPinOrRoute) { // // Removes filter // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove'); // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove'); @@ -511,79 +437,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; - /* - * Pushpin onclick - */ - @action - pushpinClicked = (pinDoc: Doc) => { - this.deselectPinOrRoute(); - this.selectedPinOrRoute = pinDoc; - this.bingSearchBarContents = pinDoc.map; - - // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); - // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); - - this.recolorPin(this.selectedPinOrRoute, 'green'); - - MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; - MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; - MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; - MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; - - const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.longitude)); - const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2; - const y = point.y + this._props.PanelHeight() / 2 + 32; - const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y); - MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); - - document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); - }; - - /** - * Map OnClick - */ - @action - mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { - this._props.select(false); - this.deselectPinOrRoute(); - }; - /* - * Updates values of layout doc to match the current map - */ - @action - mapRecentered = () => { - if ( - Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || // - Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7 - ) { - this.dataDoc.latitude = this._bingMap.current.getCenter().latitude; - this.dataDoc.longitude = this._bingMap.current.getCenter().longitude; - this.dataDoc.map = ''; - this.bingSearchBarContents = ''; - } - this.dataDoc.map_zoom = this._bingMap.current.getZoom(); - }; - /* - * Updates maptype - */ - @action - updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId()); - - /* - * For Bing Maps - * Called by search button's onClick - * Finds the geocode of the searched contents and sets location to that location - **/ - @action - bingSearch = () => { - return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => { - this.dataDoc.latitude = location.latitude; - this.dataDoc.longitude = location.longitude; - this.dataDoc.map_zoom = this._bingMap.current.getZoom(); - this.dataDoc.map = this.bingSearchBarContents; - }); - }; /* * Returns doc w/ relevant info @@ -592,14 +445,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ title: 'MapAnchor:' + this.Document.title, - text: (StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any, - config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), - config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), + text: (StrCast(this._selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any, + config_latitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), // config_map_type: StrCast(this.dataDoc.map_type), - config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), + config_map: StrCast((existingPin ?? this._selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map), layout_unrendered: true, - mapPin: existingPin ?? this.selectedPinOrRoute, + mapPin: existingPin ?? this._selectedPinOrRoute, annotationOn: this.Document, }); if (anchor) { @@ -613,25 +466,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem map_docToPinMap = new Map<Doc, any>(); map_pinHighlighted = new Map<Doc, boolean>(); - /* - * Input: pin doc - * Adds MicrosoftMaps Pushpin to the map (render) - */ - @action - addPushpin = (pin: Doc) => { - const pushPin = pin.infoWindowOpen - ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {}) - : new this.MicrosoftMaps.Pushpin( - new this.MicrosoftMaps.Location(pin.latitude, pin.longitude) - // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'} - ); - - this._bingMap.current.entities.push(pushPin); - - this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); - // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); - this.map_docToPinMap.set(pin, pushPin); - }; /* * Input: pin doc @@ -640,27 +474,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey); - /* - * Removes pushpin from map render - */ - deletePushpin = (pinDoc: Doc) => { - if (!this._unmounting) { - this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); - } - this.map_docToPinMap.delete(pinDoc); - this.selectedPinOrRoute = undefined; - }; - @action deleteSelectedPinOrRoute = undoable(() => { console.log('deleting'); - if (this.selectedPinOrRoute) { + if (this._selectedPinOrRoute) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove'); - Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', this._selectedPinOrRoute.latitude, 'remove'); + Doc.setDocFilter(this.Document, 'longitude', this._selectedPinOrRoute.longitude, 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove'); - this.removePushpinOrRoute(this.selectedPinOrRoute); + this.removePushpinOrRoute(this._selectedPinOrRoute); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -677,7 +500,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); runInAction(() => { - this.temporaryRouteSource = { + this._temporaryRouteSource = { type: 'FeatureCollection', features: [], }; @@ -688,9 +511,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action centerOnSelectedPin = () => { - if (this.selectedPinOrRoute) { + if (this._selectedPinOrRoute) { this._mapRef.current?.flyTo({ - center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)], + center: [NumCast(this._selectedPinOrRoute.longitude), NumCast(this._selectedPinOrRoute.latitude)], }); } // if (this.selectedPin) { @@ -703,33 +526,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); }; - /** - * View options for bing maps - */ - bingViewOptions = { - // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng }, - zoom: this.dataDoc.latitude ?? 10, - mapTypeId: 'grayscale', - }; - - /** - * Map options - */ - bingMapOptions = { - navigationBarMode: 'square', - backgroundColor: '#f1f3f4', - enableInertia: true, - supportedMapTypes: ['grayscale', 'canvasLight'], - disableMapTypeSelectorMouseOver: true, - // showScalebar:true - // disableRoadView:true, - // disableBirdseye:true - streetsideOptions: { - showProblemReporting: false, - showCurrentAddress: false, - }, - }; - recolorPin = (pin: Doc, color?: string) => { // this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); // this.map_docToPinMap.delete(pin); @@ -739,109 +535,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // this.map_docToPinMap.set(pin, newpin); }; - /* - * Called when BingMap is first rendered - * Initializes starting values - */ - @observable _mapReady = false; - @action - bingMapReady = (map: any) => { - this._mapReady = true; - this._bingMap = map.map; - if (!this._bingMap.current) { - alert('NO Map!?'); - } - this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick); - this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change')); - this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); - - this._disposers.mapLocation = reaction( - () => this.Document.map, - mapLoc => (this.bingSearchBarContents = mapLoc), - { fireImmediately: true } - ); - this._disposers.highlight = reaction( - () => this.allAnnotations.map(doc => doc[Highlight]), - () => { - const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); - allConfigPins.forEach(({ doc, pushpin }) => { - if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { - this.recolorPin(pushpin); - this.map_pinHighlighted.delete(pushpin); - } - }); - allConfigPins.forEach(({ doc, pushpin }) => { - if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) { - this.recolorPin(pushpin, 'orange'); - this.map_pinHighlighted.set(pushpin, true); - } - }); - }, - { fireImmediately: true } - ); - - this._disposers.location = reaction( - () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }), - locationObject => { - // if (this._bingMap.current) - try { - locationObject?.zoom && - this._bingMap.current?.setView({ - mapTypeId: locationObject.mapType, - zoom: locationObject.zoom, - center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng), - }); - } catch (e) { - console.log(e); - } - }, - { fireImmediately: true } - ); - }; - - dragToggle = (e: React.PointerEvent) => { - let dragClone: HTMLDivElement | undefined; - - setupMoveUpEvents( - e, - e, - e => { - // move event - if (!dragClone) { - dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin - dragClone.style.position = 'absolute'; - dragClone.style.zIndex = '10000'; - DragManager.Root().appendChild(dragClone); // add clone to root - } - dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; - return false; - }, - e => { - // up event - if (!dragClone) return; - DragManager.Root().removeChild(dragClone); - let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates - while (target) { - if (target === this._ref.current) { - const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); - const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2; - const y = cpt[1] - 20 /* height of search bar */ - this._props.PanelHeight() / 2; - const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); - this.createPushpin(location.latitude, location.longitude); - break; - } - target = target.parentElement; - } - }, - e => { - const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map); - if (this.bingSearchBarContents) { - this.bingSearch().then(createPin); - } else createPin(); - } - ); - }; - // incrementer: number = 0; /* * Creates Pushpin doc and adds it to the list of annotations @@ -880,7 +573,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (createPinForDestination) { this.createPushpin(destination.center[1], destination.center[0], destination.place_name); } - this.temporaryRouteSource = { + this._temporaryRouteSource = { type: 'FeatureCollection', features: [], }; @@ -890,10 +583,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // TODO: Display error that can't create route to same location }, 'createmaproute'); - searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); - - @observable - featuresFromGeocodeResults: any[] = []; + @action + searchbarKeyDown = (e: any) => { + if (e.key === 'Enter' && this._featuresFromGeocodeResults) { + const center = this._featuresFromGeocodeResults[0]?.center; + this._featuresFromGeocodeResults = []; + setTimeout(() => center && this._mapRef.current?.flyTo({ center })); + } + }; @action addMarkerForFeature = (feature: any) => { @@ -910,7 +607,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem center: feature.center, }); } - this.featuresFromGeocodeResults = []; + this._featuresFromGeocodeResults = []; } else { // TODO: handle error } @@ -922,11 +619,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem */ handleSearchChange = async (searchText: string) => { const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText); - if (features && !this.isAnimating) { + if (features && !this._isAnimating) { runInAction(() => { - this.settingsOpen = false; - this.featuresFromGeocodeResults = features; - this.routeToAnimate = undefined; + this._settingsOpen = false; + this._featuresFromGeocodeResults = features; + this._routeToAnimate = undefined; }); } // try { @@ -946,8 +643,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action handleMapClick = (e: MapLayerMouseEvent) => { - this.featuresFromGeocodeResults = []; - this.settingsOpen = false; + this._featuresFromGeocodeResults = []; + this._settingsOpen = false; if (this._mapRef.current) { const features = this._mapRef.current.queryRenderedFeatures(e.point, { layers: ['map-routes-layer'], @@ -960,8 +657,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle); this.deselectPinOrRoute(); // TODO: Also deselect route if selected if (routeDoc) { - this.selectedPinOrRoute = routeDoc; - Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + this._selectedPinOrRoute = routeDoc; + Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check'); // TODO: Recolor route @@ -1008,7 +705,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude); if (features) { runInAction(() => { - this.featuresFromGeocodeResults = features; + this._featuresFromGeocodeResults = features; }); } @@ -1028,21 +725,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // } }; - @observable - currentPopup: PopupInfo | undefined = undefined; - @action handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => { - this.featuresFromGeocodeResults = []; + this._featuresFromGeocodeResults = []; this.deselectPinOrRoute(); // TODO: check this method - this.selectedPinOrRoute = pinDoc; + this._selectedPinOrRoute = pinDoc; // this.bingSearchBarContents = pinDoc.map; // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match'); // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check'); - this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method + this.recolorPin(this._selectedPinOrRoute, 'green'); // TODO: check this method MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute; MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; @@ -1072,12 +766,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // }) }; - @observable - temporaryRouteSource: FeatureCollection = { - type: 'FeatureCollection', - features: [], - }; - @action displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => { if (routeInfoMap) { @@ -1096,41 +784,23 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; // TODO: Create pin for destination // TODO: Fly to point where full route will be shown - this.temporaryRouteSource = newTempRouteSource; + this._temporaryRouteSource = newTempRouteSource; } }; - @observable - isAnimating: boolean = false; - - @observable - routeToAnimate: Doc | undefined = undefined; - - @observable - animationPhase: number = 0; - - @observable - finishedFlyTo: boolean = false; - @action setAnimationPhase = (newValue: number) => { - this.animationPhase = newValue; + this._animationPhase = newValue; }; - @observable - frameId: number | null = null; - @action setFrameId = (frameId: number) => { - this.frameId = frameId; + this._frameId = frameId; }; - @observable - animationUtility: AnimationUtility | null = null; - @action setAnimationUtility = (util: AnimationUtility) => { - this.animationUtility = util; + this._animationUtility = util; }; @action @@ -1138,8 +808,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem if (routeDoc) { MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); - this.featuresFromGeocodeResults = []; - this.routeToAnimate = routeDoc; + this._featuresFromGeocodeResults = []; + this._routeToAnimate = routeDoc; } }; @@ -1161,29 +831,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @computed get preAnimationViewState() { - if (!this.isAnimating) { + if (!this._isAnimating) { return this.mapboxMapViewState; } } - @observable - isStreetViewAnimation: boolean = false; - - @observable - animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM; - - @observable - animationLineColor: string = '#ffff00'; - @action setAnimationLineColor = (color: ColorResult) => { - this.animationLineColor = color.hex; + this._animationLineColor = color.hex; }; @action updateAnimationSpeed = () => { let newAnimationSpeed: AnimationSpeed; - switch (this.animationSpeed) { + switch (this._animationSpeed) { case AnimationSpeed.SLOW: newAnimationSpeed = AnimationSpeed.MEDIUM; break; @@ -1197,63 +858,33 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem newAnimationSpeed = AnimationSpeed.MEDIUM; break; } - this.animationSpeed = newAnimationSpeed; - if (this.animationUtility) { - this.animationUtility.updateAnimationSpeed(newAnimationSpeed); + this._animationSpeed = newAnimationSpeed; + if (this._animationUtility) { + this._animationUtility.updateAnimationSpeed(newAnimationSpeed); } }; @computed get animationSpeedTooltipText(): string { - switch (this.animationSpeed) { - case AnimationSpeed.SLOW: - return '1x speed'; - case AnimationSpeed.MEDIUM: - return '2x speed'; - case AnimationSpeed.FAST: - return '3x speed'; - default: - return '2x speed'; - } + switch (this._animationSpeed) { + case AnimationSpeed.SLOW: return '1x speed'; + case AnimationSpeed.MEDIUM: return '2x speed'; + case AnimationSpeed.FAST: return '3x speed'; + default: return '2x speed'; + } // prettier-ignore } @computed get animationSpeedIcon(): JSX.Element { - switch (this.animationSpeed) { - case AnimationSpeed.SLOW: - return slowSpeedIcon; - case AnimationSpeed.MEDIUM: - return mediumSpeedIcon; - case AnimationSpeed.FAST: - return fastSpeedIcon; - default: - return mediumSpeedIcon; - } + switch (this._animationSpeed) { + case AnimationSpeed.SLOW: return slowSpeedIcon; + case AnimationSpeed.MEDIUM: return mediumSpeedIcon; + case AnimationSpeed.FAST: return fastSpeedIcon; + default: return mediumSpeedIcon; + } // prettier-ignore } @action toggleIsStreetViewAnimation = () => { - const newVal = !this.isStreetViewAnimation; - this.isStreetViewAnimation = newVal; - if (this.animationUtility) { - this.animationUtility.updateIsStreetViewAnimation(newVal); - } - }; - - @observable - dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [], - }, - }; - - @observable - path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = { - type: 'Feature', - geometry: { - type: 'LineString', - coordinates: [], - }, - properties: {}, + const newVal = !this._isStreetViewAnimation; + this._isStreetViewAnimation = newVal; + this._animationUtility?.updateIsStreetViewAnimation(newVal); }; getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => { @@ -1272,19 +903,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action playAnimation = (status: AnimationStatus) => { - if (!this._mapRef.current || !this.routeToAnimate) { + if (!this._mapRef.current || !this._routeToAnimate) { return; } - this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0; - this.frameId = AnimationStatus.RESUME ? this.frameId : null; - this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false; + this._animationPhase = status === AnimationStatus.RESUME ? this._animationPhase : 0; + this._frameId = AnimationStatus.RESUME ? this._frameId : null; + this._finishedFlyTo = AnimationStatus.RESUME ? this._finishedFlyTo : false; const path = turf.lineString(this.selectedRouteCoordinates); - this.settingsOpen = false; + this._settingsOpen = false; this.path = path; - this.isAnimating = true; + this._isAnimating = true; runInAction(() => { return new Promise<void>(async resolve => { @@ -1293,18 +924,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem lat: this.selectedRouteCoordinates[0][1], }; - const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed, this.showTerrain, this._mapRef.current); - runInAction(() => { - this.setAnimationUtility(animationUtil); - }); - - const updateFrameId = (newFrameId: number) => { - this.setFrameId(newFrameId); - }; + const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this._isStreetViewAnimation, this._animationSpeed, this._showTerrain, this._mapRef.current); + runInAction(() => this.setAnimationUtility(animationUtil)); - const updateAnimationPhase = (newAnimationPhase: number) => { - this.setAnimationPhase(newAnimationPhase); - }; + const updateFrameId = (newFrameId: number) => this.setFrameId(newFrameId); + const updateAnimationPhase = (newAnimationPhase: number) => this.setAnimationPhase(newAnimationPhase); if (status !== AnimationStatus.RESUME) { const result = await animationUtil.flyInAndRotate({ @@ -1324,9 +948,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem console.log('Altitude: ', result.altitude); } - runInAction(() => { - this.finishedFlyTo = true; - }); + runInAction(() => (this._finishedFlyTo = true)); // follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation await animationUtil.animatePath({ @@ -1335,7 +957,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // startBearing: -20, // startAltitude: this.isStreetViewAnimation ? 80 : 12000, // pitch: this.isStreetViewAnimation ? 80: 50, - currentAnimationPhase: this.animationPhase, + currentAnimationPhase: this._animationPhase, updateAnimationPhase, updateFrameId, }); @@ -1353,7 +975,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }); setTimeout(() => { - this.isStreetViewAnimation = false; + this._isStreetViewAnimation = false; resolve(); }, 10000); }); @@ -1362,27 +984,27 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action pauseAnimation = () => { - if (this.frameId && this.animationPhase > 0) { - window.cancelAnimationFrame(this.frameId); - this.frameId = null; - this.isAnimating = false; + if (this._frameId && this._animationPhase > 0) { + window.cancelAnimationFrame(this._frameId); + this._frameId = null; + this._isAnimating = false; } }; @action stopAnimation = (close: boolean) => { - if (this.frameId) { - window.cancelAnimationFrame(this.frameId); + if (this._frameId) { + window.cancelAnimationFrame(this._frameId); } - this.animationPhase = 0; - this.frameId = null; - this.finishedFlyTo = false; - this.isAnimating = false; + this._animationPhase = 0; + this._frameId = null; + this._finishedFlyTo = false; + this._isAnimating = false; if (close) { - this.animationSpeed = AnimationSpeed.MEDIUM; - this.isStreetViewAnimation = false; - this.routeToAnimate = undefined; - this.animationUtility = null; + this._animationSpeed = AnimationSpeed.MEDIUM; + this._isStreetViewAnimation = false; + this._routeToAnimate = undefined; + this._animationUtility = null; } }; @@ -1390,21 +1012,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return ( <> <IconButton - tooltip={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'} + tooltip={this._isAnimating && this._finishedFlyTo ? 'Pause Animation' : 'Play Animation'} onPointerDown={() => { - if (this.isAnimating && this.finishedFlyTo) { + if (this._isAnimating && this._finishedFlyTo) { this.pauseAnimation(); - } else if (this.animationPhase > 0) { + } else if (this._animationPhase > 0) { this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase } else { this.playAnimation(AnimationStatus.START); // Play from the beginning } }} - icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} + icon={this._isAnimating && this._finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />} color="black" size={Size.MEDIUM} /> - {this.isAnimating && this.finishedFlyTo && ( + {this._isAnimating && this._finishedFlyTo && ( <IconButton tooltip="Restart animation" onPointerDown={() => { @@ -1420,7 +1042,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <> <div className="animation-suboptions"> <div>|</div> - <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> + <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} /> <div id="divider">|</div> <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> <div id="divider">|</div> @@ -1436,26 +1058,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action hideRoute = () => { - this.temporaryRouteSource = { + this._temporaryRouteSource = { type: 'FeatureCollection', features: [], }; }; - @observable - settingsOpen: boolean = false; - - @observable - mapStyle: string = 'mapbox://styles/mapbox/standard'; - - @observable - showTerrain: boolean = true; - @action toggleSettings = () => { - if (!this.isAnimating && this.animationPhase == 0) { - this.featuresFromGeocodeResults = []; - this.settingsOpen = !this.settingsOpen; + if (!this._isAnimating && this._animationPhase == 0) { + this._featuresFromGeocodeResults = []; + this._settingsOpen = !this._settingsOpen; } }; @@ -1500,14 +1113,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem @action onStepZoomChange = (increment: boolean) => { if (this._mapRef.current) { - let newZoom: number; - if (increment) { - console.log('inc'); - newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1); - } else { - console.log('dec'); - newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1); - } + const newZoom = increment // + ? Math.min(16, this.mapboxMapViewState.zoom + 1) + : Math.max(0, this.mapboxMapViewState.zoom - 1); + this._mapRef.current.setZoom(newZoom); this.dataDoc.map_zoom = newZoom; } @@ -1523,7 +1132,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action - toggleShowTerrain = () => (this.showTerrain = !this.showTerrain); + toggleShowTerrain = () => (this._showTerrain = !this._showTerrain); getMarkerIcon = (pinDoc: Doc): JSX.Element | null => { const markerType = StrCast(pinDoc.markerType); @@ -1532,25 +1141,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null; }; - static _firstRender = true; - static _rerenderDelay = 500; - _rerenderTimeout: any; + _textRef = React.createRef<any>(); render() { - // bcz: no idea what's going on here, but bings maps have some kind of bug - // such that we need to delay rendering a second map on startup until the first map is rendered. - this.Document[DocCss]; - if (MapBox._rerenderDelay) { - // prettier-ignore - this._rerenderTimeout = this._rerenderTimeout ?? - setTimeout(action(() => { - if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) { - MapBox._rerenderDelay = 0; - } - this._rerenderTimeout = undefined; - this.Document[DocCss] = this.Document[DocCss] + 1; - }), MapBox._rerenderDelay); - return null; - } const scale = this._props.NativeDimScaling?.() || 1; const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1; @@ -1560,20 +1152,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} - onPointerDown={async e => { - e.button === 0 && !e.ctrlKey && e.stopPropagation(); - }} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.IsDragging ? null : renderAnnotations()} - {!this.routeToAnimate && ( + {!this._routeToAnimate && ( <div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}> - <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} /> + <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} /> <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + <div style={{ opacity: 0 }}> + <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} /> + </div> </div> )} - {this.settingsOpen && !this.routeToAnimate && ( + {this._settingsOpen && !this._routeToAnimate && ( <div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}> <div className="mapbox-style-select"> <div>Map Style:</div> @@ -1605,21 +1198,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem </div> <div className="mapbox-terrain-selection"> <div>Show terrain: </div> - <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} /> + <input type="checkbox" checked={this._showTerrain} onChange={this.toggleShowTerrain} /> </div> </div> )} - {this.routeToAnimate && ( + {this._routeToAnimate && ( <div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}> - <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div> + <div id="route-to-animate-title">{StrCast(this._routeToAnimate.title)}</div> <div className="route-animation-options">{this.getRouteAnimationOptions()}</div> </div> )} - {this.featuresFromGeocodeResults.length > 0 && ( + {this._featuresFromGeocodeResults.length > 0 && ( <div className="mapbox-geocoding-search-results"> - <React.Fragment> + <> <h4>Choose a location for your pin: </h4> - {this.featuresFromGeocodeResults + {this._featuresFromGeocodeResults .filter(feature => feature.place_name) .map((feature, idx) => ( <div @@ -1632,14 +1225,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem <div className="search-result-place-name">{feature.place_name}</div> </div> ))} - </React.Fragment> + </> </div> )} <MapProvider> <MapboxMap ref={this._mapRef} mapboxAccessToken={MAPBOX_ACCESS_TOKEN} - viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + viewState={this._isAnimating || this._routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} style={{ position: 'absolute', @@ -1649,20 +1242,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem width: NumCast(this.layoutDoc._width) * parscale, height: NumCast(this.layoutDoc._height) * parscale, }} - initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState} + initialViewState={this._isAnimating ? undefined : this.mapboxMapViewState} onZoom={this.onMapZoom} onMove={this.onMapMove} onClick={this.handleMapClick} onDblClick={this.handleMapDblClick} - terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> + terrain={this._showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}> <Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} /> - <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} /> + <Source id="temporary-route" type="geojson" data={this._temporaryRouteSource} /> <Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} /> <Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} /> - {!this.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />} - {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && ( + {!this._isAnimating && this._animationPhase == 0 && ( + <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} /> + )} + {this._routeToAnimate && (this._isAnimating || this._animationPhase > 0) && ( <> - {!this.isStreetViewAnimation && ( + {!this._isStreetViewAnimation && ( <> <Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} /> <Layer @@ -1670,7 +1265,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem type="line" source="animated-route" paint={{ - 'line-color': this.animationLineColor, + 'line-color': this._animationLineColor, 'line-width': 5, }} /> @@ -1722,10 +1317,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem )} <> - {!this.isAnimating && - this.animationPhase == 0 && - this.allPushpins - // .filter(anno => !anno.layout_unrendered) + {!this._isAnimating && + this._animationPhase == 0 && + this.allPushpins // .filter(anno => !anno.layout_unrendered) .map((pushpin, idx) => ( <Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}> {this.getMarkerIcon(pushpin)} diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx index fc5b4dd18..8ebc90157 100644 --- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; -import { MapBox } from './MapBox'; +import { MapBoxContainer } from '../MapboxMapBox/MapboxContainer'; /** * Map Pushpin doc class @@ -18,7 +18,7 @@ export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { } get mapBoxView() { - return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBox; + return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBoxContainer; } get mapBox() { return this.DocumentView?.().containerViewPath?.().lastElement()?.Document; diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index e38a42b29..1f976f926 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -65,7 +65,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; @undoBatch - @action public static WorkspaceStopRecording() { const remDoc = RecordingBox.screengrabber?.Document; if (remDoc) { @@ -90,7 +89,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { * @returns */ @undoBatch - @action public static WorkspaceStartRecording(value: string) { const screengrabber = value === 'Record Workspace' @@ -120,7 +118,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param value RecordingBox rootdoc */ @undoBatch - @action public static replayWorkspace(value: Doc) { Doc.UserDoc().currentRecording = value; value.overlayX = 70; @@ -138,7 +135,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param value current recordingbox */ @undoBatch - @action public static addRecToWorkspace(value: RecordingBox) { let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView); (ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document); @@ -149,7 +145,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.UserDoc().workspaceRecordingState = undefined; } - @action public static resumeWorkspaceReplaying(doc: Doc) { const docView = DocumentManager.Instance.getDocumentView(doc); if (docView?.ComponentView instanceof VideoBox) { @@ -158,7 +153,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.UserDoc().workspaceReplayingState = media_state.Playing; } - @action public static pauseWorkspaceReplaying(doc: Doc) { const docView = DocumentManager.Instance.getDocumentView(doc); const videoBox = docView?.ComponentView as VideoBox; @@ -168,7 +162,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.UserDoc().workspaceReplayingState = media_state.Paused; } - @action public static stopWorkspaceReplaying(value: Doc) { Doc.RemFromMyOverlay(value); Doc.UserDoc().currentRecording = undefined; @@ -178,7 +171,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { } @undoBatch - @action public static removeWorkspaceReplaying(value: Doc) { Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value); Doc.RemFromMyOverlay(value); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss index 287cccd8f..f2d5a980d 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.scss +++ b/src/client/views/nodes/RecordingBox/RecordingView.scss @@ -1,208 +1,200 @@ video { - // flex: 100%; - width: 100%; - // min-height: 400px; - //height: auto; - height: 100%; - //display: block; - object-fit: cover; - background-color: black; -} - -button { - margin: 0 .5rem + // flex: 100%; + width: 100%; + // min-height: 400px; + //height: auto; + height: 100%; + //display: block; + object-fit: cover; + background-color: black; } .recording-container { - height: 100%; - width: 100%; - // display: flex; - pointer-events: all; - background-color: black; + height: 100%; + width: 100%; + // display: flex; + pointer-events: all; + background-color: black; + button { + margin: 0 0.5rem; + } } .video-wrapper { - // max-width: 600px; - // max-width: 700px; - // position: relative; - display: flex; - justify-content: center; - // overflow: hidden; - border-radius: 10px; - margin: 0; + // max-width: 600px; + // max-width: 700px; + // position: relative; + display: flex; + justify-content: center; + // overflow: hidden; + border-radius: 10px; + margin: 0; } .video-wrapper:hover .controls { - bottom: 34.5px; - transform: translateY(0%); - opacity: 100%; + bottom: 34.5px; + transform: translateY(0%); + opacity: 100%; } .controls { - display: flex; - align-items: center; - justify-content: center; - position: absolute; - width: 100%; - flex-wrap: wrap; - background: rgba(255, 255, 255, 0.25); - box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1); - // backdrop-filter: blur(4px); - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.18); - transition: all 0.3s ease-in-out; - bottom: 34.5px; - height: 60px; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + width: 100%; + flex-wrap: wrap; + background: rgba(255, 255, 255, 0.25); + box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1); + // backdrop-filter: blur(4px); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.18); + transition: all 0.3s ease-in-out; + bottom: 34.5px; + height: 60px; } .controls:active { - bottom: 40px; + bottom: 40px; } .actions button { - background: none; - border: none; - outline: none; - cursor: pointer; + background: none; + border: none; + outline: none; + cursor: pointer; } .actions button i { - background-color: none; - color: white; - font-size: 30px; + background-color: none; + color: white; + font-size: 30px; } - .velocity { - appearance: none; - background: none; - color: white; - outline: none; - border: none; - text-align: center; - font-size: 16px; + appearance: none; + background: none; + color: white; + outline: none; + border: none; + text-align: center; + font-size: 16px; } .mute-btn { - background: none; - border: none; - outline: none; - cursor: pointer; + background: none; + border: none; + outline: none; + cursor: pointer; } .mute-btn i { - background-color: none; - color: white; - font-size: 20px; + background-color: none; + color: white; + font-size: 20px; } .recording-sign { - height: 20px; - width: auto; - display: flex; - flex-direction: row; - position: absolute; - top: 10px; - right: 15px; - align-items: center; - justify-content: center; - - .timer { - font-size: 15px; - color: white; - margin: 0; - } - - .dot { - height: 15px; - width: 15px; - margin: 5px; - background-color: red; - border-radius: 50%; - display: inline-block; - } + height: 20px; + width: auto; + display: flex; + flex-direction: row; + position: absolute; + top: 10px; + right: 15px; + align-items: center; + justify-content: center; + + .timer { + font-size: 15px; + color: white; + margin: 0; + } + + .dot { + height: 15px; + width: 15px; + margin: 5px; + background-color: red; + border-radius: 50%; + display: inline-block; + } } .controls-inner-container { - display: flex; - flex-direction: row; - position: relative; - width: 100%; - align-items: center; - justify-content: center; + display: flex; + flex-direction: row; + position: relative; + width: 100%; + align-items: center; + justify-content: center; } .record-button-wrapper { - width: 35px; - height: 35px; - font-size: 0; - background-color: grey; - border: 0px; - border-radius: 35px; - margin: 10px; - display: flex; - justify-content: center; - - .record-button { - background-color: red; - border: 0px; - border-radius: 50%; - height: 80%; - width: 80%; - align-self: center; - margin: 0; - - &:hover { - height: 85%; - width: 85%; - } - } - - .stop-button { - background-color: red; - border: 0px; - border-radius: 10%; - height: 70%; - width: 70%; - align-self: center; - margin: 0; - - - // &:hover { - // width: 40px; - // height: 40px - // } - } - + width: 35px; + height: 35px; + font-size: 0; + background-color: grey; + border: 0px; + border-radius: 35px; + margin: 10px; + display: flex; + justify-content: center; + + .record-button { + background-color: red; + border: 0px; + border-radius: 50%; + height: 80%; + width: 80%; + align-self: center; + margin: 0; + + &:hover { + height: 85%; + width: 85%; + } + } + + .stop-button { + background-color: red; + border: 0px; + border-radius: 10%; + height: 70%; + width: 70%; + align-self: center; + margin: 0; + + // &:hover { + // width: 40px; + // height: 40px + // } + } } .options-wrapper { - height: 100%; - display: flex; - flex-direction: row; - align-content: center; - position: relative; - top: 0; - bottom: 0; - - &.video-edit-wrapper { - - // right: 50% - 15; - - .track-screen { - font-weight: 200; - } - - } - - &.track-screen-wrapper { - - // right: 50% - 30; - - .track-screen { - font-weight: 200; - color: aqua; - } - - } -}
\ No newline at end of file + height: 100%; + display: flex; + flex-direction: row; + align-content: center; + position: relative; + top: 0; + bottom: 0; + + &.video-edit-wrapper { + // right: 50% - 15; + + .track-screen { + font-weight: 200; + } + } + + &.track-screen-wrapper { + // right: 50% - 30; + + .track-screen { + font-weight: 200; + color: aqua; + } + } +} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index d9d0dbe3e..8c65fd34e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -670,7 +670,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const definedParameters = !this.compileParams.length ? null : ( <div className="scriptingBox-plist" style={{ width: '30%' }}> {this.compileParams.map((parameter, i) => ( - <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}> + <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <EditableView display={'block'} maxHeight={72} @@ -749,7 +749,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {!this.compileParams.length || !this.paramsNames ? null : ( <div className="scriptingBox-plist"> {this.paramsNames.map((parameter: string, i: number) => ( - <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}> + <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}> <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}> <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div> {this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b2ae7201c..4773a21c9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -8,12 +8,13 @@ import { DocData } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; +import { dropActionType } from '../../util/DragManager'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ReplayMovements } from '../../util/ReplayMovements'; @@ -27,11 +28,10 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; import { DocumentView } from './DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; -import { dropActionType } from '../../util/DragManager'; /** * VideoBox @@ -335,7 +335,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); this._props.addDocument?.(imageSnapshot); const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); - link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); + // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; @@ -343,10 +343,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; - const anchor = addAsAnnotation - ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document - : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document); + const anchor = + addAsAnnotation && marquee + ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document + : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); return anchor; }; @@ -373,7 +374,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl getView = (doc: Doc, options: FocusViewOptions) => { if (this._stackedTimeline?.makeDocUnfiltered(doc)) { if (this.heightPercent === 100) { - this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; + // do we want to always open up the timeline when followin a link? kind of clunky visually + //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; options.didMove = true; } return this._stackedTimeline.getView(doc, options); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index c9340edc0..033b01d24 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -34,7 +34,7 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; import { DocumentView, OpenWhere } from './DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; import { LinkInfo } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; @@ -179,9 +179,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem // bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created) if (this._url) { const reqdFuncs: { [key: string]: string } = {}; - reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"])`; - reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"] = value`; - reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"sidebar"])`; + reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`; + reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`; + reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`; DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); } }); @@ -341,7 +341,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations; @action + iframeMove = (e: PointerEvent) => { + const theclick = this.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); + this._marqueeref.current?.onMove(theclick); + }; + @action iframeUp = (e: PointerEvent) => { + this._iframe?.contentDocument?.removeEventListener('pointermove', this.iframeMove); + this.marqueeing = undefined; this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value this._textAnnotationCreator = undefined; this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. @@ -358,6 +368,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } + } else { + const theclick = this.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); + if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]); + else { + if (!(e.target as any)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]); + this._getAnchor = AnchorMenu.Instance?.GetAnchor; + this.marqueeing = undefined; + } + + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(false); + if (e?.button === 2 || e?.altKey) { + e?.preventDefault(); + e?.stopPropagation(); + setTimeout(() => { + // if menu comes up right away, the down event can still be active causing a menu item to be selected + this.specificContextMenu(undefined as any); + this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]); + }); + } } }; @action @@ -400,6 +433,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action iframeDown = (e: PointerEvent) => { + this._textAnnotationCreator = undefined; + const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); + if (sel?.empty) + sel.empty(); // Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + this._props.select(false); const theclick = this.props .ScreenToLocalTransform() @@ -409,6 +448,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const word = getWordAtPoint(e.target, e.clientX, e.clientY); if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { this.marqueeing = theclick; + this._marqueeref.current?.onInitiateSelection(this.marqueeing); + this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove); e.preventDefault(); } }; @@ -739,28 +780,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.marqueeing = undefined; } }; - @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { + @action finishMarquee = (x?: number, y?: number) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.marqueeing = undefined; - this._textAnnotationCreator = undefined; - const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty) - sel.empty(); // Chrome - else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document); - if (x !== undefined && y !== undefined) { - ContextMenu.Instance.closeMenu(); - ContextMenu.Instance.setIgnoreEvents(false); - if (e?.button === 2 || e?.altKey) { - e?.preventDefault(); - e?.stopPropagation(); - setTimeout(() => { - // if menu comes up right away, the down event can still be active causing a menu item to be selected - this.specificContextMenu(undefined as any); - this.DocumentView?.().onContextMenu(undefined, x, y); - }); - } - } }; @observable lighttext = false; @@ -992,6 +1015,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined); @computed get webpage() { + TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any); const scale = previewScale * (this._props.NativeDimScaling?.() || 1); @@ -1071,6 +1095,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem : 'none'; annotationPointerEvents = () => (this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); render() { + TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any); const scale = previewScale * (this._props.NativeDimScaling?.() || 1); diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 7a0ff8776..74eeb014c 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,20 +1,11 @@ @import '../../global/globalCssVariables.module.scss'; +.dashFieldView-active, .dashFieldView { position: relative; display: inline-flex; align-items: center; - select { - display: none; - } - - &:hover { - select { - display: unset; - } - } - .dashFieldView-enumerables { width: 10px; height: 10px; @@ -50,6 +41,27 @@ } } } + +.dashFieldView, +.dashFieldView-active { + .dashFieldView-select { + height: 10p; + font-size: 12px; + background: transparent; + opacity: 0; + width: 5px; + } +} + +.dashFieldView { + &:hover { + .dashFieldView-select { + opacity: unset; + width: 15px !important; + } + } +} + .ProseMirror-selectedNode { outline: solid 1px $light-blue !important; } diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 62cb460c2..22a0cbe5e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; @@ -8,12 +8,12 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast } from '../../../../fields/Types'; +import { Cast, DocCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { FilterPanel } from '../../FilterPanel'; @@ -21,27 +21,50 @@ import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; +import { DocData } from '../../../../fields/DocSymbols'; +import { NodeSelection } from 'prosemirror-state'; export class DashFieldView { dom: HTMLDivElement; // container for label and value root: any; node: any; tbox: FormattedTextBox; + getpos: any; + @observable _nodeSelected = false; + NodeSelected = () => this._nodeSelected; unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + makeObservable(this); + const self = this; this.node = node; this.tbox = tbox; + this.getpos = getPos; this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-block'; - this.dom.onkeypress = function (e: any) { + const tBox = this.tbox; + this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; - this.dom.onkeydown = function (e: any) { + this.dom.onkeydown = function (e: KeyboardEvent) { e.stopPropagation(); + if (e.key === 'Tab') { + e.preventDefault(); + const editor = tbox.EditorView; + if (editor) { + const state = editor.state; + for (var i = self.getpos() + 1; i < state.doc.content.size; i++) { + if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) { + editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i)))); + return; + } + } + // tBox.setFocus(state.selection.to); + } + } }; this.dom.onkeyup = function (e: any) { e.stopPropagation(); @@ -61,7 +84,10 @@ export class DashFieldView { width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} + hideValue={node.attrs.hideValue} editable={node.attrs.editable} + nodeSelected={this.NodeSelected} + dataDoc={node.attrs.dataDoc} tbox={tbox} /> ); @@ -74,9 +100,11 @@ export class DashFieldView { }); } deselectNode() { + runInAction(() => (this._nodeSelected = false)); this.dom.classList.remove('ProseMirror-selectednode'); } selectNode() { + setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100); this.dom.classList.add('ProseMirror-selectednode'); } } @@ -85,10 +113,13 @@ interface IDashFieldViewInternal { fieldKey: string; docId: string; hideKey: boolean; + hideValue: boolean; tbox: FormattedTextBox; width: number; height: number; editable: boolean; + nodeSelected: () => boolean; + dataDoc: boolean; node: any; getPos: any; unclickable: () => boolean; @@ -101,18 +132,19 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi _fieldKey: string; _fieldRef = React.createRef<HTMLDivElement>(); @observable _dashDoc: Doc | undefined = undefined; - @observable _expanded = false; + @observable _expanded = this._props.nodeSelected(); constructor(props: IDashFieldViewInternal) { super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; - this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + this._textBoxDoc = this._props.tbox.Document; + const setDoc = action((doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc)); if (this._props.docId) { - DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); } else { - this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + setDoc(this._props.tbox.Document); } } @@ -126,31 +158,44 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi componentWillUnmount() { this._reactionDisposer?.(); } - return100 = () => 100; + isRowActive = () => (this._props.nodeSelected() || this._expanded) && this._props.editable; + + finishEdit = action(() => { + if (this._expanded) { + this._expanded = false; + // if the edit finishes, then we want to lose focus on the textBox unless something else in the textBox got focus + // the timeout allows switching focus from one dashFieldView to another in the same text box + setTimeout(() => !this._props.tbox.ProseRef?.contains(document.activeElement) && this._props.tbox._props.onBlur?.()); + } + }); + selectedCell = (): [Doc, number] => [this._dashDoc!, 0]; + columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { return !this._dashDoc ? null : ( - <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this._props.hideKey ? this._props.tbox._props.PanelWidth() - 20 : undefined }}> + <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this._props.hideKey ? undefined : this._props.tbox._props.PanelWidth} - columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero} - selectedCell={() => [this._dashDoc!, 0]} + maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth} + columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero} + selectedCell={this.selectedCell} fieldKey={this._fieldKey} rowHeight={returnZero} - isRowActive={() => this._expanded && this._props.editable} + isRowActive={this.isRowActive} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} allowCRs={true} - oneLine={!this._expanded} - finishEdit={action(() => (this._expanded = false))} + oneLine={!this._expanded && !this._props.nodeSelected()} + finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} + autoFocus={true} + rootSelected={this._props.tbox._props.rootSelected} /> </div> ); @@ -173,21 +218,50 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } }; + toggleFieldHide = undoable( + action(() => { + const editor = this._props.tbox.EditorView!; + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false })); + }), + 'hideKey' + ); + + toggleValueHide = undoable( + action(() => { + const editor = this._props.tbox.EditorView!; + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false })); + }), + 'hideValue' + ); + + @computed get _hideKey() { + return this._props.hideKey && !this._expanded; + } + + @computed get _hideValue() { + return this._props.hideValue && !this._props.nodeSelected(); + } + // clicking on the label creates a pivot view collection of all documents // in the same collection. The pivot field is the fieldKey of this label - onPointerDownLabelSpan = (e: any) => { + onPointerDownLabelSpan = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { DashFieldViewMenu.createFieldView = this.createPivotForField; + DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide; + DashFieldViewMenu.toggleValueHide = this.toggleValueHide; DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); + const editor = this._props.tbox.EditorView!; + setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100); }); }; @undoBatch selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => { - event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value); + event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value === '-unset-' ? undefined : event.target.value); }; @computed get values() { + if (this._props.nodeSelected()) return []; const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []); return vals.strings.map(facet => ({ value: facet, label: facet })); @@ -196,21 +270,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi render() { return ( <div - className="dashFieldView" + className={`dashFieldView${this.isRowActive() ? '-active' : ''}`} ref={this._fieldRef} style={{ width: this._props.width, height: this._props.height, pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> - {this._props.hideKey ? null : ( + {this._hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey} + {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey} </span> )} - {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} + {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} {!this.values.length ? null : ( - <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}> + <select className="dashFieldView-select" tabIndex={-1} defaultValue={Field.toKeyValueString(this._dashDoc!, this._fieldKey)} onChange={this.selectVal}> + <option value="-unset-">-unset-</option> {this.values.map(val => ( <option value={val.value}>{val.label}</option> ))} @@ -224,6 +299,8 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: DashFieldViewMenu; static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + static toggleFieldHide: () => void = emptyFunction; + static toggleValueHide: () => void = emptyFunction; constructor(props: any) { super(props); DashFieldViewMenu.Instance = this; @@ -233,6 +310,14 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { DashFieldViewMenu.createFieldView(e); DashFieldViewMenu.Instance.fadeOut(true); }; + toggleFieldHide = (e: React.MouseEvent) => { + DashFieldViewMenu.toggleFieldHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleValueHide = (e: React.MouseEvent) => { + DashFieldViewMenu.toggleValueHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; @observable _fieldKey = ''; @@ -248,11 +333,29 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { }; render() { return this.getElement( - <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.showFields}> - <FontAwesomeIcon icon="eye" size="lg" /> - </button> - </Tooltip> + <> + {!this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="sm" /> + </button> + </Tooltip> + )} + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> + <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> + <FontAwesomeIcon icon="bullseye" size="sm" /> + </button> + </Tooltip> + )} + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}> + <button className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> + <FontAwesomeIcon icon="hashtag" size="sm" /> + </button> + </Tooltip> + )} + </> ); } } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index b786c5ffb..b90653acc 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -8,6 +8,7 @@ import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; import EquationEditor from './EquationEditor'; import { FormattedTextBox } from './FormattedTextBox'; +import { DocData } from '../../../../fields/DocSymbols'; export class EquationView { dom: HTMLDivElement; // container for label and value @@ -88,7 +89,6 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> } e.stopPropagation(); }} - onKeyPress={e => e.stopPropagation()} style={{ position: 'relative', display: 'inline-block', @@ -96,12 +96,11 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> height: this.props.height, background: 'white', borderRadius: '10%', - bottom: 3, }}> <EquationEditor ref={this._ref} - value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')} - onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)} + value={StrCast(this._textBoxDoc[DocData][this._fieldKey])} + onChange={(str: any) => (this._textBoxDoc[DocData][this._fieldKey] = str)} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" spaceBehavesLikeTab={true} diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx index cf48e1250..b327e5137 100644 --- a/src/client/views/nodes/formattedText/FootnoteView.tsx +++ b/src/client/views/nodes/formattedText/FootnoteView.tsx @@ -23,6 +23,7 @@ export class FootnoteView { this.dom = document.createElement('footnote'); this.dom.addEventListener('pointerup', this.toggle, true); + this.dom.addEventListener('mouseup', (e: MouseEvent) => e.stopPropagation(), true); // These are used when the footnote is selected this.innerView = null; } @@ -82,9 +83,10 @@ export class FootnoteView { document.removeEventListener('pointerup', this.ignore, true); }; - toggle = () => { + toggle = (e: PointerEvent) => { if (this.innerView) this.close(); else this.open(); + e.stopPropagation(); }; close() { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 03ff0436b..00d890860 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -156,6 +156,7 @@ audiotag:hover { .formattedTextBox-inner, .formattedTextBox-inner-minimal { height: 100%; + overflow: auto; white-space: pre-wrap; .ProseMirror:hover { background: rgba(200, 200, 200, 0.2); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 52e47a4ee..bb910737b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -21,7 +21,7 @@ import { List } from '../../../../fields/List'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; @@ -68,8 +68,13 @@ import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; // import * as applyDevTools from 'prosemirror-dev-tools'; + +interface FormattedTextBoxProps extends FieldViewProps { + onBlur?: () => void; // callback when text loses focus + autoFocus?: boolean; // whether text should get input focus when created +} @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -99,7 +104,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; private _ignoreScroll = false; - private _hadDownFocus = false; private _focusSpeed: Opt<number>; private _keymap: any = undefined; private _rules: RichTextRules | undefined; @@ -200,7 +204,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId } - constructor(props: FieldViewProps) { + constructor(props: FormattedTextBoxProps) { super(props); makeObservable(this); FormattedTextBox.Instance = this; @@ -242,8 +246,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - if (!pinProps && this._editorView?.state.selection.empty) return this.Document; - const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document }); + const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); + if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; + const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); @@ -321,8 +326,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps leafText = (node: Node) => { if (node.type === this._editorView?.state.schema.nodes.dashField) { - const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); - return Field.toJavascriptString(refDoc[node.attrs.fieldKey as string] as Field); + const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + const fieldKey = StrCast(node.attrs.fieldKey); + return ( + (node.attrs.hideKey ? '' : fieldKey + ':') + // + (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field)) + ); } return ''; }; @@ -337,12 +346,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tryUpdateDoc = (force: boolean) => { if (this._editorView && (this._editorView as any).docView) { const state = this._editorView.state; - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const dataDoc = this.dataDoc; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype + const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, ''); @@ -359,25 +369,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } let unchanged = true; - if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) { + const textChange = newText !== prevData?.Text; // the Text string can change even if the RichText doesn't because dashFieldViews may return new strings as the data they reference changes + if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) { this._applyingChange = this.fieldKey; - const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); - if ((!prevData && !protoData) || newText || (!newText && !protoData)) { + if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) { + if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) { const numstring = NumCast(dataDoc[this.fieldKey], null); - dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText ? new RichTextField(newJson, newText) : undefined; + dataDoc[this.fieldKey] = + numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false - dataDoc[this.fieldKey + '_noTemplate'] = newText ? true : false; // mark the data field as being split from the template if it has been edited unchanged = false; } } else { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); - dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data))); ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } @@ -443,16 +452,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.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))) || + (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && + link.link_relationship === LinkManager.AutoKeywords + ); // prettier-ignore if (this._editorView?.state.doc.textContent) { - const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; + const t = this._editorView.state.selection.to; var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); - Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); + tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); @@ -617,7 +633,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps docId: draggedDoc[Id], float: 'unset', }); - if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { + if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; } else { added = true; @@ -944,6 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; + optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' }); optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); this._props.renderDepth && @@ -958,8 +975,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); - optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); + const help = cm.findByDescription('Help...'); + const helpItems = help && 'subitems' in help ? help.subitems : []; + helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); + !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; }; @@ -1011,7 +1031,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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)); + this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); if (this._recordingDictation) { this.recordDictation(); } @@ -1235,9 +1255,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); this._disposers.editorState = reaction( () => { - const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; - const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) }; + const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey]; + 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 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)) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1247,11 +1273,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } - } else if (incomingValue?.str) { - selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue.str))); + } else if (this._editorView.state.doc.textContent !== incomingValue?.str) { + selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); } } - } + }, + { fireImmediately: true } ); this._disposers.search = reaction( @@ -1347,15 +1374,14 @@ 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, hideValue: false, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }], - title: `from: ${DocCast(pdfAnchor.embedContainer).title}`, + title: StrCast(pdfAnchor.title), noPreview: true, - docref: false, + docref: true, + fontSize: '8px', }), - view.state.schema.marks.pFontSize.create({ fontSize: '8px' }), - view.state.schema.marks.em.create({}), ]), ]); @@ -1489,6 +1515,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.PasteOnLoad = undefined; pdfAnchorId && this.addPdfReference(pdfAnchorId); } + if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. } // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. @@ -1561,7 +1588,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps (this.ProseRef?.children?.[0] as any).focus(); } } - this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } @@ -1572,9 +1598,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerUp = (e: React.PointerEvent): void => { const editor = this._editorView!; const state = editor?.state; - if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime) && !this._hadDownFocus) { - (this.ProseRef?.children[0] as HTMLElement)?.blur?.(); - } if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this._props.isContentActive() && !e.button) { @@ -1583,10 +1606,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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'); + let node: any; + try { + node = state.doc.nodeAt(xpos); + } catch (e) {} + if (node?.type !== schema.nodes.dashFieldView) { + editor.dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(xpos)))); + let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + while (target && !target.dataset?.targethrefs) target = target.parentElement; + FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + } else if (node) { + try { + editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos)))); + } catch (e) { + editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos - 1)))); + } + } } }; @action @@ -1607,10 +1642,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.stopPropagation(); } }; - setFocus = () => { - const pos = this._editorView?.state.selection.$from.pos || 1; - (this.ProseRef?.children?.[0] as any).focus(); - setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + setFocus = (ipos?: number) => { + const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1); + setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100); + setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200); }; @action onFocused = (e: React.FocusEvent): void => { @@ -1662,10 +1697,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); - let olistPos = clickPos?.pos; + const clickPosVal = clickPos?.pos || 1; + let olistPos = clickPosVal; if (clickPos && olistPos && this._props.rootSelected?.()) { - const clickNode = this._editorView?.state.doc.nodeAt(olistPos); - const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1)); + const clickNode = this._editorView?.state.doc.resolve(olistPos).node(); + const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node(); olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; let $olistPos = this._editorView?.state.doc.resolve(olistPos); let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; @@ -1675,18 +1711,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); } } - const listPos = this._editorView?.state.doc.resolve(clickPos.pos); - const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos); + const maxSize = this._editorView?.state.doc.content.size ?? 0; + const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal)); + const listNode = listPos?.node(); if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) { if (!highlightOnly) { if (selectOrderedList) { this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); } else { - const tr = this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); - this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, clickPos.pos))); + const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1); + if (this._editorView.state.doc.nodeAt(nodePos)) { + const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos))); + } } } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'lightgray' }); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'gray' }); } } } @@ -1703,13 +1743,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) { const stordMarks = this._editorView?.state.storedMarks?.slice(); - this.autoLink(); - if (this._editorView?.state.tr) { - const tr = stordMarks?.reduce((tr, m) => { - tr.addStoredMark(m); - return tr; - }, this._editorView.state.tr); - tr && this._editorView.dispatch(tr); + if (!(this.EditorView?.state.selection instanceof NodeSelection)) { + this.autoLink(); + if (this._editorView?.state.tr) { + const tr = stordMarks?.reduce((tr, m) => { + tr.addStoredMark(m); + return tr; + }, this._editorView.state.tr); + tr && this._editorView.dispatch(tr); + } } } if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) { @@ -1730,6 +1772,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection()); }, 'titler'); } + // if the text box blurs and none of its contents are focused(), then pass the blur along + setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; onKeyDown = (e: React.KeyboardEvent) => { @@ -1775,7 +1819,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': - if (e.code !== 'Space') { + if (e.code !== 'Space' && e.code !== 'Backspace') { [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) && this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } @@ -1810,7 +1854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); const scrollHeight = this.ProseRef && proseHeight; - if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) { + if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); @@ -1934,11 +1978,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps </div> ); } - cycleAlternateText = () => { - if (this.layoutDoc._layout_enableAltContentUI) { - const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; - } + cycleAlternateText = (skipHover?: boolean) => { + this.layoutDoc._layout_enableAltContentUI = true; + const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined; }; @computed get overlayAlternateIcon() { const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; @@ -1973,7 +2016,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); } get fieldKey() { - const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); + return this._fieldKey; + } + @computed get _fieldKey() { + const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; @@ -1994,6 +2040,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( + !Number.isNaN(height) && (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { @@ -2026,8 +2073,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" - onPointerEnter={action(() => (this._isHovering = true))} - onPointerLeave={action(() => (this._isHovering = false))} + onPointerEnter={action(() => { + this._isHovering = true; + this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true); + })} + onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))} ref={r => { this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = r; diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 47527847b..ab49a53ea 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,7 +1,7 @@ import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands'; import { redo, undo } from 'prosemirror-history'; import { Schema } from 'prosemirror-model'; -import { splitListItem, wrapInList } from 'prosemirror-schema-list'; +import { splitListItem, wrapInList, sinkListItem, liftListItem } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; @@ -11,8 +11,9 @@ import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { OpenWhere } from '../DocumentView'; -import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; +//import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; import { Doc } from '../../../../fields/Doc'; +import { EditorView } from 'prosemirror-view'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -106,7 +107,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - dispatch(tx3); + const tx4 = tx3.setSelection(TextSelection.near(tx3.doc.resolve(state.selection.to + 2))); + dispatch(tx4); }) ) { console.log('bullet promote fail'); @@ -120,7 +122,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if ( - !liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => { + !liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); @@ -164,12 +166,6 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey SelectionManager.DeselectAll(); }); - const splitMetadata = (marks: any, tx: Transaction) => { - marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - return tx; - }; - bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true)); bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true)); bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => { @@ -260,7 +256,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); - bind('Backspace', (state: EditorState, dispatch: (tx: Transaction) => void) => { + const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; @@ -272,6 +268,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if ( !joinBackward(state, (tx: Transaction) => { dispatch(updateBullets(tx, schema)); + if (once && view.state.selection.$from.depth > 1 && view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === view.state.schema.nodes.list_item) backspace(view.state, view.dispatch, view, false); }) ) { if ( @@ -284,27 +281,33 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey } } return true; - }); + }; + bind('Backspace', backspace); //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line - bind('Enter', (state: EditorState, dispatch: (tx: Transaction) => void) => { + + const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); - const path = (state.selection.$from as any).path; - const depth = trange ? liftTarget(trange) : undefined; - const split = path.length > 5 && !path[path.length - 3].textContent && path[path.length - 6].type !== schema.nodes.list_item; - if (split && trange && depth !== undefined && depth !== null) { + const depth = trange ? liftTarget(trange) : null; + if ( + depth !== null && + state.selection.$from.node(state.selection.$from.depth - 1)?.type === state.schema.nodes.blockquote && // + !state.selection.$from.node().content.size && + trange + ) { dispatch(state.tr.lift(trange, depth) as any); return true; } const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - const cr = state.selection.$from.node().textContent.endsWith('\n'); - if (/*cr ||*/ !newlineInCode(state, dispatch as any)) { - if ( + if (!newlineInCode(state, dispatch as any)) { + if (once && view.state.selection.$from.depth > 1 && !view.state.selection.$from.nodeBefore && !view.state.selection.$from.nodeBefore) { + for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); + } else if ( !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); @@ -318,10 +321,20 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const tonode = tx3.selection.$to.node(); if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); - splitMetadata(marks, tx4); - if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as (tx: Transaction) => void)) { - dispatch(tx4); - } + dispatch(tx4); + if ( + view.state.selection.$from.parentOffset && // + !view.state.selection.$from.node().content.size + ) + liftListItem(schema.nodes.list_item)(view.state, view.dispatch); + else if ( + once && + view.state.selection.$from.parentOffset && + view.state.selection.$from.depth > 1 && // + view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === schema.nodes.list_item + ) + enter(view.state, view.dispatch, view, false); + else if (once && depth && !view.state.selection.$from.parentOffset) backspace(view.state, view.dispatch, view, false); } else dispatch(tx3.insertText('\r\n')); }) ) { @@ -330,13 +343,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey } } return true; - }); + }; + bind('Enter', enter); //Command to create a blank space bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true; - const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); - dispatch(splitMetadata(marks, state.tr)); return false; }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index dc2c06701..b5d0f28d8 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -318,6 +318,23 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } + elideSelection = (txstate: EditorState | undefined = undefined, visibility = false) => { + const state = txstate ?? this.view?.state; + if (!state || state.selection.empty) return false; + const mark = state.schema.marks.summarize.create(); + const tr = state.tr.addMark(state.tr.selection.from, state.selection.to, mark); + const text = tr.selection.content(); + const elideNode = state.schema.nodes.summary.create({ visibility, text, textslice: text.toJSON() }); + const summary = tr.replaceSelectionWith(elideNode).removeMark(tr.selection.from - 1, tr.selection.from, mark); + const expanded = () => { + const endOfElidableText = summary.selection.to + text.content.size; + const res = summary.insert(summary.selection.to, text.content).insert(endOfElidableText, state.schema.nodes.paragraph.create({})); + return res.setSelection(new TextSelection(res.doc.resolve(endOfElidableText + 1))); + }; + this.view?.dispatch?.(visibility ? expanded() : summary); + return true; + }; + toggleNoAutoLinkAnchor = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index c798ae4b3..5e53a019e 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,6 +1,6 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { Doc, FieldResult, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; @@ -137,6 +137,7 @@ export class RichTextRules { 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.isDataDoc = true; textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation @@ -243,10 +244,19 @@ export class RichTextRules { }), // activate a style by name using prefix '%<color name>' - new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => { + new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => { const color = match[0].substring(1, match[0].length); const marks = RichTextMenu.Instance._brushMap.get(color); + if ( + DocListCast((Doc.UserDoc().template_notes as Doc).data) + .concat(DocListCast((Doc.UserDoc().template_user as Doc).data)) + .map(d => StrCast(d.title)) + .includes(color) + ) { + setTimeout(() => this.TextBox.DocumentView?.().switchViews(true, color, undefined, true)); + return state.tr.deleteRange(start, end); + } if (marks) { const tr = state.tr.deleteRange(start, end); return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr; @@ -267,7 +277,7 @@ export class RichTextRules { // toggle alternate text UI %/ new InputRule(new RegExp(/%\//), (state, match, start, end) => { - setTimeout(this.TextBox.cycleAlternateText); + setTimeout(() => this.TextBox.cycleAlternateText(true)); return state.tr.deleteRange(start, end); }), @@ -283,96 +293,113 @@ export class RichTextRules { : tr; }), - new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => { - var count = 0; // ignore first return value which will be the notation that chat is pending a result - KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { - count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string))); - count++; - }); - return null; - }), - - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - // [[<fieldKey> : <Doc>]] - // [[:docTitle]] => hyperlink - // [[fieldKey]] => show field - // [[fieldKey{:,=:}=value]] => show field and also set its value - // [[fieldKey:docTitle]] => show field of doc - new InputRule( - new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), - (state, match, start, end) => { - const fieldKey = match[1]; - const assign = match[2] === '=' ? '' : match[2]; - const value = match[4]; - const docTitle = match[5]?.replace(':', ''); + // create a hyperlink to a titled document + // @(<doctitle>) + new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => { + const docTitle = match[2]; + const prefixLength = '@('.length; + if (docTitle) { const linkToDoc = (target: Doc) => { - const rstate = this.TextBox.EditorView?.state; - const selection = rstate?.selection.$from.pos; - if (rstate) { - this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); + const editor = this.TextBox.EditorView; + const selection = editor?.state?.selection.$from.pos; + if (editor) { + const estate = editor.state; + 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 fstate = this.TextBox.EditorView?.state; - if (fstate && selection) { - this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); + const teditor = this.TextBox.EditorView; + if (teditor && selection) { + const tstate = teditor.state; + teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection)))); } }; const getTitledDoc = (docTitle: string) => { if (!DocServer.FindDocByTitle(docTitle)) { - Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_fitWidth: true, _layout_autoHeight: true }); } const titledDoc = DocServer.FindDocByTitle(docTitle); return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; }; - if (!fieldKey) { - if (docTitle) { - const target = getTitledDoc(docTitle); - if (target) { - setTimeout(() => linkToDoc(target)); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); - } - } - return state.tr; + const target = getTitledDoc(docTitle); + if (target) { + setTimeout(() => linkToDoc(target)); + return state.tr.insertText(' ').deleteRange(start, start + prefixLength); } + } + return state.tr; + }), + + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document + // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value] + // [@{this,doctitle,}.fieldKey] + new InputRule( + new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_\(\)\.@\?\+\-\*\/\ 0-9\(\)]*))?\]/), + (state, match, start, end) => { + const docTitle = match[1].substring(1).replace(/\.$/, ''); + const fieldKey = match[2]; + const assign = match[4] === ':' ? (match[4] = '') : match[4]; + const value = match[5]; + const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('='); + const getTitledDoc = (docTitle: string) => { + if (!DocServer.FindDocByTitle(docTitle)) { + Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + } + return DocServer.FindDocByTitle(docTitle); + }; // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); } else if (value) { - KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: - (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore + KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined: + (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore + if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr; } - const target = getTitledDoc(docTitle); - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); + const target = docTitle ? getTitledDoc(docTitle) : undefined; + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, hideValue: false, dataDoc }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }, { inCode: true } ), + // pass the contents between '((' and '))' to chatGPT and append the result + new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))$/), (state, match, start, end) => { + var count = 0; // ignore first return value which will be the notation that chat is pending a result + KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { + if (count) { + const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); + tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); + RichTextMenu.Instance.elideSelection(this.TextBox.EditorView?.state, true); + } + count++; + }); + return null; + }), + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document - // wiki:title - new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => { - const title = match[1]; + // @(wiki:title) + new InputRule(new RegExp(/@\(wiki:([a-zA-Z_@:\.\?\-0-9 ]+)\)$/), (state, match, start, end) => { + const title = match[1].trim().replace(/ /g, '_'); this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))); this.TextBox.makeLinkAnchor(undefined, 'add:right', `https://en.wikipedia.org/wiki/${title.trim()}`, 'wikipedia reference'); const fstate = this.TextBox.EditorView?.state; if (fstate) { - const tr = fstate?.tr.deleteRange(start, start + 5); - return tr.setSelection(new TextSelection(tr.doc.resolve(end - 5))).insertText(' '); + const tr = fstate?.tr.deleteRange(start, start + '@(wiki:'.length); + return tr.setSelection(new TextSelection(tr.doc.resolve(end - '@(wiki:'.length))).insertText(' '); } return state.tr; }), // create an inline equation node - // eq:<equation>> - new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => { + // %eq + new InputRule(new RegExp(/%eq/), (state, match, start, end) => { const fieldKey = 'math' + Utils.GenerateGuid(); - this.TextBox.dataDoc[fieldKey] = match[1]; + this.TextBox.dataDoc[fieldKey] = 'y='; const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey })); return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1))); }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index a141ef041..ccf7de4a1 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -74,6 +74,7 @@ export const marks: { [index: string]: MarkSpec } = { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, title: { default: null }, noPreview: { default: false }, + fontSize: { default: null }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text }, inclusive: false, @@ -93,14 +94,16 @@ export const marks: { [index: string]: MarkSpec } = { const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); return node.attrs.docref && node.attrs.title ? [ - 'div', + 'a', ['span', 0], [ 'span', { ...node.attrs, class: 'prosemirror-attribution', + 'data-targethrefs': targethrefs, href: node.attrs.allAnchors[0].href, + style: `font-size: ${node.attrs.fontSize}`, }, node.attrs.title, ], @@ -232,22 +235,6 @@ export const marks: { [index: string]: MarkSpec } = { }, }, - metadata: { - toDOM() { - return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }]; - }, - }, - metadataKey: { - toDOM() { - return ['span', { style: 'font-style:italic; ' }]; - }, - }, - metadataVal: { - toDOM() { - return ['span']; - }, - }, - summarizeInclusive: { parseDOM: [ { diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index c9115be90..e335044ea 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -264,7 +264,9 @@ export const nodes: { [index: string]: NodeSpec } = { fieldKey: { default: '' }, docId: { default: '' }, hideKey: { default: false }, + hideValue: { default: false }, editable: { default: true }, + dataDoc: { default: false }, }, leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), group: 'inline', diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index e34144fae..cd9fec839 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -18,6 +18,7 @@ import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; +import { dropActionType } from '../../../util/DragManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; import { SerializationHelper } from '../../../util/SerializationHelper'; @@ -32,11 +33,10 @@ import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; -import { dropActionType } from '../../../util/DragManager'; export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -703,7 +703,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (pinProps.pinData.temporal) { pinDoc.config_clipStart = targetDoc._layout_currentTimecode; const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); - pinDoc.config_clipEnd = NumCast(targetDoc.clipEnd, duration); + pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration); } } if (pinProps?.pinViewport) { |
