aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/DashFieldView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util/DashFieldView.tsx')
-rw-r--r--src/client/util/DashFieldView.tsx312
1 files changed, 130 insertions, 182 deletions
diff --git a/src/client/util/DashFieldView.tsx b/src/client/util/DashFieldView.tsx
index 73d05009c..a135db003 100644
--- a/src/client/util/DashFieldView.tsx
+++ b/src/client/util/DashFieldView.tsx
@@ -1,58 +1,79 @@
-import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { IReactionDisposer, observable, runInAction, computed, action } from "mobx";
import { Doc, DocListCast, Field } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { listSpec } from "../../new_fields/Schema";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { ComputedField } from "../../new_fields/ScriptField";
import { Cast, StrCast } from "../../new_fields/Types";
-import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { CollectionViewType } from "../views/collections/CollectionView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import React = require("react");
import * as ReactDOM from 'react-dom';
import "./DashFieldView.scss";
+import { observer } from "mobx-react";
export class DashFieldView {
_fieldWrapper: HTMLDivElement; // container for label and value
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- this._fieldWrapper = document.createElement("p");
+ 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";
- ReactDOM.render(<DashFieldViewInternal node={node} view={view} getPos={getPos} tbox={tbox} />, this._fieldWrapper);
+ 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(); };
+
+ ReactDOM.render(<DashFieldViewInternal
+ fieldKey={node.attrs.fieldKey}
+ docid={node.attrs.docid}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ view={view}
+ getPos={getPos}
+ tbox={tbox}
+ />, this._fieldWrapper);
(this as any).dom = this._fieldWrapper;
}
destroy() {
ReactDOM.unmountComponentAtNode(this._fieldWrapper);
}
+ selectNode() { }
}
interface IDashFieldViewInternal {
- node: any,
- view: any,
- getPos: any,
- tbox: FormattedTextBox
+ fieldKey: string;
+ docid: string;
+ view: any;
+ getPos: any;
+ tbox: FormattedTextBox;
+ width: number;
+ height: number;
}
+
+@observer
export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> {
_reactionDisposer: IReactionDisposer | undefined;
- _textBoxDoc?: Doc; //Added "?""
- @observable _dashDoc: Doc | undefined;
- _fieldKey?: string; //Added "?" and added "as string"
+ _textBoxDoc: Doc;
+ _fieldKey: string;
_options: Doc[] = [];
+ _fieldStringRef = React.createRef<HTMLSpanElement>();
+ @observable _showEnumerables: boolean = false;
+ @observable _dashDoc: Doc | undefined;
constructor(props: IDashFieldViewInternal) {
- super(props)
- this._fieldKey = this.props.node.attrs.fieldKey;
+ super(props);
+ this._fieldKey = this.props.fieldKey;
this._textBoxDoc = this.props.tbox.props.Document;
- if (this.props.node.attrs.docid) {
- DocServer.GetRefField(this.props.node.attrs.docid).
+ if (this.props.docid) {
+ DocServer.GetRefField(this.props.docid).
then(async dashDoc => dashDoc instanceof Doc && runInAction(() => this.setDashDoc(dashDoc)));
} else {
this.setDashDoc(this.props.tbox.props.DataDoc || this.props.tbox.dataDoc);
@@ -61,114 +82,104 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
componentWillUnmount() {
this._reactionDisposer?.();
}
- componentDidMount() {
- var elementFieldCheck = document.getElementById("fieldCheckId") as HTMLInputElement;
- if (elementFieldCheck) {
- this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes
- const dashVal = this._dashDoc?.[this._fieldKey as string];
- return StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this.props.tbox.props.Document)[this._fieldKey as string] : dashVal;
- }, fval => {
- const boolVal = Cast(fval, "boolean", null);
- if (boolVal === true || boolVal === false) {
- elementFieldCheck.checked = boolVal;
- } else {
- // elementFieldCheck.innerHTML = Field.toString(fval as Field) || "";
- }
- elementFieldCheck.style.display = (boolVal === true || boolVal === false) ? "inline-block" : "none";
- elementFieldCheck.style.display = !(boolVal === true || boolVal === false) ? "inline-block" : "none";
- }, { fireImmediately: true });
+
+ // set the display of the field's value (checkbox for booleans, span of text for strings)
+ @computed get fieldValueContent() {
+ if (this._dashDoc) {
+ const dashVal = this._dashDoc[this._fieldKey];
+ const fval = StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal;
+ const boolVal = Cast(fval, "boolean", null);
+ const strVal = Field.toString(fval as Field) || "";
+
+ // field value is a boolean, so use a checkbox of similar widget to display it
+ if (boolVal === true || boolVal === false) {
+ return <input
+ className="dashFieldView-fieldCheck"
+ type="checkbox"
+ onChange={e => this._dashDoc![this._fieldKey] = e.target.checked}
+ />;
+ }
+ 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 <span contentEditable={true} ref={r => {
+ r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r));
+ r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false));
+ r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true));
+ }}>
+ {strVal}
+ </span>
+ }
}
}
setDashDoc = (doc: Doc) => {
this._dashDoc = doc;
- if (this._options?.length && !this._dashDoc[this._fieldKey as string]) {
- this._dashDoc[this._fieldKey as string] = StrCast(this._options[0].title);
+ if (this._options?.length && !this._dashDoc[this._fieldKey]) {
+ this._dashDoc[this._fieldKey] = StrCast(this._options[0].title);
}
// NOTE: if the field key starts with "@", then the actual field key is stored in the field 'fieldKey' (removing the @).
- this._fieldKey = this._fieldKey?.startsWith("@") ? StrCast(this.props.tbox.props.Document[StrCast(this._fieldKey as string).substring(1)]) : this._fieldKey as string;
- // var elementlabelSpan = document.getElementById("labelSpanId") as HTMLElement;
- // elementlabelSpan.innerHTML = `${this._fieldKey}: `;
- // const fieldVal = Cast(this._dashDoc?.[this._fieldKey], "boolean", null);
- // var elementfieldCheck = document.getElementById("fieldCheckId") as HTMLElement;
- // elementfieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
- // elementfieldCheck.style.display = !(fieldVal === true || fieldVal === false) ? "inline-block" : "none";
- };
-
- updateText = (forceMatch: boolean) => {
- var elementEnumarables = document.getElementById("enumarablesId") as HTMLElement;
- elementEnumarables.style.display = "none";
- var elementfieldSpan = document.getElementById("fieldSpanId") as HTMLElement;
- const newText = elementfieldSpan.innerText.startsWith(":=") || elementfieldSpan.innerText.startsWith("=:=") ? ":=-computed-" : elementfieldSpan.innerText;
-
- // 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 as string).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)));
- var elementfieldSpan = document.getElementById("fieldSpanId") as HTMLElement;
- if (modText) {
- // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
- Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey as string, []);
- } // 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 (elementfieldSpan.innerText.startsWith(":=")) {
- this._dashDoc![this._fieldKey as string] = ComputedField.MakeFunction(elementfieldSpan.innerText.substring(2));
- } else if (elementfieldSpan.innerText.startsWith("=:=")) {
- Doc.Layout(this.props.tbox.props.Document)[this._fieldKey as string] = ComputedField.MakeFunction(elementfieldSpan.innerText.substring(3));
- } else {
- this._dashDoc![this._fieldKey as string] = newText;
- }
- });
- };
-
- onPointerDownEnumerables = async (e: any) => {
- e.stopPropagation();
- var elementfieldSpan = document.getElementById("fieldSpanId") as HTMLElement;
- const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey as string, [{ title: elementfieldSpan.innerText }]);
- collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
- };
-
- onChangefieldCheck = (e: any) => {
- this._dashDoc![this._fieldKey as string] = e.target.checked;
- };
-
- onKeyPressfieldSpan = function (e: any) { e.stopPropagation(); };
-
- onKeyUpfieldSpan = function (e: any) { e.stopPropagation(); };
-
- onMouseDownfieldSpan = function (e: any) {
- e.stopPropagation();
- var element = document.getElementById("enumerables") as HTMLElement;
- element.style.display = "inline-block";
- };
-
- onBlurfieldSpan = (e: any) => { this.updateText(false); }; //Pas importé
+ //this._fieldKey = this._fieldKey?.startsWith("@") ? StrCast(this._textBoxDoc[StrCast(this._fieldKey as string).substring(1)]) : this._fieldKey as string;
+ }
- onKeyDownfieldSpan = (e: any) => {
- e.stopPropagation();
- if ((e.key === "a" && e.ctrlKey) || (e.key === "a" && e.metaKey)) {
+ // 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.
+ e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+ 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();
- var elementfieldSpan = document.getElementById("fieldSpanId") as HTMLElement;
-
- range.selectNodeContents(elementfieldSpan);
+ range.selectNodeContents(span);
window.getSelection()!.removeAllRanges();
window.getSelection()!.addRange(range);
}
- e.preventDefault();
+ e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
}
- if (e.key === "Enter") {
- e.preventDefault();
- var elementfieldSpan = document.getElementById("fieldSpanId") as HTMLElement;
+ e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
+ }
- e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey as string, [{ title: elementfieldSpan.innerText }]);
- this.updateText(true); //added this
+ @action
+ updateText = (nodeText: string, forceMatch: boolean) => {
+ this._showEnumerables = false;
+ 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.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
+ this._dashDoc![this._fieldKey] = modText;
+ } // 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![this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
+ } else if (nodeText.startsWith("=:=")) {
+ Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
+ } else {
+ this._dashDoc![this._fieldKey] = newText;
+ }
+ });
}
- };
+ }
+
+ onPointerDownEnumerables = async (e: any) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
+ collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
+ }
- onPointerDownLabelSpan = (e: any) => {
+ onPointerDownLabelSpan = (e: any) => {
e.stopPropagation();
let container = this.props.tbox.props.ContainingCollectionView;
while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
@@ -181,91 +192,28 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
if (!list) {
alias.schemaColumns = list = new List<SchemaHeaderField>();
}
- list.map(c => c.heading).indexOf(this._fieldKey as string) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb"));
+ 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 as string;
+ alias._pivotField = this._fieldKey;
this.props.tbox.props.addDocTab(alias, "onRight");
}
- };
-
- destroy() {
- this._reactionDisposer?.();
}
- selectNode() { }
render() {
+ return <div className="dashFieldView" style={{
+ width: this.props.width,
+ height: this.props.height,
+ }}>
+ <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
+ {this._fieldKey}
+ </span>
- const fieldStyle = {
- width: this.props.node.attrs.width,
- height: this.props.node.attrs.height,
- };
-
- const fieldCheckStyle = {
- minWidth: "12px",
- position: 'relative' as 'relative',
- display: 'none',
- backgroundColor: "rgba(155, 155, 155, 0.24)"
- };
-
- const fieldSpanStyle = {
- minWidth: "12px",
- position: 'relative' as 'relative',
- display: 'none',
- backgroundColor: "rgba(155, 155, 155, 0.24)"
- };
-
- const labelSpanStyle = {
- position: 'relative' as 'relative',
- display: 'inline-block',
- backgroundColor: "rgba(155, 155, 155, 0.44)",
- fontSize: "small",
- title: "click to see related tags"
- };
-
- const fieldCheckId = Utils.GenerateGuid();
- const fieldSpanId = Utils.GenerateGuid();
-
- return (
- <div className="fieldWrapper" style={fieldStyle}>
-
- <span
- className="labelSpan"
- id='labelSpanId'
- style={labelSpanStyle}
- onPointerDown={this.onPointerDownLabelSpan}
- //innerHTML= {this._fieldKey}
- >
- </span>
-
- <input
- className="fieldCheck"
- id={fieldCheckId}
- type="checkbox"
- style={fieldCheckStyle}
- onChange={this.onChangefieldCheck}>
- </input>
-
- <div
- className="fieldSpan"
- id={fieldSpanId}
- contentEditable="true"
- style={fieldSpanStyle}
- onBlur={this.onBlurfieldSpan}
- onKeyDown={this.onKeyDownfieldSpan}
- onKeyPress={this.onKeyPressfieldSpan}
- onKeyUp={this.onKeyUpfieldSpan}
- onMouseDown={this.onMouseDownfieldSpan}
- >
- </div>
-
- <div
- className="enumerablesStyle"
- id="enumerablesId"
- onPointerDown={this.onPointerDownEnumerables}>
+ <div className="dashFieldView-fieldSpan">
+ {this.fieldValueContent}
+ </div>
- </div>
+ {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
- </div >
- )
+ </div >;
}
} \ No newline at end of file