diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/DashFieldView.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/DashFieldView.tsx | 343 |
1 files changed, 229 insertions, 114 deletions
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 5c4d850ad..9903d0e8a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,94 +1,105 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { NodeSelection } from 'prosemirror-state'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; +import { returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { Cast, DocCast } from '../../../../fields/Types'; +import { emptyFunction } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { FilterPanel } from '../../FilterPanel'; import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { OpenWhere } from '../DocumentView'; +import { OpenWhere } from '../OpenWhere'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -export class DashFieldView { - dom: HTMLDivElement; // container for label and value - root: any; - node: any; - tbox: FormattedTextBox; +@observer +export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { + // eslint-disable-next-line no-use-before-define + 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; + } - 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; - 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) { - e.stopPropagation(); - }; - this.dom.onkeydown = function (e: any) { - e.stopPropagation(); - }; - this.dom.onkeyup = function (e: any) { - e.stopPropagation(); - }; - this.dom.onmousedown = function (e: any) { - e.stopPropagation(); - }; + showFields = (e: React.MouseEvent) => { + DashFieldViewMenu.createFieldView(e); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleFieldHide = () => { + DashFieldViewMenu.toggleFieldHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; + toggleValueHide = () => { + DashFieldViewMenu.toggleValueHide(); + DashFieldViewMenu.Instance.fadeOut(true); + }; - this.root = ReactDOM.createRoot(this.dom); - this.root.render( - <DashFieldViewInternal - node={node} - unclickable={this.unclickable} - getPos={getPos} - fieldKey={node.attrs.fieldKey} - docId={node.attrs.docId} - width={node.attrs.width} - height={node.attrs.height} - hideKey={node.attrs.hideKey} - editable={node.attrs.editable} - tbox={tbox} - /> + @observable _fieldKey = ''; + + @action + public show = (x: number, y: number, fieldKey: string) => { + this._fieldKey = fieldKey; + this.jumpTo(x, y, true); + const hideMenu = () => { + this.fadeOut(true); + document.removeEventListener('pointerdown', hideMenu, true); + }; + document.addEventListener('pointerdown', hideMenu, true); + }; + render() { + return this.getElement( + <> + <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> + <button type="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 type="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 type="button" className="antimodeMenu-button" onPointerDown={this.toggleValueHide}> + <FontAwesomeIcon icon="hashtag" size="sm" /> + </button> + </Tooltip> + )} + </> ); } - destroy() { - setTimeout(() => { - try { - this.root.unmount(); - } catch {} - }); - } - deselectNode() { - this.dom.classList.remove('ProseMirror-selectednode'); - } - selectNode() { - this.dom.classList.add('ProseMirror-selectednode'); - } } - interface IDashFieldViewInternal { fieldKey: string; docId: string; hideKey: boolean; + hideValue: boolean; tbox: FormattedTextBox; width: number; height: number; editable: boolean; + nodeSelected: () => boolean; node: any; getPos: any; unclickable: () => boolean; @@ -101,18 +112,21 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi _fieldKey: string; _fieldRef = React.createRef<HTMLDivElement>(); @observable _dashDoc: Doc | undefined = undefined; - @observable _expanded = false; + @observable _expanded = this._props.nodeSelected(); constructor(props: IDashFieldViewInternal) { super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; - this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + this._textBoxDoc = this._props.tbox.Document; + const setDoc = action((doc: Doc) => { + this._dashDoc = doc; + }); if (this._props.docId) { - DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); + DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); } else { - this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; + setDoc(this._props.tbox.Document); } } @@ -126,38 +140,56 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi componentWillUnmount() { this._reactionDisposer?.(); } - return100 = () => 100; + isRowActive = () => (this._props.nodeSelected() || this._expanded) && this._props.editable; + finishEdit = action(() => { + if (this._expanded) { + this._expanded = false; + // if the edit finishes, then we want to lose focus on the textBox unless something else in the textBox got focus + // the timeout allows switching focus from one dashFieldView to another in the same text box + setTimeout(() => !this._props.tbox.ProseRef?.contains(document.activeElement) && this._props.tbox._props.onBlur?.()); + } + }); + 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(() => { + this._expanded = !this._props.editable ? !this._expanded : true; + })} + style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}> <SchemaTableCell Document={this._dashDoc} col={0} deselectCell={emptyFunction} selectCell={emptyFunction} - maxWidth={this._props.hideKey ? undefined : this.return100} - columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero} - selectedCell={() => [this._dashDoc!, 0]} + maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth} + columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero} + selectedCells={this.selectedCells} + selectedCol={returnZero} fieldKey={this._fieldKey} rowHeight={returnZero} - isRowActive={() => this._expanded && this._props.editable} + isRowActive={this.isRowActive} padding={0} getFinfo={emptyFunction} setColumnValues={returnFalse} - allowCRs={true} - oneLine={!this._expanded} - finishEdit={action(() => (this._expanded = false))} + setSelectedColumnValues={returnFalse} + allowCRs + oneLine={!this._expanded && !this._props.nodeSelected()} + finishEdit={this.finishEdit} transform={Transform.Identity} menuTarget={null} + autoFocus + rootSelected={this._props.tbox._props.rootSelected} /> </div> ); } - createPivotForField = (e: React.MouseEvent) => { - let container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); + createPivotForField = () => { + const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; @@ -173,21 +205,50 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } }; + toggleFieldHide = undoable( + action(() => { + const editor = this._props.tbox.EditorView!; + editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey })); + }), + '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 })); + }), + 'hideValue' + ); + + @computed get _hideKey() { + return this._props.hideKey && !this._expanded; + } + + @computed get _hideValue() { + return this._props.hideValue && !this._props.nodeSelected(); + } + // clicking on the label creates a pivot view collection of all documents // in the same collection. The pivot field is the fieldKey of this label - onPointerDownLabelSpan = (e: any) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, e => { + onPointerDownLabelSpan = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, moveEv => { DashFieldViewMenu.createFieldView = this.createPivotForField; - DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey); + DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide; + DashFieldViewMenu.toggleValueHide = this.toggleValueHide; + DashFieldViewMenu.Instance.show(moveEv.clientX, moveEv.clientY + 16, this._fieldKey); + const editor = this._props.tbox.EditorView!; + setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100); }); }; @undoBatch selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => { - event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value); + event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value === '-unset-' ? undefined : event.target.value); }; @computed get values() { + if (this._props.nodeSelected()) return []; const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []); return vals.strings.map(facet => ({ value: facet, label: facet })); @@ -196,21 +257,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi render() { return ( <div - className="dashFieldView" + className={`dashFieldView${this.isRowActive() ? '-active' : ''}`} ref={this._fieldRef} style={{ width: this._props.width, height: this._props.height, pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> - {this._props.hideKey ? null : ( + {this._hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey} + {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : (this._dashDoc?.title ?? '') + ':') + this._fieldKey} </span> )} - {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} + {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} {!this.values.length ? null : ( - <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}> + <select className="dashFieldView-select" tabIndex={-1} defaultValue={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> ))} @@ -220,39 +282,92 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi ); } } -@observer -export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: DashFieldViewMenu; - static createFieldView: (e: React.MouseEvent) => void = emptyFunction; - constructor(props: any) { - super(props); - DashFieldViewMenu.Instance = this; - } - - showFields = (e: React.MouseEvent) => { - DashFieldViewMenu.createFieldView(e); - DashFieldViewMenu.Instance.fadeOut(true); - }; - - @observable _fieldKey = ''; +export class DashFieldView { + dom: HTMLDivElement; // container for label and value + root: any; + node: any; + tbox: FormattedTextBox; + getpos: any; + @observable _nodeSelected = false; + NodeSelected = () => this._nodeSelected; - @action - public show = (x: number, y: number, fieldKey: string) => { - this._fieldKey = fieldKey; - this.jumpTo(x, y, true); - const hideMenu = () => { - this.fadeOut(true); - document.removeEventListener('pointerdown', hideMenu, true); + 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); + 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: KeyboardEvent) { + e.stopPropagation(); }; - document.addEventListener('pointerdown', hideMenu, true); - }; - render() { - return this.getElement( - <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> - <button className="antimodeMenu-button" onPointerDown={this.showFields}> - <FontAwesomeIcon icon="eye" size="lg" /> - </button> - </Tooltip> + this.dom.onkeydown = (e: KeyboardEvent) => { + e.stopPropagation(); + if (e.key === 'Tab') { + e.preventDefault(); + const editor = tbox.EditorView; + if (editor) { + const { state } = editor; + for (let i = this.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; + } + } + } + } + }; + this.dom.onkeyup = function (e: any) { + e.stopPropagation(); + }; + this.dom.onmousedown = function (e: any) { + e.stopPropagation(); + }; + + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashFieldViewInternal + node={node} + unclickable={this.unclickable} + getPos={getPos} + fieldKey={node.attrs.fieldKey} + docId={node.attrs.docId} + width={node.attrs.width} + height={node.attrs.height} + hideKey={node.attrs.hideKey} + hideValue={node.attrs.hideValue} + editable={node.attrs.editable} + nodeSelected={this.NodeSelected} + tbox={tbox} + /> ); } + destroy() { + setTimeout(() => { + try { + this.root.unmount(); + } catch { + /* empty */ + } + }); + } + deselectNode() { + runInAction(() => { + this._nodeSelected = false; + }); + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + setTimeout( + action(() => { + this._nodeSelected = true; + }), + 100 + ); + this.dom.classList.add('ProseMirror-selectednode'); + } } |