/* 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, 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, DocCast } from '../../../../fields/Types'; import { emptyFunction } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; 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 '../OpenWhere'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; @observer export class DashFieldViewMenu extends AntimodeMenu { // 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; } 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); }; @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( <> {`Show Pivot Viewer for '${this._fieldKey}'`}}> {this._fieldKey.startsWith('#') ? null : ( Toggle view of field key}> )} {this._fieldKey.startsWith('#') ? null : ( Toggle view of field value}> )} ); } } 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; } @observer export class DashFieldViewInternal extends ObservableReactComponent { _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; _fieldKey: string; _fieldRef = React.createRef(); @observable _dashDoc: Doc | undefined = undefined; @observable _expanded = this._props.nodeSelected(); constructor(props: IDashFieldViewInternal) { super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; this._textBoxDoc = this._props.tbox.Document; const setDoc = action((doc: Doc) => { this._dashDoc = doc; }); if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc)); } else { setDoc(this._props.tbox.Document); } } componentDidMount() { this._reactionDisposer = reaction( () => (this._dashDoc ? Field.toKeyValueString(this._dashDoc, this._props.fieldKey) : undefined), keyvalue => keyvalue && this._props.tbox.tryUpdateDoc(true) ); } componentWillUnmount() { this._reactionDisposer?.(); } 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 : (
{ this._expanded = !this._props.editable ? !this._expanded : true; })} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}>
); } createPivotForField = () => { const container = this._props.tbox.DocumentView?.().containerViewPath?.().lastElement(); if (container) { const embedding = Doc.MakeEmbedding(container.Document); embedding._type_collection = CollectionViewType.Time; const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders'; let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField)); if (!list) { embedding[colHdrKey] = list = new List(); } list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; this._props.tbox._props.addDocTab(embedding, OpenWhere.addRight); } }; 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: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, returnFalse, moveEv => { DashFieldViewMenu.createFieldView = this.createPivotForField; 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 | undefined) => { 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 })); } render() { return (
{this._hideKey ? null : ( {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : (this._dashDoc?.title ?? '') + ':') + this._fieldKey} )} {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent} {!this.values.length ? null : ( )}
); } } export class DashFieldView { dom: HTMLDivElement; // container for label and value root: any; node: any; tbox: FormattedTextBox; getpos: any; @observable _nodeSelected = false; NodeSelected = () => this._nodeSelected; unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { makeObservable(this); 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(); }; 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( ); } 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'); } }