import { action, computed, IReactionDisposer, observable } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import { DataSym, Doc, DocListCast, Field } from "../../../../fields/Doc"; import { List } from "../../../../fields/List"; import { listSpec } from "../../../../fields/Schema"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../../fields/ScriptField"; import { Cast, StrCast } from "../../../../fields/Types"; import { DocServer } from "../../../DocServer"; import { CollectionViewType } from "../../collections/CollectionView"; import "./DashFieldView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; import { Tooltip } from "@material-ui/core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export class DashFieldView { _fieldWrapper: HTMLDivElement; // container for label and value constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey); this._fieldWrapper = document.createElement("div"); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.fontWeight = "bold"; this._fieldWrapper.style.position = "relative"; this._fieldWrapper.style.display = "inline-block"; this._fieldWrapper.textContent = node.attrs.fieldKey.startsWith("#") ? node.attrs.fieldKey : node.attrs.fieldKey + " " + strVal; this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; setTimeout(() => ReactDOM.render(, this._fieldWrapper)); (this as any).dom = this._fieldWrapper; } destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } selectNode() { } } interface IDashFieldViewInternal { fieldKey: string; docid: string; hideKey: boolean; tbox: FormattedTextBox; width: number; height: number; } @observer export class DashFieldViewInternal extends React.Component { _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; _fieldKey: string; _fieldStringRef = React.createRef(); @observable _dashDoc: Doc | undefined; constructor(props: IDashFieldViewInternal) { super(props); this._fieldKey = this.props.fieldKey; this._textBoxDoc = this.props.tbox.props.Document; if (this.props.docid) { DocServer.GetRefField(this.props.docid). then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { this._dashDoc = this.props.tbox.rootDoc; } } componentWillUnmount() { this._reactionDisposer?.(); } public static multiValueDelimeter = ";"; public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) { const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? (fieldKey === "PARAMS" ? textBoxDoc[fieldKey] : ""); const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal; return { boolVal: Cast(fval, "boolean", null), strVal: Field.toString(fval as Field) || "" } } // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { if (this._dashDoc) { const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey); // field value is a boolean, so use a checkbox or similar widget to display it if (boolVal === true || boolVal === false) { return { if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked; Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true); }} />; } else // field value is a string, so display it as an editable span { // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't // use React events. Essentially, React events occur after native events have been processed, so corresponding React events // will never fire because Prosemirror has handled the native events. So we add listeners for native events here. return { r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); r?.addEventListener("pointerdown", action(e => e.stopPropagation())); }} > {strVal} ; } } } // we need to handle all key events on the input span or else they will propagate to prosemirror. @action fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { if (e.key === "Enter") { // handle the enter key by "submitting" the current text to Dash's database. this.updateText(span.textContent!, true); e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view } if (e.key === "a" && (e.ctrlKey || e.metaKey)) { // handle ctrl-A to select all the text within the span if (window.getSelection) { const range = document.createRange(); range.selectNodeContents(span); window.getSelection()!.removeAllRanges(); window.getSelection()!.addRange(range); } e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected } e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror. } @action updateText = (nodeText: string, forceMatch: boolean) => { if (nodeText) { const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText; // look for a document whose id === the fieldKey being displayed. If there's a match, then that document // holds the different enumerated values for the field in the titles of its collected documents. // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. DocServer.GetRefField(this._fieldKey).then(options => { let modText = ""; (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title))); if (modText) { // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key else if (nodeText.startsWith(":=")) { this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); } else if (nodeText.startsWith("=:=")) { Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); } else { if (Number(newText).toString() === newText) { if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); Doc.SetInPlace(this._dashDoc!, this._fieldKey, newText, true); } else { const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { const strVal = splits.length > 1 ? new List(splits) : newText; if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true); } } } }); } } createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.ContainingCollectionView; while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) { container = container.props.ContainingCollectionView; } if (container) { const alias = Doc.MakeAlias(container.props.Document); alias._viewType = CollectionViewType.Time; let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField)); if (!list) { alias._columnHeaders = 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")); alias._pivotField = this._fieldKey.startsWith("#") ? "#" : this._fieldKey; this.props.tbox.props.addDocTab(alias, "add:right"); } } // 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) => { DashFieldViewMenu.createFieldView = this.createPivotForField; DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16); }); } render() { return
{this.props.hideKey ? (null) : {this._fieldKey} } {this.props.fieldKey.startsWith("#") ? (null) : this.fieldValueContent}
; } } @observer export class DashFieldViewMenu extends AntimodeMenu { static Instance: DashFieldViewMenu; static createFieldView: (e: React.MouseEvent) => void = emptyFunction; constructor(props: any) { super(props); DashFieldViewMenu.Instance = this; } @action showFields = (e: React.MouseEvent) => { DashFieldViewMenu.createFieldView(e); DashFieldViewMenu.Instance.fadeOut(true); } public show = (x: number, y: number) => { this.jumpTo(x, y, true); const hideMenu = () => { this.fadeOut(true); document.removeEventListener("pointerdown", hideMenu); } document.addEventListener("pointerdown", hideMenu) } render() { const buttons = [ {"Remove Link Anchor"}}> , ]; return this.getElement(buttons); } }