diff options
Diffstat (limited to 'src/client/views/nodes')
41 files changed, 1503 insertions, 1494 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 62f630c6c..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, @@ -182,7 +180,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // 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 ... + 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 } diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index 6b5738790..e9a346fbe 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -32,6 +32,10 @@ .liveSchema-checkBox { margin-bottom: -35px; } + + .displaySchemaLive { + margin-bottom: 20px; + } .dataviz-sidebar { position: absolute; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 66a08f13e..60c5fdba2 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -18,7 +18,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponen import { MarqueeAnnotator } from '../../MarqueeAnnotator'; import { SidebarAnnos } from '../../SidebarAnnos'; import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; import { DocumentView } from '../DocumentView'; import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; @@ -28,6 +28,7 @@ import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import { Checkbox } from '@mui/material'; +import { ContextMenu } from '../../ContextMenu'; export enum DataVizView { TABLE = 'table', @@ -43,6 +44,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); + sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined; crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; @observable _marqueeing: number[] | undefined = undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -118,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; @@ -266,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; @@ -283,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 } @@ -402,6 +405,26 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive } + specificContextMenu = (e: React.MouseEvent): void => { + const cm = ContextMenu.Instance; + const options = cm.findByDescription('Options...'); + const optionItems = options && 'subitems' in options ? options.subitems : []; + optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' }); + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); + } + + + askGPT = action(async () => { + GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.addDoc = this.sidebarAddDocument; + GPTPopup.Instance.setDataJson(""); + GPTPopup.Instance.setMode(GPTPopupMode.DATA); + let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + let input = JSON.stringify(data); + GPTPopup.Instance.setDataJson(input); + GPTPopup.Instance.generateDataAnalysis(); + }); + render() { const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( @@ -418,6 +441,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im transform: `scale(${scale})`, position: 'absolute', }} + onContextMenu={this.specificContextMenu} onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> @@ -428,9 +452,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im </div> {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?( - <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> - <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> - Display Live Updates to Canvas + <div className={'displaySchemaLive'}> + <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> + <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> + Display Live Updates to Canvas + </div> </div> ) : null} diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 41ce637ac..cf0007cfd 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -120,11 +120,62 @@ } } } -.selectAll-buttons { - display: flex; - flex-direction: row; - justify-content: flex-end; +.tableBox-selectButtons { margin-top: 5px; - margin-right: 10px; - float: right; + margin-left: 25px; + display: inline-block; + padding: 2px; + .tableBox-selectTitle { + display: inline-flex; + flex-direction: row; + } + .tableBox-filtering { + display: flex; + flex-direction: row; + float: right; + margin-right: 10px; + .tableBox-filterAll { + min-width: 75px; + } + } +} + +.tableBox-filterPopup { + background: $light-gray; + position: absolute; + min-width: 235px; + top: 60px; + display: flex; + flex-direction: column; + align-items: flex-start; + z-index: 2; + padding: 7px; + border-radius: 5px; + margin: 3px; + .tableBox-filterPopup-selectColumn { + margin-top: 5px; + flex-direction: row; + .tableBox-filterPopup-selectColumn-each { + margin-left: 25px; + border-radius: 3px; + background: $light-gray; + } + } + .tableBox-filterPopup-setValue { + margin-top: 5px; + display: flex; + flex-direction: row; + .tableBox-filterPopup-setValue-each { + margin-right: 5px; + border-radius: 3px; + background: $light-gray; + } + .tableBox-filterPopup-setValue-input { + margin: 5px; + } + } + .tableBox-filterPopup-setFilter { + margin-top: 5px; + align-self: center; + } } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 1b239b5e5..67e1c67bd 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -12,7 +12,8 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; -const { default: { DATA_VIZ_TABLE_ROW_HEIGHT } } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore +import { undoable } from '../../../../util/UndoManager'; +const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { Document: Doc; layoutDoc: Doc; @@ -37,6 +38,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { _inputChangedDisposer?: IReactionDisposer; _containerRef: HTMLDivElement | null = null; + @observable settingTitle: boolean = false; // true when setting a title column + @observable hasRowsToFilter: boolean = false; // true when any rows are selected + @observable filtering: boolean = false; // true when the filtering menu is open + @observable filteringColumn: any = ''; // column to filter + @observable filteringType: string = 'Value'; // "Value" or "Range" + filteringVal: any[] = ['', '']; // value or range to filter the column with + @observable _scrollTop = -1; @observable _tableHeight = 0; @observable _tableContainerHeight = 0; @@ -49,6 +57,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { // if the tableData changes (ie., when records are selected by the parent (input) visulization), // then we need to remove any selected rows that are no longer part of the visualized dataset. this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true }); + const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows); + if (selected.length > 0) this.hasRowsToFilter = true; this.handleScroll(); } componentWillUnmount() { @@ -64,9 +74,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link - // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get columns() { @@ -115,6 +122,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } else selected?.push(rowId); } e.stopPropagation(); + this.hasRowsToFilter = selected.length > 0 ? true : false; }; columnPointerDown = (e: React.PointerEvent, col: string) => { @@ -155,15 +163,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }, emptyFunction, action(e => { - if (e.shiftKey){ - if (this._props.titleCol == col) this._props.titleCol = ""; + if (e.shiftKey || this.settingTitle) { + if (this.settingTitle) this.settingTitle = false; + if (this._props.titleCol == col) this._props.titleCol = ''; else this._props.titleCol = col; this._props.selectTitleCol(this._props.titleCol); - } - else{ + } else { const newAxes = this._props.axes; if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 2) newAxes[newAxes.length-1] = col; + else if (newAxes.length > 2) newAxes[newAxes.length - 1] = col; else newAxes.push(col); this._props.selectAxes(newAxes); } @@ -171,6 +179,134 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ); }; + /** + * These functions handle the filtering popup for when the "filter" button is pressed to select rows + */ + filter = undoable((e: any) => { + var start: any; + var end: any; + if (this.filteringType == 'Range') { + start = (this.filteringVal[0] as Number) ? Number(this.filteringVal[0]) : this.filteringVal[0]; + end = (this.filteringVal[1] as Number) ? Number(this.filteringVal[1]) : this.filteringVal[0]; + } + + this._tableDataIds.forEach(rowID => { + if (this.filteringType == 'Value') { + if (this._props.records[rowID][this.filteringColumn] == this.filteringVal[0]) { + if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) { + this.tableRowClick(e, rowID); + } + } + } else { + let compare = this._props.records[rowID][this.filteringColumn]; + if (compare as Number) compare = Number(compare); + if (start <= compare && compare <= end) { + if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) { + this.tableRowClick(e, rowID); + } + } + } + }); + this.filtering = false; + this.filteringColumn = ''; + this.filteringVal = ['', '']; + }, 'filter table'); + @action + setFilterColumn = (e: any) => { + this.filteringColumn = e.currentTarget.value; + }; + @action + setFilterType = (e: any) => { + this.filteringType = e.currentTarget.value; + }; + changeFilterValue = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[0] = e.target.value; + }); + changeFilterRange0 = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[0] = e.target.value; + }); + changeFilterRange1 = action((e: React.ChangeEvent<HTMLInputElement>) => { + this.filteringVal[1] = e.target.value; + }); + @computed get renderFiltering() { + if (this.filteringColumn === '') this.filteringColumn = this.columns[0]; + return ( + <div className="tableBox-filterPopup" style={{ right: this._props.width * 0.05 }}> + <div className="tableBox-filterPopup-selectColumn"> + Column: + <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn != '' ? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}> + {this.columns.map(column => ( + <option className="" key={column} value={column}> + {' '} + {column}{' '} + </option> + ))} + </select> + </div> + <div className="tableBox-filterPopup-setValue"> + <select className="tableBox-filterPopup-setValue-each" value={this.filteringType} onChange={e => this.setFilterType(e)}> + <option className="" key={'Value'} value={'Value'}> + {' '} + {'Value'}{' '} + </option> + <option className="" key={'Range'} value={'Range'}> + {' '} + {'Range'}{' '} + </option> + </select> + : + {this.filteringType == 'Value' ? ( + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterValue} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + /> + ) : ( + <div> + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterRange0} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + style={{ width: this._props.width * 0.15 }} + /> + to + <input + className="tableBox-filterPopup-setValue-input" + defaultValue="" + autoComplete="off" + onChange={this.changeFilterRange1} + onKeyDown={e => { + e.stopPropagation(); + }} + type="text" + placeholder="" + id="search-input" + style={{ width: this._props.width * 0.15 }} + /> + </div> + )} + </div> + <div className="tableBox-filterPopup-setFilter"> + <Button onClick={action(e => this.filter(e))} text="Set Filter" type={Type.SEC} color={'black'} /> + </div> + </div> + ); + } + render() { if (this._tableData.length > 0) { return ( @@ -184,9 +320,39 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); } }}> - <div className="selectAll-buttons"> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> - <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + <div className="tableBox-selectButtons"> + <div className="tableBox-selectTitle"> + <Button onClick={action(() => (this.settingTitle = !this.settingTitle))} text="Select Title Column" type={Type.SEC} color={'black'} /> + </div> + <div className="tableBox-filtering"> + {this.filtering ? this.renderFiltering : null} + <Button onClick={action(() => (this.filtering = !this.filtering))} text="Filter" type={Type.SEC} color={'black'} /> + <div className="tableBox-filterAll"> + {this.hasRowsToFilter ? ( + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(); + this.hasRowsToFilter = false; + })} + text="Deselect All" + type={Type.SEC} + color={'black'} + tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." + /> + ) : ( + <Button + onClick={action(() => { + this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + this.hasRowsToFilter = true; + })} + text="Select All" + type={Type.SEC} + color={'black'} + tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one." + /> + )} + </div> + </div> </div> <div className={`tableBox-container ${this.columns[0]}`} @@ -220,15 +386,23 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { <th key={this.columns.indexOf(col)} style={{ - color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' - : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? 'darkred' - : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? 'darkblue' : undefined, - background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' - : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? '#Fbdbdb' - : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? '#c6ebf7' : undefined, - // blue: #ADD8E6 - // green: #E3fbdb - // red: #Fbdbdb + color: + this._props.axes.slice().reverse().lastElement() === col + ? 'darkgreen' + : this._props.axes.length > 2 && this._props.axes.lastElement() === col + ? 'darkred' + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + ? 'darkblue' + : undefined, + background: this.settingTitle + ? 'lightgrey' + : this._props.axes.slice().reverse().lastElement() === col + ? '#E3fbdb' + : this._props.axes.length > 2 && this._props.axes.lastElement() === col + ? '#Fbdbdb' + : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col) + ? '#c6ebf7' + : undefined, fontWeight: 'bolder', border: '3px solid black', }} @@ -251,10 +425,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }}> {this.columns.map(col => { var colSelected = false; - if (this._props.axes.length>2) colSelected = this._props.axes[0]==col || this._props.axes[1]==col || this._props.axes[2]==col; - else if (this._props.axes.length>1) colSelected = this._props.axes[0]==col || this._props.axes[1]==col; - else if (this._props.axes.length>0) colSelected = this._props.axes[0]==col; - if (this._props.titleCol==col) colSelected = true; + if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col; + else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col; + else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col; + if (this._props.titleCol == col) colSelected = true; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> <div className="tableBox-cell">{this._props.records[rowId][col]}</div> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 5a326ecb0..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,7 +128,9 @@ 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>'; @@ -154,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 ae83819e5..21c7f3079 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -104,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; @@ -201,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' @@ -223,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); } @@ -560,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); @@ -597,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' }); @@ -810,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]; @@ -838,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)`, @@ -851,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; @@ -892,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} @@ -909,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, @@ -1053,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); } @@ -1357,6 +1382,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } //} }; + backgroundColor = () => this._docViewInternal?.backgroundBoxColor; DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition); ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; @@ -1410,7 +1436,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 5b47dd91d..771856788 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -92,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/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 e2b0ee7df..a79dda74e 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,20 +1,23 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; +import { Colors } from 'browndash-components'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; 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'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; +import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -23,29 +26,39 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; +import { OverlayView } from '../OverlayView'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; import { OpenWhere } from './DocumentView'; -import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; +import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; +export class ImageEditorData { + private static _instance: ImageEditorData; + private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore + @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined }); + @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => (this._instance.imageData = { open, rootDoc, source, addDoc }); + + constructor() { + makeObservable(this); + ImageEditorData._instance = this; + } + + public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore + public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore + public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore + public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore + public static set Open(open: boolean) { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore + public static set Source(source: string) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore + public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore + public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore +} @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } - - @observable public static imageRootDoc: Doc | undefined = undefined; - @observable public static imageEditorOpen: boolean = false; - @observable public static imageEditorSource: string = ''; - @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined; - @action public static setImageEditorOpen(open: boolean) { - ImageBox.imageEditorOpen = open; - } - @action public static setImageEditorSource(source: string) { - ImageBox.imageEditorSource = source; - } private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; @@ -134,7 +147,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 +172,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; @@ -243,10 +255,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl funcs.push({ description: 'Open Image Editor', event: action(() => { - ImageBox.setImageEditorOpen(true); - ImageBox.setImageEditorSource(this.choosePath(field.url)); - ImageBox.addDoc = this._props.addDocument; - ImageBox.imageRootDoc = this.Document; + ImageEditorData.Open = true; + ImageEditorData.Source = this.choosePath(field.url); + ImageEditorData.AddDoc = this._props.addDocument; + ImageEditorData.RootDoc = this.Document; }), icon: 'pencil-alt', }); @@ -259,7 +271,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 +309,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 +323,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 +340,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 +384,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 +483,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 d85432631..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,14 +105,15 @@ 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 false; 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/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 0a4325d8c..ff1e62885 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -13,7 +13,7 @@ import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; import { LinkInfo } from './LinkDocPreview'; -const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore +const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { 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..d79df4272 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; @@ -35,6 +26,7 @@ display: inline-block; font-weight: normal; background: rgba(0, 0, 0, 0.1); + cursor: default; } .dashFieldView-fieldSpan { min-width: 8px; @@ -50,6 +42,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 6186b3d99..17b8b53e7 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, trace } 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'; @@ -22,27 +22,49 @@ 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(); @@ -62,9 +84,9 @@ export class DashFieldView { width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} + hideValue={node.attrs.hideValue} editable={node.attrs.editable} - expanded={node.attrs.expanded} - dataDoc={node.attrs.dataDoc} + nodeSelected={this.NodeSelected} tbox={tbox} /> ); @@ -77,9 +99,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'); } } @@ -88,12 +112,12 @@ interface IDashFieldViewInternal { fieldKey: string; docId: string; hideKey: boolean; + hideValue: boolean; tbox: FormattedTextBox; width: number; height: number; editable: boolean; - expanded: boolean; - dataDoc: boolean; + nodeSelected: () => boolean; node: any; getPos: any; unclickable: () => boolean; @@ -106,14 +130,14 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi _fieldKey: string; _fieldRef = React.createRef<HTMLDivElement>(); @observable _dashDoc: Doc | undefined = undefined; - @observable _expanded = this._props.expanded; + @observable _expanded = this._props.nodeSelected(); constructor(props: IDashFieldViewInternal) { super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; this._textBoxDoc = this._props.tbox.Document; - const setDoc = (doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc); + const setDoc = action((doc: Doc) => (this._dashDoc = doc)); if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); @@ -132,22 +156,29 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi componentWillUnmount() { this._reactionDisposer?.(); } - isRowActive = () => this._expanded && this._props.editable; - finishEdit = action(() => (this._expanded = false)); - selectedCell = (): [Doc, number] => [this._dashDoc!, 0]; - selectedCells = () => [this._dashDoc!]; + 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?.()); + } + }); + selectedCells = () => (this._dashDoc ? [this._dashDoc] : undefined); + columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey // set the display of the field's value (checkbox for booleans, span of text for strings) @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 || this._hideKey ? undefined : this._props.tbox._props.PanelWidth} - columnWidth={returnZero} + columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero} selectedCells={this.selectedCells} selectedCol={returnZero} fieldKey={this._fieldKey} @@ -158,10 +189,12 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi setColumnValues={returnFalse} setSelectedColumnValues={returnFalse} allowCRs={true} - oneLine={!this._expanded} + oneLine={!this._expanded && !this._props.nodeSelected()} finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} + autoFocus={true} + rootSelected={this._props.tbox._props.rootSelected} /> </div> ); @@ -185,31 +218,49 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }; toggleFieldHide = undoable( - action(() => this._dashDoc && (this._dashDoc[this._fieldKey + '_hideKey'] = !this._dashDoc[this._fieldKey + '_hideKey'])), + 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._dashDoc && this._dashDoc[this._fieldKey + '_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.expanded) return []; + if (this._props.nodeSelected()) return []; const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []); return vals.strings.map(facet => ({ value: facet, label: facet })); @@ -218,21 +269,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 || this._hideKey ? null : ( + {this._hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> {(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={this._dashDoc && Field.toKeyValueString(this._dashDoc, this._fieldKey)} onChange={this.selectVal}> + <option value="-unset-">-unset-</option> {this.values.map(val => ( <option value={val.value}>{val.label}</option> ))} @@ -247,6 +299,7 @@ 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; @@ -260,6 +313,10 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { DashFieldViewMenu.toggleFieldHide(); DashFieldViewMenu.Instance.fadeOut(true); }; + toggleValueHide = (e: React.MouseEvent) => { + DashFieldViewMenu.toggleValueHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; @observable _fieldKey = ''; @@ -275,14 +332,27 @@ 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> - <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> - <FontAwesomeIcon icon="bullseye" size="lg" /> - </button> - </Tooltip> + <> + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="sm" /> + </button> + </Tooltip> + {this._fieldKey.startsWith('#') ? null : ( + <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}> + <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}> + <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..38dd2e847 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -273,6 +273,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; @@ -348,6 +349,8 @@ footnote::before { touch-action: none; span { font-family: inherit; + background-color: inherit; + display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span } blockquote { @@ -397,6 +400,7 @@ footnote::before { font-family: inherit; } margin-left: 0; + background-color: inherit; } .decimal2-ol { counter-reset: deci2; @@ -406,6 +410,7 @@ footnote::before { } font-size: smaller; padding-left: 2.1em; + background-color: inherit; } .decimal3-ol { counter-reset: deci3; @@ -415,6 +420,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .decimal4-ol { counter-reset: deci4; @@ -424,6 +430,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } .decimal5-ol { counter-reset: deci5; @@ -432,6 +439,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal6-ol { counter-reset: deci6; @@ -440,6 +448,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .decimal7-ol { counter-reset: deci7; @@ -448,6 +457,7 @@ footnote::before { font-family: inherit; } font-size: smaller; + background-color: inherit; } .multi1-ol { @@ -458,6 +468,7 @@ footnote::before { } margin-left: 0; padding-left: 1.2em; + background-color: inherit; } .multi2-ol { counter-reset: multi2; @@ -467,6 +478,7 @@ footnote::before { } font-size: smaller; padding-left: 2em; + background-color: inherit; } .multi3-ol { counter-reset: multi3; @@ -476,6 +488,7 @@ footnote::before { } font-size: smaller; padding-left: 2.85em; + background-color: inherit; } .multi4-ol { counter-reset: multi4; @@ -485,6 +498,7 @@ footnote::before { } font-size: smaller; padding-left: 3.85em; + background-color: inherit; } //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } @@ -788,6 +802,7 @@ footnote::before { height: 20px; &::before { content: '→'; + cursor: default; } &:hover { background: orange; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2b48494f2..43010b2ed 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -8,7 +8,7 @@ import { history } from 'prosemirror-history'; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; -import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; +import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; @@ -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); } @@ -86,7 +91,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _scrollRef: HTMLDivElement | null = null; private _editorView: Opt<EditorView>; public _applyingChange: string = ''; private _inDrop = false; @@ -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); @@ -286,7 +291,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }); }; AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -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.filter(term => term.title).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))); + 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); @@ -462,7 +478,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing - (title.startsWith('-') || title.startsWith('@')) && + title.startsWith('-') && this._editorView && !this.dataDoc.title_custom && (Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text') @@ -470,14 +486,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; - const prefix = str.startsWith('@') ? '' : '-'; + const prefix = '-'; const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); - if (str.startsWith('@') && str.length > 1) { - Doc.AddToMyPublished(this.Document); - } } } }; @@ -494,7 +507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { const editorView = this._editorView; if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { - const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); + const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, ''); var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { if ( @@ -617,7 +630,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; @@ -871,7 +884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; - setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50); + setTimeout(() => (this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50), 50); }), icon: 'eye', }); @@ -944,6 +957,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 && @@ -969,10 +983,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this._editorView?.state.storedMarks ?? []; - this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks)); - setTimeout(() => { - this.animateRes(resIndex + 1, newText); - }, 20); + this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); + setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; @@ -980,13 +992,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps try { let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { - console.error('GPT call failed'); this.animateRes(0, 'Something went wrong.'); - } else { - this.animateRes(0, res); + } else if (this._editorView) { + const { dispatch, state } = this._editorView; + // for no animation, use: dispatch(state.tr.insertText(res)); + // for animted response starting at end of text, use: + dispatch(state.tr.setSelection(Selection.atEnd(state.doc))); + this.animateRes(0, '\n\n' + res); } } catch (err) { - console.error('GPT call failed'); + console.error(err); this.animateRes(0, 'Something went wrong.'); } }); @@ -1006,7 +1021,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(); } @@ -1230,9 +1245,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) { @@ -1242,11 +1263,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } - } else { + } 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( @@ -1284,25 +1306,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); if (this._recordingDictation) setTimeout(this.recordDictation); } - var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { - if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) { - const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); - const durationMiliStr = viewTrans.match(/([0-9]*)ms/); - const durationSecStr = viewTrans.match(/([0-9.]*)s/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - if (duration) { - this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper); - } else { - this._scrollRef.current.scrollTo({ top: pos }); - } + if (!this._ignoreScroll && this._scrollRef) { + const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]+)(m?)s/); + const duration = Number(durationStr?.[1]) * (durationStr?.[2] ? 1 : 1000); + this._scrollStopper = smoothScroll(duration || 0, this._scrollRef, Math.abs(pos || 0), 'ease', this._scrollStopper); } }, { fireImmediately: true } ); - quickScroll = undefined; this.tryUpdateScrollHeight(); setTimeout(this.tryUpdateScrollHeight, 250); } @@ -1342,7 +1356,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ - view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false, expanded: true }, 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: StrCast(pdfAnchor.title), @@ -1394,14 +1408,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps handleScrollToSelection: editorView => { const docPos = editorView.coordsAtPos(editorView.state.selection.to); const viewRect = self._ref.current!.getBoundingClientRect(); - const scrollRef = self._scrollRef.current; + const scrollRef = self._scrollRef; const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { - scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); + setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper))); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1471,6 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); + this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } } if (selectOnLoad) { @@ -1483,6 +1498,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. @@ -1555,7 +1571,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(); } @@ -1564,23 +1579,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps document.removeEventListener('pointerup', this.onSelectEnd); }; 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) { - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - let xpos = pcords?.pos || 0; - while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { - xpos = xpos - 1; - } - editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(xpos)))); + const state = this.EditorView?.state; + if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { + if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); } }; @action @@ -1601,10 +1606,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 => { @@ -1656,10 +1661,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; @@ -1669,18 +1675,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' }); } } } @@ -1697,33 +1707,38 @@ 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?.()) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } FormattedTextBox._hadSelection = window.getSelection()?.toString() !== ''; + + // this is the markdown for @<published name> document publishing to Doc.myPublishedDocs + const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); + if (match) { + this.dataDoc.title_custom = true; + this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc + this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1)); + } + this.endUndoTypingBatch(); FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; const state = this._editorView!.state; - if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) { - UndoManager.RunInBatch(() => { - this.dataDoc.title_custom = true; - this.dataDoc.layout_showTitle = 'title'; - const tr = this._editorView!.state.tr; - this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection()); - }, 'titler'); - } + // if the text box blurs and none of its contents are focused(), then pass the blur along + setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; onKeyDown = (e: React.KeyboardEvent) => { @@ -1756,7 +1771,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); - RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); + RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); @@ -1769,7 +1784,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) }))); } @@ -1783,28 +1798,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { - if (!LinkInfo.Instance?.LinkInfo && this._scrollRef.current) { - if (!this._props.dontSelectOnLoad) { - this._ignoreScroll = true; - this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; - this._ignoreScroll = false; - e.stopPropagation(); - e.preventDefault(); - } + if (!LinkInfo.Instance?.LinkInfo && this._scrollRef) { + this._ignoreScroll = true; + this.layoutDoc._layout_scrollTop = this._scrollRef.scrollTop; + this._ignoreScroll = false; + e.stopPropagation(); + e.preventDefault(); } }; tryUpdateScrollHeight = () => { const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children && !SnappingManager.IsDragging) { - const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); - const toHgt = (node: Element) => { + const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; + const toNum = (val: string) => Number(val.replace('px', '')); + const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); - return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); + const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); + return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; - const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); + const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children); const scrollHeight = this.ProseRef && proseHeight; - if (this._props.setHeight && 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); @@ -1986,11 +2001,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this._props.isContentActive()) { const scale = this._props.NativeDimScaling?.() || 1; - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > const height = Number(styleFromLayoutString.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + !Number.isNaN(height) && + (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); @@ -2018,12 +2034,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" - 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; @@ -2065,9 +2084,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onDoubleClick={this.onDoubleClick}> <div className="formattedTextBox-outer" - ref={this._scrollRef} + ref={r => (this._scrollRef = r)} style={{ - width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, + width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2084,8 +2103,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} + {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} </div> diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 47527847b..03c902580 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,8 @@ import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { OpenWhere } from '../DocumentView'; -import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; import { Doc } from '../../../../fields/Doc'; +import { EditorView } from 'prosemirror-view'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -89,7 +89,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); - const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); + const marks = state.storedMarks || state.selection.$to.parentOffset ? state.selection.$from.marks() : undefined; if ( !sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); @@ -102,11 +102,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end))); if ( !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => { - const tx3 = updateBullets(tx2, schema); + const tx25 = updateBullets(tx2, schema); + const ol_node = tx25.doc.nodeAt(range!.start)!; + const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks); // when promoting to a list, assume list will format things so don't copy the stored marks. marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - dispatch(tx3); + const tx4 = tx3.setSelection(TextSelection.near(tx3.doc.resolve(state.selection.to + 2))); + dispatch(tx4); }) ) { console.log('bullet promote fail'); @@ -120,7 +123,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 +167,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 +257,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) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; @@ -272,6 +269,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey if ( !joinBackward(state, (tx: Transaction) => { dispatch(updateBullets(tx, schema)); + if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { + // gets rid of an extra paragraph when joining two list items together. + joinBackward(view.state, (tx: Transaction) => view.dispatch(tx)); + } }) ) { if ( @@ -284,59 +285,80 @@ 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(-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)) { + const olNode = view.state.selection.$anchor.node(-2); + const liNode = view.state.selection.$anchor.node(-1); + // prettier-ignore + if (liNode?.type === schema.nodes.list_item && !liNode.textContent && + olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3) + { + // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph + for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++); + } else if ( !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); dispatch(tx3); + // removes an extra paragraph created when selecting text across two list items or splitting an empty list item + !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2)); }) ) { - const fromattrs = state.selection.$from.node().attrs; - if ( - !splitBlockKeepMarks(state, (tx3: Transaction) => { - const tonode = tx3.selection.$to.node(); - if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); - splitMetadata(marks, tx4); - if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as (tx: Transaction) => void)) { + if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') { + // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again + view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({}))); + enter(view.state, view.dispatch, view, false); + } else { + const fromattrs = state.selection.$from.node().attrs; + if ( + !splitBlockKeepMarks(state, (tx3: Transaction) => { + const tonode = tx3.selection.$to.node(); + if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); dispatch(tx4); } - } else dispatch(tx3.insertText('\r\n')); - }) - ) { - return false; + + if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) { + // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items. + enter(view.state, dispatch, view, false); + } + }) + ) { + return false; + } } } } 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 bee0d72e3..cecf106a3 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -27,7 +27,10 @@ const { toggleMark } = require('prosemirror-commands'); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { - @observable static Instance: RichTextMenu; + static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); + static get Instance() { + return RichTextMenu._instance?.menu; + } public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); @@ -48,7 +51,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; - @observable private activeListType: string = ''; + @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set<Mark> = new Set(); @@ -67,7 +70,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu.Instance = this; + RichTextMenu._instance.menu = this; this.updateMenu(undefined, undefined, props, this.layoutDoc); this._canFade = false; this.Pinned = true; @@ -100,6 +103,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get fontSize() { return this._activeFontSize; } + @computed get listStyle() { + return this._activeListType; + } @computed get textAlign() { return this._activeAlignment; } @@ -131,11 +137,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; } - // update active marks - const activeMarks = this.getActiveMarksOnSelection(); - this.setActiveMarkButtons(activeMarks); - - // update active font family and size + this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); const activeFamilies = active.activeFamilies; const activeSizes = active.activeSizes; @@ -144,7 +146,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); - this.activeListType = this.getActiveListStyle(); + this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; @@ -161,17 +163,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); - const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from; + const fromRange = numberRange(state.selection.from).reverse(); + const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); - if (node?.type === schema.nodes.ordered_list) { - let attrs = node.attrs; - if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; - if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; - const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); - } - { + if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { + const hasMark = node.marks.some(m => m.type === mark.type); + const otherMarks = node.marks.filter(m => m.type !== mark.type); + const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); + const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); + dispatch(updateBullets(markup, state.schema)); + } else { const state = this.view?.state; const tr = this.view?.state.tr; if (tr && state) { @@ -201,16 +202,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // finds font sizes and families in selection getActiveListStyle() { - if (this.view && this.TextView?._props.rootSelected?.()) { - const path = (this.view.state.selection.$from as any).path; - for (let i = 0; i < path.length; i += 3) { - if (path[i].type === this.view.state.schema.nodes.ordered_list) { - return path[i].attrs.mapStyle; + const state = this.view?.state; + if (state) { + const pos = state.selection.$anchor; + for (let i = 0; i < pos.depth; i++) { + const node = pos.node(i); + if (node.type === schema.nodes.ordered_list) { + return node.attrs.mapStyle; } } - if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) { - return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle; - } } return ''; } @@ -224,11 +224,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.view && this.TextView?._props.rootSelected?.()) { const state = this.view.state; const pos = this.view.state.selection.$from; - const marks: Mark[] = [...(state.storedMarks ?? [])]; + var marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { } else if (state.selection.empty) { - const ref_node = this.reference_node(pos); - marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; + } } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); @@ -255,41 +256,26 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { //finds all active marks on selection in given group getActiveMarksOnSelection() { - let activeMarks: MarkType[] = []; - if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks; + if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[]; - const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; - if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); - //current selection - const { empty, ranges, $to } = this.view.state.selection as TextSelection; const state = this.view.state; - if (!empty) { - activeMarks = markGroup.filter(mark => { - const has = false; - for (let i = 0; !has && i < ranges.length; i++) { - return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark); - } - return false; - }); - } else { - const pos = this.view.state.selection.$from; - const ref_node: ProsNode | null = this.reference_node(pos); - if (ref_node !== null && ref_node !== this.view.state.doc) { - if (ref_node.isText) { - } else { - return []; - } - activeMarks = markGroup.filter(mark_type => { - // if (mark_type === state.schema.marks.pFontSize) { - // return mark.isINSet - // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); - // } - const mark = state.schema.mark(mark_type); - return mark.isInSet(ref_node.marks); - }); + var marks: Mark[] = [...(state.storedMarks ?? [])]; + const pos = this.view.state.selection.$from; + if (state.storedMarks !== null) { + } else if (state.selection.empty) { + for (let i = 0; i <= pos.depth; i++) { + marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } + } else { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); + }); } - return activeMarks; + const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; + return markGroup.filter(mark_type => { + const mark = state.schema.mark(mark_type); + return mark.isInSet(marks); + }); } @action @@ -318,16 +304,20 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } - elideSelection = () => { - const state = this.view?.state; - if (!state) return; - if (state.selection.empty) return false; + 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; - tr.addMark(state.selection.from, state.selection.to, mark); - const content = tr.selection.content(); - const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }); - this.view?.dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); + 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; }; @@ -366,7 +356,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontSize = (fontSize: string) => { if (this.view) { if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { - this.TextView.dataDoc.fontSize = fontSize; + this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize; this.view.focus(); } else { const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); @@ -381,7 +371,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontFamily = (family: string) => { if (this.view) { - const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); + const fmark = this.view.state.schema.marks.pFontFamily.create({ family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } else if (SelectionManager.Views.length) { @@ -413,40 +403,24 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { - const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle(); - const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle }); - if (!this.view || nodeType?.attrs.mapStyle === '') return; - - const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list; - let inList: any = undefined; - let fromList = -1; - const path: any = Array.from((this.view.state.selection.$from as any).path); - for (let i = 0; i < path.length; i++) { - if (path[i]?.type === schema.nodes.ordered_list) { - inList = path[i]; - fromList = path[i - 1]; - } - } + const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle(); + const newMapStyle = active === mapStyle ? '' : mapStyle; + if (!this.view || newMapStyle === '') return; + let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if ( - inList || + if (inList) { + const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + this.view.dispatch(tx2); + } else !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); - marks && tx3.ensureMarks([...marks]); - marks && tx3.setStoredMarks([...marks]); - - this.view!.dispatch(tx2); - }) - ) { - const tx2 = this.view.state.tr; - if (nodeType && (inList || nextIsOL)) { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to); + const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - this.view.dispatch(tx3); - } - } + this.view!.dispatch(tx3); + }); this.view.focus(); this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; @@ -561,7 +535,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // todo: add brushes to brushMap to save with a style name onBrushNameKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); + RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; @@ -569,7 +543,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @action clearBrush() { - RichTextMenu.Instance.brushMarks = new Set(); + RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set()); } @action @@ -705,118 +679,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } }; - linkExtend($start: ResolvedPos, href: string) { - const mark = this.view!.state.schema.marks.linkAnchor; - - let startIndex = $start.index(); - let endIndex = $start.indexAfter(); - - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++; - - let startPos = $start.start(); - let endPos = startPos; - for (let i = 0; i < endIndex; i++) { - const size = $start.parent.child(i).nodeSize; - if (i < startIndex) startPos += size; - endPos += size; - } - return { from: startPos, to: endPos }; - } - - reference_node(pos: ResolvedPos): ProsNode | null { - if (!this.view) return null; - - let ref_node: ProsNode = this.view.state.doc; - if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) { - ref_node = pos.nodeBefore; - } - if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) { - if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) { - ref_node = pos.nodeAfter; - } - } - if (!ref_node && pos.pos > 0) { - let skip = false; - for (let i: number = pos.pos - 1; i > 0; i--) { - this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => { - if (node.isLeaf && !skip) { - ref_node = node; - skip = true; - } - }); - } - } - if (!ref_node.isLeaf && ref_node.childCount > 0) { - ref_node = ref_node.child(0); - } - return ref_node; - } - render() { return null; - // TraceMobx(); - // const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ - // //!this.collapsed ? this.getDragger() : (null), - // // !this.Pinned ? (null) : <div key="frag1"> {[ - // // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // // <div className="richTextMenu-divider" key="divider" /> - // // ]}</div>, - // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - // this.createColorButton(), - // this.createHighlighterButton(), - // this.createLinkButton(), - // this.createBrushButton(), - // <div className="collectionMenu-divider" key="divider 2" />, - // this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), - // this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), - // this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), - // this.createButton("indent", "Inset More", undefined, this.insetParagraph), - // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), - // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), - // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), - // ]}</div>; - - // const row2 = <div className="antimodeMenu-row row-2" key="row2"> - // {this.collapsed ? this.getDragger() : (null)} - // <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> - // <div className="collectionMenu-divider" key="divider 3" /> - // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { - // this.activeFontSize = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val); - // })), - // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { - // this.activeFontFamily = val; - // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val); - // })), - // <div className="collectionMenu-divider" key="divider 4" />, - // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), - // this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), - // this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), - // this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule) - // ]} - // </div> - // {/* <div key="collapser"> - // {<div key="collapser"> - // <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> - // <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> - // </button> - // </div> } - // <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> - // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - // </button> - // </div> */} - // </div>; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index b97141e92..42665830f 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); - + 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; @@ -285,8 +295,8 @@ export class RichTextRules { // create a hyperlink to a titled document // @(<doctitle>) - new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@:\.\? \-0-9]+)\)/), (state, match, start, end) => { - const docTitle = match[2]; + new InputRule(new RegExp(/@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => { + const docTitle = match[1]; const prefixLength = '@('.length; if (docTitle) { const linkToDoc = (target: Doc) => { @@ -307,7 +317,7 @@ export class RichTextRules { }; 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; @@ -325,19 +335,14 @@ export class RichTextRules { // [@{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\(\)]*))?\]/), + 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); - }; + const getTitledDoc = (docTitle: string) => DocServer.FindDocByTitle(docTitle); // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); @@ -346,44 +351,50 @@ export class RichTextRules { } else if (value) { 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 = docTitle ? getTitledDoc(docTitle) : undefined; - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, dataDoc }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, hideValue: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }, { inCode: true } ), - new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => { + // 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) => { - count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string))); + 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 b68acc8f8..ccf7de4a1 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -235,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 905146ee2..62b8b03d6 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -24,6 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = { // :: NodeSpec The top level document node. doc: { content: 'block+', + marks: '_', }, paragraph: ParagraphNodeSpec, @@ -120,7 +121,6 @@ export const nodes: { [index: string]: NodeSpec } = { ...ParagraphNodeSpec.attrs, level: { default: 1 }, }, - defining: true, parseDOM: [ { tag: 'h1', attrs: { level: 1 } }, { tag: 'h2', attrs: { level: 2 } }, @@ -131,8 +131,7 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node) { const dom = toParagraphDOM(node) as any; - const level = node.attrs.level || 1; - dom[0] = 'h' + level; + dom[0] = `h${node.attrs.level || 1}`; return dom; }, getAttrs(dom: any) { @@ -264,9 +263,8 @@ export const nodes: { [index: string]: NodeSpec } = { fieldKey: { default: '' }, docId: { default: '' }, hideKey: { default: false }, + hideValue: { default: false }, editable: { default: true }, - expanded: { default: null }, - dataDoc: { default: false }, }, leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), group: 'inline', @@ -332,12 +330,10 @@ export const nodes: { [index: string]: NodeSpec } = { ...orderedList, content: 'list_item+', group: 'block', + marks: '_', attrs: { bulletStyle: { default: 0 }, - mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet" - fontColor: { default: 'inherit' }, - fontSize: { default: undefined }, - fontFamily: { default: undefined }, + mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet", visibility: { default: true }, indent: { default: undefined }, }, @@ -377,9 +373,10 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; - const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : ''; - const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : ''; - const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : ''; + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ @@ -387,7 +384,7 @@ export const nodes: { [index: string]: NodeSpec } = { { 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `${fsize} ${ffam} ${fcol} ${marg}`, + style: `${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ]; @@ -399,7 +396,7 @@ export const nodes: { [index: string]: NodeSpec } = { class: `${map}-ol`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle, - style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`, + style: `list-style: none; ${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`, }, 0, ] @@ -423,16 +420,22 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, ], - toDOM(node: any) { + toDOM(node: Node) { + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', - { class: `${map}`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, + { class: `${map}`, style: `${fhigh} ${fsize} ${ffam} ${fcol} `, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle }, node.attrs.visibility ? 0 : [ 'span', - { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre` }, + { + style: `${fhigh} ${fsize} ${ffam} ${fcol} position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre`, + }, `${node.firstChild?.textContent}...`, ], ]; diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 87e1b69c3..a485ea4c3 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { OpenWhereMod } from '../DocumentView'; -import { ImageBox } from '../ImageBox'; +import { ImageBox, ImageEditorData } from '../ImageBox'; import './GenerativeFill.scss'; import Buttons from './GenerativeFillButtons'; import { BrushHandler } from './generativeFillUtils/BrushHandler'; @@ -419,8 +419,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // Closes the editor view const handleViewClose = () => { - ImageBox.setImageEditorOpen(false); - ImageBox.setImageEditorSource(''); + ImageEditorData.Open = false; + ImageEditorData.Source = ''; if (newCollectionRef.current) { DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); } diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index e34144fae..91fdb90fc 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[]; @@ -324,8 +324,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); progressiveReveal(true); // shows first progressive document, but without a transition effect + return 0; } - return 0; + return false; } return this.itemIndex; }; @@ -703,7 +704,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) { @@ -963,7 +964,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const func = () => { const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition); this._presTimer = setTimeout(() => { - if (!this.next()) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; + if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual; this.layoutDoc.presentation_status === PresStatus.Autoplay && func(); }, delay); }; @@ -1065,7 +1066,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } else if (doc.type !== DocumentType.PRES) { if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide'; - doc.presentation_targetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else. + doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else. doc.presentation_movement = PresMovement.Zoom; if (this._expandBoolean) doc.presentation_expandInlineButton = true; } diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 5b2aa1cde..28139eb14 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -61,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // Since this node is being rendered with a template, this method retrieves // the actual slide being rendered from the auto-generated rendering template @computed get slideDoc() { - return this._props.TemplateDataDocument ?? this.Document; + return DocCast(this.Document.rootDocument, this.Document); } // this is the document in the workspaces that is targeted by the slide |