import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnAlways, returnTrue } from '../../../Utils'; import { Doc, Field, FieldResult } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { DocCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript } from '../../util/Scripting'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DocumentIconContainer } from './DocumentIcon'; import { OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; export type KVPScript = { script: CompiledScript; type: 'computed' | 'script' | false; onDelegate: boolean; }; @observer export class KeyValueBox extends ObservableReactComponent { public static LayoutString() { return FieldView.LayoutString(KeyValueBox, 'data'); } constructor(props: any) { super(props); makeObservable(this); } private _mainCont = React.createRef(); private _keyHeader = React.createRef(); private _keyInput = React.createRef(); private _valInput = React.createRef(); componentDidMount() { this._props.setContentViewBox?.(this); } isKeyValueBox = returnTrue; able = returnAlways; layout_fitWidth = returnTrue; onClickScriptDisable = returnAlways; @observable private rows: KeyValuePair[] = []; @observable _splitPercentage = 50; get fieldDocToLayout() { return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document; } @action onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { e.stopPropagation(); if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) { if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) { this._keyInput.current.value = ''; this._valInput.current.value = ''; document.body.focus(); } } } }; /** * this compiles a string as a script after parsing off initial characters that determine script parameters * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc * if the script then starts with a ':=', then it will be treated as ComputedField, * '$=', then it will just be a Script * @param value * @returns */ public static CompileKVPScript(rawvalue: string): KVPScript | undefined { const onDelegate = rawvalue.startsWith('='); rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false; rawvalue = type ? rawvalue.substring(2) : rawvalue; rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)'); const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`'; var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer()); if (!script.compiled) { script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer()); } return !script.compiled ? undefined : { script, type, onDelegate }; } public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); let field: Field | undefined; switch (type) { case 'computed': field = new ComputedField(script); break; // prettier-ignore case 'script': field = new ScriptField(script); break; // prettier-ignore default: { const _setCacheResult_ = (value: FieldResult) => { field = value as Field; if (setResult) setResult?.(value); else target[key] = field; }; const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log); if (!res.success) { if (key) target[key] = script.originalScript; return false; } field === undefined && (field = res.result); } } if (!key) return false; if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) { target[key] = field; return true; } return false; } @undoBatch public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { const script = this.CompileKVPScript(value); if (!script) return false; return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); } onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this._props.isSelected()) { e.stopPropagation(); } }; onPointerWheel = (e: React.WheelEvent): void => e.stopPropagation(); rowHeight = () => 30; @computed get createTable() { const doc = this.fieldDocToLayout; if (!doc) { return ( Loading... ); } const realDoc = doc; const ids: { [key: string]: string } = {}; const protos = Doc.GetAllPrototypes(doc); for (const proto of protos) { Object.keys(proto).forEach(key => { if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { ids[key] = key; } }); } const rows: JSX.Element[] = []; let i = 0; const self = this; const keys = Object.keys(ids).slice(); //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { for (const key of keys.sort((a: string, b: string) => { const a_ = a.split('_')[0]; const b_ = b.split('_')[0]; if (a_ < b_) return -1; if (a_ > b_) return 1; if (a === a_) return -1; if (b === b_) return 1; return a === b ? 0 : a < b ? -1 : 1; })) { rows.push( { if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); oldEl = el; if (el) self.rows.push(el); }; })()} keyWidth={100 - this._splitPercentage} rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')} key={key} keyName={key} /> ); } return rows; } @computed get newKeyValue() { return ( { this._keyInput.current!.select(); e.stopPropagation(); }} style={{ width: `${100 - this._splitPercentage}%` }}> { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this._splitPercentage}%` }}> ); } @action onDividerMove = (e: PointerEvent): void => { const nativeWidth = this._mainCont.current!.getBoundingClientRect(); this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); }; @action onDividerUp = (e: PointerEvent): void => { document.removeEventListener('pointermove', this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); }; onDividerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); document.addEventListener('pointermove', this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); }; getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true }); for (const row of rows) { const field = this.createFieldView(DocCast(this._props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); } return parent; } return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row._props.keyName; const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); fieldTemplate.title = metaKey; fieldTemplate.layout_fitWidth = true; fieldTemplate._xMargin = 10; fieldTemplate._yMargin = 10; fieldTemplate._width = 100; fieldTemplate._height = 40; fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); return fieldTemplate; }; inferType = (data: FieldResult, metaKey: string) => { const options = { _width: 300, _height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') { return FormattedTextBox.LayoutString(metaKey); } else if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } const first = DocCast(data[0]); if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } switch (first.data.constructor) { case RichTextField: return Docs.Create.TreeDocument([], options); case ImageField: return Docs.Create.MasonryDocument([], options); default: console.log(`Template for ${first.data.constructor} not supported!`); return undefined; } } else if (data instanceof ImageField) { return ImageBox.LayoutString(metaKey); } return new Doc(); }; specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const open = cm.findByDescription('Change Perspective...'); const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : []; openItems.push({ description: 'Default Perspective', event: () => { this._props.addDocTab(this._props.Document, OpenWhere.close); this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', }); !open && cm.addItem({ description: 'Change Perspective...', subitems: openItems, icon: 'external-link-alt' }); }; render() { const dividerDragger = this._splitPercentage === 0 ? null : (
); return (
{this.createTable} {this.newKeyValue}
Key Fields
{dividerDragger}
); } }