diff options
Diffstat (limited to 'src/client/views/nodes')
26 files changed, 405 insertions, 321 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a0a64ab59..2800ea200 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { OmitKeys, numberRange } from '../../../Utils'; @@ -12,14 +12,16 @@ import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { DocComponent } from '../DocComponent'; -import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProvider'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldViewProps } from './FieldView'; -export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps { +/// 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 +const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition']; +interface freeFormProps { x: number; y: number; z: number; @@ -33,102 +35,17 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView opacity?: number; highlight?: boolean; transition?: string; - RenderCutoffProvider: (doc: Doc) => boolean; - CollectionFreeFormView: CollectionFreeFormView; -} -@observer -export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> { - constructor(props: CollectionFreeFormDocumentViewWrapperProps) { - super(props); - makeObservable(this); - } - @observable X = this.props.x; - @observable Y = this.props.y; - @observable Z = this.props.z; - @observable ZIndex = this.props.zIndex; - @observable Rotation = this.props.rotation; - @observable Opacity = this.props.opacity; - @observable BackgroundColor = this.props.backgroundColor; - @observable Color = this.props.color; - @observable Highlight = this.props.highlight; - @observable Width = this.props.width; - @observable Height = this.props.height; - @observable AutoDim = this.props.autoDim; - @observable Transition = this.props.transition; - CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking - RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking - - get Document() { - return this._props.Document; - } - @computed get WrapperKeys() { - return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', '')) - .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore - } - - // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing. - // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes - w_X = () => this.X; // prettier-ignore - w_Y = () => this.Y; // prettier-ignore - w_Z = () => this.Z; // prettier-ignore - w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore - w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore - w_Opacity = () => this.Opacity; // prettier-ignore - w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore - w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore - w_Highlight = () => this.Highlight; // prettier-ignore - w_Width = () => this.Width; // prettier-ignore - w_Height = () => this.Height; // prettier-ignore - w_AutoDim = () => this.AutoDim; // prettier-ignore - w_Transition = () => this.Transition; // prettier-ignore - - PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore - PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore - - componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) { - super.componentDidUpdate(prevProps); - this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); - } - render() { - const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function }); - return ( - <CollectionFreeFormDocumentView - {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore - {...layoutProps} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - /> - ); - } } export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - w_X: () => number; - w_Y: () => number; - w_Z: () => number; - w_ZIndex?: () => number; - w_Rotation?: () => number; - w_Color: () => string; - w_BackgroundColor: () => string; - w_Opacity: () => number | undefined; - w_Highlight: () => boolean | undefined; - w_Transition: () => string | undefined; - w_Width: () => number; - w_Height: () => number; - PanelWidth: () => number; - PanelHeight: () => number; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } - @observer -export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() { - constructor(props: CollectionFreeFormDocumentViewProps) { - super(props); - makeObservable(this); - } +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() { get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; } // prettier-ignore + public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; public static animFields: { key: string; val?: number }[] = [ { key: 'x' }, { key: 'y' }, @@ -145,16 +62,53 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames - get CollectionFreeFormView() { - return this._props.CollectionFreeFormView; + constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) { + super(props); + makeObservable(this); + } + + get WrapperKeys() { + // each of these keys is set by the CollectionView and passed via props. however, if any one of these props changes + // (or any other prop), then it's as if they all change. + // Anything that accesses these props will then invalidate unncessarily. + // To avoid this, we copy these prop values into local observables. Now when 'props' changes, nothing invalidates. + // Instead, we copy each values into its observable which ohnly triggers invalidations if the new value is different + // than the old value, and then only things that access that observable will invalidate. + return freeFormPropsKeys + .map(key => ({upper:key[0].toUpperCase() + key.substring(1), lower:key})); // prettier-ignore + } + @observable X = this.props.x; + @observable Y = this.props.y; + @observable Z = this.props.z; + @observable ZIndex = this.props.zIndex; + @observable Rotation = this.props.rotation; + @observable Opacity = this.props.opacity; + @observable BackgroundColor = this.props.backgroundColor; + @observable Color = this.props.color; + @observable Highlight = this.props.highlight; + @observable Width = this.props.width; + @observable Height = this.props.height; + @observable AutoDim = this.props.autoDim; + @observable Transition = this.props.transition; + + 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]))); } + 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 + 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) { - case StyleProp.Opacity: return this._props.w_Opacity(); // only change the opacity for this specific document, not its children - case StyleProp.BackgroundColor: return this._props.w_BackgroundColor(); - case StyleProp.Color: return this._props.w_Color(); + 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; } // prettier-ignore } return this._props.styleProvider?.(doc, props, property); @@ -253,14 +207,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF nudge = (x: number, y: number) => { const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y); - this.Document.x = this._props.w_X() + locX; - this.Document.y = this._props.w_Y() + locY; + this.Document.x = this.X + locX; + this.Document.y = this.Y + locY; }; screenToLocalTransform = () => this._props .ScreenToLocalTransform() - .translate(-this._props.w_X(), -this._props.w_Y()) - .rotateDeg(-(this._props.w_Rotation?.() || 0)); + .translate(-this.X, -this.Y) + .rotateDeg(-(this.Rotation || 0)); returnThis = () => this; /// this indicates whether the doc view is activated because of its relationshop to a group @@ -273,27 +227,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; - public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; render() { TraceMobx(); - const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore + return ( <div className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName} style={{ - width: this._props.PanelWidth(), - height: this._props.PanelHeight(), - transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, - transition: this._props.w_Transition?.() || StrCast(this.Document.dataTransition), - zIndex: this._props.w_ZIndex?.(), - display: this._props.w_Width?.() ? undefined : 'none', + 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), + zIndex: this.ZIndex, + display: this.Width ? undefined : 'none', }}> - {this._props.RenderCutoffProvider(this.Document) ? ( - <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> + {this.RenderCutoffProvider(this.Document) ? ( + <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView - {...passOnProps} - DataTransition={this._props.w_Transition} + {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore + DataTransition={this.DataTransition} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 116dc48a6..ef8c045cc 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -6,7 +6,7 @@ import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } import { Doc, Opt } from '../../../fields/Doc'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { DocUtils, Docs } from '../../documents/Documents'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; @@ -135,7 +135,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this, e, e => { - const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move'); + const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move); de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { this.clearDoc(which); return addDocument(doc); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index a3132dc6e..6b5738790 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -29,6 +29,10 @@ } } + .liveSchema-checkBox { + margin-bottom: -35px; + } + .dataviz-sidebar { position: absolute; right: 0; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index e453bcee0..66a08f13e 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -11,7 +11,7 @@ import { listSpec } from '../../../../fields/Schema'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; -import { Docs } from '../../../documents/Documents'; +import { DocUtils, Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; @@ -27,6 +27,7 @@ import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { Checkbox } from '@mui/material'; export enum DataVizView { TABLE = 'table', @@ -85,6 +86,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records static dataset = new ObservableMap<string, { [key: string]: string }[]>(); + // when a dataset comes from schema view, this stores the original dataset to refer back to + // href : dataset + static datasetSchemaOG = new ObservableMap<string, { [key: string]: string }[]>(); + private _vizRenderer: LineChart | Histogram | PieChart | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); @@ -103,10 +108,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im return Cast(this.dataDoc[this.fieldKey], CsvField); } @computed.struct get axes() { - return StrListCast(this.layoutDoc.dataViz_axes); + return StrListCast(this.layoutDoc._dataViz_axes); } - - selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes)); + selectAxes = (axes: string[]) => (this.layoutDoc._dataViz_axes = new List<string>(axes)); + @computed.struct get titleCol() { + return StrCast(this.layoutDoc._dataViz_titleCol); + } + selectTitleCol = (titleCol: string) => (this.layoutDoc._dataViz_titleCol = titleCol); @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { @@ -258,6 +266,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; const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); if (!keys) return; @@ -274,10 +283,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); + } + 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 => { - current && DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, 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)!); + } }, { fireImmediately: true } ); @@ -299,6 +341,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im layoutDoc: this.layoutDoc, records: this.records, axes: this.axes, + titleCol: this.titleCol, //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9, @@ -306,7 +349,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} vizBox={this} />; case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} @@ -354,6 +397,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im GPTPopup.Instance.addDoc = this.sidebarAddDocument; }; + @action + changeLiveSchemaCheckbox = () => { + this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive + } + render() { const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( @@ -379,27 +427,15 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> </div> - {/* <CollectionFreeFormView - ref={this._ffref} - {...this._props} - setContentView={emptyFunction} - renderDepth={this._props.renderDepth - 1} - fieldKey={this.annotationKey} - styleProvider={this._props.styleProvider} - isAnnotationOverlay={true} - annotationLayerHostsContent={false} - PanelWidth={this._props.PanelWidth} - PanelHeight={this._props.PanelHeight} - select={emptyFunction} - isAnyChildContentActive={returnFalse} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument}> - {this.renderVizView} - </CollectionFreeFormView> */} + {(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> + ) : null} {this.renderVizView} + <div className="dataviz-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }} onPointerDown={this.onPointerDown}> <SidebarAnnos ref={this._sidebarRef} diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 116a45623..41ce637ac 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -19,8 +19,6 @@ margin-bottom: -20px; } .asHistogram-checkBox { - // display: flex; - // flex-direction: row; align-items: left; align-self: left; align-content: left; diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index a7f292104..6672603f3 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -20,6 +20,7 @@ export interface HistogramProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -63,17 +64,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { var ax0 = this._props.axes[0]; - if (/\d/.test(this._props.records[0][ax0])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])){ this.numericalXData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } var ax0 = this._props.axes[0]; var ax1 = this._props.axes[1]; - if (/\d/.test(this._props.records[0][ax0])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this._props.records[0][ax1])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax1])) { this.numericalYData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); @@ -89,9 +90,6 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @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 rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -454,6 +452,18 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { : '' ); selected = selected.substring(0, selected.length - 2) + ' }'; + if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ + selected+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { + if (this._props.axes[1]){ + if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; + } + else selected+= each[this._props.titleCol] + ", "; + } + }) + selected = selected.slice(0,-1).slice(0,-1); + } } var selectedBarColor; var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index bea1b8222..e093ec648 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -28,6 +28,7 @@ export interface LineChartProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -46,7 +47,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - @observable _currSelected: SelectedDataPoint | undefined = undefined; + @observable _currSelected: any | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates constructor(props: any) { super(props); @@ -235,21 +236,16 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } } - // TODO: nda - can use d3.create() to create html element instead of appending drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { // clearing tooltip and the current chart d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); - const { xMin, xMax, yMin, yMax } = rangeVals; + var { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } - // creating the x and y scales - const xScale = scaleCreatorNumerical(xMin, xMax, 0, width); - const yScale = scaleCreatorNumerical(0, yMax, height, 0); - // adding svg const margin = this._props.margin; const svg = (this._lineChartSvg = d3 @@ -261,24 +257,71 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); + var validSecondData; + if (this._props.axes.length>2){ // for when there are 2 lines on the chart + var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + validSecondData = next.filter(d => { + if (!d.x || Number.isNaN(d.x) || !d.y || Number.isNaN(d.y)) return false; + return true; + }); + var secondDataRange = minMaxRange([validSecondData]); + if (secondDataRange.xMax!>xMax) xMax = secondDataRange.xMax; + if (secondDataRange.yMax!>yMax) yMax = secondDataRange.yMax; + if (secondDataRange.xMin!<xMin) xMin = secondDataRange.xMin; + if (secondDataRange.yMin!<yMin) yMin = secondDataRange.yMin; + } + + // creating the x and y scales + const xScale = scaleCreatorNumerical(xMin!, xMax!, 0, width); + const yScale = scaleCreatorNumerical(0, yMax!, height, 0); + const lineGen = createLineGenerator(xScale, yScale); + // create x and y grids xGrid(svg.append('g'), height, xScale); yGrid(svg.append('g'), width, yScale); xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); + if (validSecondData) { + drawLine(svg.append('path'), validSecondData, lineGen, true); + this.drawDataPoints(validSecondData, 0, xScale, yScale); + svg.append('path').attr("stroke", "red"); + + // legend + var color = d3.scaleOrdinal() + .range(["black", "blue"]) + .domain([this._props.axes[1], this._props.axes[2]]) + svg.selectAll("mydots") + .data([this._props.axes[1], this._props.axes[2]]) + .enter() + .append("circle") + .attr("cx", 5) + .attr("cy", function(d,i){ return -30 + i*15}) + .attr("r", 7) + .style("fill", function(d){ return color(d)}) + svg.selectAll("mylabels") + .data([this._props.axes[1], this._props.axes[2]]) + .enter() + .append("text") + .attr("x", 25) + .attr("y", function(d,i){ return -30 + i*15}) + .style("fill", function(d){ return color(d)}) + .text(function(d){ return d}) + .attr("text-anchor", "left") + .style("alignment-baseline", "middle") + } + // get valid data points const data = dataSet[0]; - const lineGen = createLineGenerator(xScale, yScale); var validData = data.filter(d => { - var valid = true; Object.keys(data[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; + if (!d[key] || Number.isNaN(d[key])) return false; }); - return valid; + return true; }); + // draw the plot line - drawLine(svg.append('path'), validData, lineGen); + drawLine(svg.append('path'), validData, lineGen, false); // draw the datapoint circle this.drawDataPoints(validData, 0, xScale, yScale); @@ -291,7 +334,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { const xPos = d3.pointer(e)[0]; const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; - if (!d0) return; + if (d0) this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); @@ -327,7 +370,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { svg.append('text') .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) - .attr('y', -20) + .attr('y', -30) .attr('height', 20) .attr('width', 20) .style('text-anchor', 'middle') @@ -356,6 +399,17 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; + var selectedTitle = ""; + if (this._currSelected && this._props.titleCol){ + selectedTitle+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + var mapThisEntry = false; + if (this._currSelected.x==each[this._props.axes[0]] && this._currSelected.y==each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected.y==each[this._props.axes[0]] && this._currSelected.x==each[this._props.axes[1]]) mapThisEntry = true; + if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ", "; + }) + selectedTitle = selectedTitle.slice(0,-1).slice(0,-1); + } if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> @@ -375,9 +429,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { {selectedPt != 'none' ? ( <div className={'selected-data'}> {`Selected: ${selectedPt}`} + {`${selectedTitle}`} <Button onClick={e => { - console.log('test plzz'); this._props.vizBox.sidebarBtnDown; this._props.vizBox.sidebarAddDocument; }}></Button> diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index a922a200b..fc23f47de 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -19,6 +19,7 @@ export interface PieChartProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -339,14 +340,26 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { + selected = '{ '; const sliceTitle = this._currSelected[this._props.axes[0]]; curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - selected = '{ '; Object.keys(this._currSelected).map(key => { key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; }); selected = selected.substring(0, selected.length - 2); selected += ' }'; + if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ + selected+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { + if (this._props.axes[1]){ + if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; + } + else selected+= each[this._props.titleCol] + ", "; + } + }) + selected = selected.slice(0,-1).slice(0,-1); + } } else selected = 'none'; var selectedSliceColor; var sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index ed44d9269..1b239b5e5 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -18,7 +18,9 @@ interface TableBoxProps { layoutDoc: Doc; records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; + selectTitleCol: (titleCol: string) => void; axes: string[]; + titleCol: string; width: number; height: number; margin: { @@ -83,14 +85,12 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return this._props.docView?.()?.screenToViewTransform().Scale || 1; } @computed get rowHeight() { - console.log('scale = ' + this.viewScale + ' table = ' + this._tableHeight + ' ids = ' + this._tableDataIds.length); return (this.viewScale * this._tableHeight) / this._tableDataIds.length; } @computed get startID() { return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0; } @computed get endID() { - console.log('start = ' + this.startID + ' container = ' + this._tableContainerHeight + ' scale = ' + this.viewScale + ' row = ' + this.rowHeight); return Math.ceil(this.startID + (this._tableContainerHeight * this.viewScale) / (this.rowHeight || 1)); } @action handleScroll = () => { @@ -155,11 +155,18 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }, emptyFunction, action(e => { - const newAxes = this._props.axes; - if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 1) newAxes[1] = col; - else newAxes.push(col); - this._props.selectAxes(newAxes); + if (e.shiftKey){ + if (this._props.titleCol == col) this._props.titleCol = ""; + else this._props.titleCol = col; + this._props.selectTitleCol(this._props.titleCol); + } + 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 newAxes.push(col); + this._props.selectAxes(newAxes); + } }) ); }; @@ -213,8 +220,15 @@ 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.lastElement() === col ? 'darkred' : undefined, - background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this._props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + 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 fontWeight: 'bolder', border: '3px solid black', }} @@ -236,7 +250,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - const colSelected = this._props.axes.length > 1 ? this._props.axes[0] == col || this._props.axes[1] == col : this._props.axes.length > 0 ? this._props.axes[0] == col : false; + 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; 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/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index 10bfb0c64..336935d23 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -61,6 +61,6 @@ export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, wi ); }; -export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => { - p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen); +export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>, extra: boolean) => { + p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('stroke', extra? 'blue' : 'black').attr('class', 'line').attr('d', lineGen); }; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index c4dab16fb..5421c1b50 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -180,7 +180,6 @@ .documentView-titleWrapper, .documentView-titleWrapper-hover { - overflow: hidden; color: $black; transform-origin: top left; top: 0; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 73c13b5dd..29266bd8e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,11 +1,10 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Dropdown, DropdownType, Type } from 'browndash-components'; import { Howl } from 'howler'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; -import { Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; +import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -48,6 +47,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; +import { FieldsDropdown } from '../FieldsDropdown'; interface Window { MediaRecorder: MediaRecorder; } @@ -413,7 +413,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); } }; @@ -518,7 +518,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.layoutDoc._layout_hideContextMenu && Doc.noviceMode) { + if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false); @@ -702,7 +702,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document panelHeight = () => this._props.PanelHeight() - this.headerMargin; screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler; - setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = height); + setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -784,28 +784,25 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); - fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { - const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields)); + fieldsDropdown = (placeholder: string) => { return ( - <div style={{ width: dropdownWidth }}> - <div - ref={action((r: any) => r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} - onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> - <Dropdown - activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))} - selectedVal={placeholder} - setSelectedVal={onChange} - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} - type={Type.TERT} - closeOnSelect={true} - dropdownType={DropdownType.SELECT} - items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))} - width={100} - fillWidth - /> - </div> + <div + ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} + onPointerDown={action(e => (this._changingTitleField = true))} + style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> + <FieldsDropdown + Document={this.Document} + placeholder={placeholder} + selectFunc={action((field: string | number) => { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + })} + menuClose={action(() => (this._changingTitleField = false))} + /> </div> ); }; @@ -839,22 +836,7 @@ 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 - : this.fieldsDropdown( - [StrCast(this.layoutDoc.layout_showTitle)], - dropdownWidth, - StrCast(this.layoutDoc.layout_showTitle).split(':')[0], - action((field: string | number) => { - if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = field; - } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = field; - } - this._changingTitleField = false; - }), - action(() => (this._changingTitleField = false)) - )} + {!dropdownWidth ? null : <div style={{ width: dropdownWidth }}>{this.fieldsDropdown(showTitle)}</div>} <div style={{ width: `calc(100% - ${dropdownWidth}px)`, @@ -864,10 +846,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }}> <EditableView ref={this._titleRef} - contents={showTitle - .split(';') - .map(field => targetDoc[field.trim()]?.toString()) - .join(' \\ ')} + contents={ + showTitle + .split(';') + .map(field => Field.toString(targetDoc[field.trim()] as Field)) + .join(' \\ ') || '-unset-' + } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} @@ -1500,8 +1484,8 @@ ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { let created = false; const matchedDocs = matchedTags .filter(tagDoc => !Doc.AreProtosEqual(collection, tagDoc)) - .map(tagDoc => { - let embedding = collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc)); + .reduce((aset, tagDoc) => { + let embedding = Array.from(aset).find(doc => Doc.AreProtosEqual(tagDoc, doc)) ?? collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc)); if (!embedding) { embedding = Doc.MakeEmbedding(tagDoc); embedding.x = wid; @@ -1510,9 +1494,11 @@ ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { wid += NumCast(tagDoc._width); created = true; } - return embedding; - }); + Doc.SetContainer(embedding, collection); + aset.add(embedding); + return aset; + }, new Set<Doc>()); - created && (collection[DocData].data = new List<Doc>(matchedDocs)); + created && (collection[DocData].data = new List<Doc>(Array.from(matchedDocs))); return true; }); diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 2e03a766a..50d4c7c78 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -11,6 +11,7 @@ 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>() { @@ -57,8 +58,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @action keyPressed = (e: KeyboardEvent) => { - const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); - const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', '')); + const _height = DivHeight(this._ref.current!.element.current); + const _width = DivWidth(this._ref.current!.element.current); if (e.key === 'Enter') { const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', { title: '# math', diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 3577cc8d9..f02ad7300 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,13 +1,13 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -81,9 +81,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { } }; - // Determining UI Specs + /** + * this chooses the appropriate title for the label + * if the Document is a template, then we use the title of the data doc that it renders + * otherwise, we use the Document's title itself. + */ @computed get label() { - return StrCast(this.dataDoc.icon_label, StrCast(this.Document.title)); + return StrCast(this.Document.isTemplateDoc ? this.dataDoc.title : this.Document.title); } Icon = (color: string, iconFalse?: boolean) => { let icon; @@ -308,13 +312,16 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { ); } + @observable _hackToRecompute = 0; // bcz: ugh ... <Toggle>'s toggleStatus initializes but doesn't track its value after a click. so a click that does nothing to the toggle state will toggle the button anyway. this forces the Toggle to re-read the ToggleStatus value. + @computed get toggleButton() { // Determine the type of toggle button const buttonText = StrCast(this.dataDoc.buttonText); const tooltip = StrCast(this.Document.toolTip); const script = ScriptCast(this.Document.onClick); - const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; + const double = ScriptCast(this.Document.onDoubleClick); + const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); @@ -330,7 +337,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} - onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnTrue, + emptyFunction, + action((e, doubleTap) => { + (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + this._hackToRecompute = this._hackToRecompute + 1; + }) + ) + } /> ); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 923aead64..251235b93 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -141,6 +141,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const targetField = Doc.LayoutFieldKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { + added = true; this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index fd3074a88..be20b5934 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -17,12 +17,8 @@ import './LabelBox.scss'; import { PinProps, PresBox } from './trails'; import { Docs } from '../../documents/Documents'; -export interface LabelBoxProps extends FieldViewProps { - label?: string; -} - @observer -export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { +export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } @@ -32,7 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; - constructor(props: LabelBoxProps) { + constructor(props: FieldViewProps) { super(props); makeObservable(this); } @@ -45,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { } @computed get Title() { - return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : Field.toString(this.dataDoc[this.fieldKey] as Field); + return Field.toString(this.dataDoc[this.fieldKey] as Field); } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 00e1f04c5..0a4325d8c 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -5,7 +5,7 @@ import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { SelectionManager } from '../../util/SelectionManager'; import { ViewBoxBaseComponent } from '../DocComponent'; @@ -54,7 +54,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.Document]); - dragData.dropAction = 'embed'; + dragData.dropAction = dropActionType.embed; dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index c185c66fc..927e6fad4 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -379,7 +379,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -592,7 +592,7 @@ 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', + 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), diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 8a5bd7ce6..3eb051dbf 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -232,7 +232,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -466,7 +466,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> /// 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.selectedPin?.map) || StrCast(this.Document.map) || 'map location', + text: (StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location') as any, config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index f6d94ce05..e38a42b29 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -11,7 +11,7 @@ import { Upload } from '../../../../server/SharedMediaTypes'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { Presentation } from '../../../util/TrackMovements'; import { undoBatch } from '../../../util/UndoManager'; @@ -251,7 +251,7 @@ ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: b ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) { if (DocCast(value.doc)) { - DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY); + DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY); value.e.preventDefault(); return true; } diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 89650889d..d9d0dbe3e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -130,7 +130,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }) ); - observer.observe(document.getElementsByClassName('scriptingBox')[0]); + observer.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]); } @action @@ -811,22 +811,20 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() render() { TraceMobx(); return ( - <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}> - <div className="scriptingBox-outerDiv" onWheel={e => this._props.isSelected() && e.stopPropagation()}> - {this._paramSuggestion ? ( - <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}> - {' '} - {this._scriptSuggestedParams}{' '} - </div> - ) : null} - {!this._applied && !this._function ? this.renderScriptingInputs : null} - {this._applied && !this._function ? this.renderParamsInputs() : null} - {!this._applied && this._function ? this.renderFunctionInputs() : null} - - {!this._applied && !this._function ? this.renderScriptingTools() : null} - {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null} - {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null} - </div> + <div className="scriptingBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined} onWheel={e => this._props.isSelected() && e.stopPropagation()}> + {this._paramSuggestion ? ( + <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}> + {' '} + {this._scriptSuggestedParams}{' '} + </div> + ) : null} + {!this._applied && !this._function ? this.renderScriptingInputs : null} + {this._applied && !this._function ? this.renderParamsInputs() : null} + {!this._applied && this._function ? this.renderFunctionInputs() : null} + + {!this._applied && !this._function ? this.renderScriptingTools() : null} + {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null} + {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null} </div> ); } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 40647feff..b2ae7201c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -31,6 +31,7 @@ import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; +import { dropActionType } from '../../util/DragManager'; /** * VideoBox @@ -335,7 +336,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl 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); - setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); + setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 2c5398e40..c9340edc0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -14,7 +14,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -83,7 +83,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return this.webField?.toString() || ''; } @computed get _urlHash() { - return ""+ (stringHash(this._url)??''); + return '' + (stringHash(this._url) ?? ''); } @computed get scrollHeight() { return Math.max(NumCast(this.layoutDoc._height), this._scrollHeight); @@ -782,7 +782,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem className="webBox-htmlSpan" ref={action((r: any) => { if (r) { - this._scrollHeight = Number(getComputedStyle(r).height.replace('px', '')); + this._scrollHeight = DivHeight(r); this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE); } })} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index ec0b76aa8..5c4d850ad 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -4,24 +4,23 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import Select from 'react-select'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast } from '../../../../fields/Types'; +import { Cast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; -import { undoable, undoBatch } from '../../../util/UndoManager'; +import { undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; +import { FilterPanel } from '../../FilterPanel'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import { FilterPanel } from '../../FilterPanel'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -29,7 +28,7 @@ export class DashFieldView { node: any; tbox: FormattedTextBox; - unclickable = () => !this.tbox._props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + 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) { this.node = node; this.tbox = tbox; @@ -108,12 +107,12 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; - this._textBoxDoc = this._props.tbox.Document; + this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { - this._dashDoc = this._props.tbox.Document; + this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; } } @@ -202,7 +201,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi style={{ width: this._props.width, height: this._props.height, - pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', + pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> @@ -210,11 +209,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi </span> )} {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} - <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}> - {this.values.map(val => ( - <option value={val.value}>{val.label}</option> - ))} - </select> + {!this.values.length ? null : ( + <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}> + {this.values.map(val => ( + <option value={val.value}>{val.label}</option> + ))} + </select> + )} </div> ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f2c4c6c8f..1ff7274f8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -23,14 +23,14 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; @@ -67,7 +67,6 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; -import Select from 'react-select'; // import * as applyDevTools from 'prosemirror-dev-tools'; @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -100,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; private _ignoreScroll = false; - private _lastText = ''; private _hadDownFocus = false; private _focusSpeed: Opt<number>; private _keymap: any = undefined; @@ -306,7 +304,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -374,9 +372,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // 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))) { const numstring = NumCast(dataDoc[this.fieldKey], null); - dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); - dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited + dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText ? 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 { @@ -623,7 +622,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps docId: draggedDoc[Id], float: 'unset', }); - if (!['embed', 'copy'].includes((dropAction ?? '') as any)) { + if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; } else { added = true; @@ -758,7 +757,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; - const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); + const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); if (preview) this._showSidebar = true; else { this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; @@ -939,9 +938,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'Make Default Layout', event: () => { if (!this.layoutDoc.isTemplateDoc) { - const title = StrCast(this.Document.title); - this.Document.title = 'text'; - MakeTemplate(this.Document, true, title); + MakeTemplate(this.Document); } Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); @@ -1458,39 +1455,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); - if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { - const selLoadChar = FormattedTextBox.SelectOnLoadChar; + const selLoadChar = FormattedTextBox.SelectOnLoadChar; + if (selectOnLoad) { FormattedTextBox.SelectOnLoad = undefined; + FormattedTextBox.SelectOnLoadChar = ''; + } + if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - const tr = this._editorView.state.tr - .setStoredMarks(storedMarks) - .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size) - .setStoredMarks(storedMarks); + const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); + const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1); + const tr = tr2.setStoredMarks(storedMarks); + 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); } } - selectOnLoad && this._editorView!.focus(); + if (selectOnLoad) { + FormattedTextBox.DontSelectInitialText = false; + this._editorView!.focus(); + } if (this._props.isContentActive()) this.prepareForTyping(); - if (this._editorView) { - const tr = this._editorView.state.tr; - const { from, to } = tr.selection; - // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated. - if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); - - if (FormattedTextBox.PasteOnLoad) { - const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); - FormattedTextBox.PasteOnLoad = undefined; - pdfAnchorId && this.addPdfReference(pdfAnchorId); - } + if (this._editorView && FormattedTextBox.PasteOnLoad) { + const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); + FormattedTextBox.PasteOnLoad = undefined; + pdfAnchorId && this.addPdfReference(pdfAnchorId); } - FormattedTextBox.DontSelectInitialText = false; } // 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. @@ -2019,7 +2014,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } render() { TraceMobx(); - const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index b8f6575dd..0d4f9ec78 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -36,6 +36,7 @@ import { FocusViewOptions, FieldView, FieldViewProps } 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[]; @@ -1074,7 +1075,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); - removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc); + removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p); getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this._props.PanelHeight() - 40; /** @@ -2222,10 +2223,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // prettier-ignore switch (layout) { case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); - case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }) + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) } }; @@ -2607,7 +2608,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} - childDragAction="move" + childDragAction={dropActionType.move} setContentViewBox={emptyFunction} //childLayoutFitWidth={returnTrue} childOpacity={returnOne} |
