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 './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';
import { CollectionViewType } from '../../../documents/DocumentTypes';
export class DashFieldView {
dom: 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.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
this.dom.style.fontWeight = 'bold';
this.dom.style.position = 'relative';
this.dom.style.display = 'inline-block';
this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal;
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();
};
setTimeout(() => ReactDOM.render(, this.dom));
(this as any).dom = this.dom;
}
destroy() {
ReactDOM.unmountComponentAtNode(this.dom);
}
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);
}}
/>
);
} // field value is a string, so display it as an editable span
else {
// 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 (