let ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnAlways, returnEmptyString } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DragManager } from '../../util/DragManager'; import { ScriptManager } from '../../util/ScriptManager'; import { CompileScript, ScriptParam } from '../../util/Scripting'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { ContextMenu } from '../ContextMenu'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { EditableView } from '../EditableView'; import { OverlayView } from '../OverlayView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { DocumentIconContainer } from './DocumentIcon'; import './ScriptingBox.scss'; const _global = (window /* browser */ || global) /* node */ as any; @observer export class ScriptingBox extends ViewBoxAnnotatableComponent() { private dropDisposer?: DragManager.DragDropDisposer; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); } private _overlayDisposer?: () => void; private _caretPos = 0; @observable private _errorMessage: string = ''; @observable private _applied: boolean = false; @observable private _function: boolean = false; @observable private _spaced: boolean = false; @observable private _scriptKeys: any = ScriptingGlobals.getGlobals(); @observable private _scriptingDescriptions: any = ScriptingGlobals.getDescriptions(); @observable private _scriptingParams: any = ScriptingGlobals.getParameters(); @observable private _currWord: string = ''; @observable private _suggestions: string[] = []; @observable private _suggestionBoxX: number = 0; @observable private _suggestionBoxY: number = 0; @observable private _lastChar: string = ''; @observable private _suggestionRef: any = React.createRef(); @observable private _scriptTextRef: any = React.createRef(); @observable private _selection: any = 0; @observable private _paramSuggestion: boolean = false; @observable private _scriptSuggestedParams: any = ''; @observable private _scriptParamsText: any = ''; constructor(props: any) { super(props); makeObservable(this); if (!this.compileParams.length) { const params = ScriptCast(this.dataDoc[this._props.fieldKey])?.script.options.params as { [key: string]: any }; if (params) { this.compileParams = Array.from(Object.keys(params)) .filter(p => !p.startsWith('_')) .map(key => key + ':' + params[key]); } } } // vars included in fields that store parameters types and names and the script itself @computed({ keepAlive: true }) get paramsNames() { return this.compileParams.map(p => p.split(':')[0].trim()); } @computed({ keepAlive: true }) get paramsTypes() { return this.compileParams.map(p => p.split(':')[1].trim()); } @computed({ keepAlive: true }) get rawScript() { return ScriptCast(this.dataDoc[this.fieldKey])?.script.originalScript ?? ''; } @computed({ keepAlive: true }) get functionName() { return StrCast(this.dataDoc[this.fieldKey + '-functionName'], ''); } @computed({ keepAlive: true }) get functionDescription() { return StrCast(this.dataDoc[this.fieldKey + '-functionDescription'], ''); } @computed({ keepAlive: true }) get compileParams() { return Cast(this.dataDoc[this.fieldKey + '-params'], listSpec('string'), []); } set rawScript(value) { this.dataDoc[this.fieldKey] = new ScriptField(undefined, undefined, value); } set functionName(value) { this.dataDoc[this.fieldKey + '-functionName'] = value; } set functionDescription(value) { this.dataDoc[this.fieldKey + '-functionDescription'] = value; } set compileParams(value) { this.dataDoc[this.fieldKey + '-params'] = new List(value); } getValue(result: any, descrip: boolean) { if (typeof result === 'object') { const text = descrip ? result[1] : result[2]; return text !== undefined ? text : ''; } else { return ''; } } onClickScriptDisable = returnAlways; @action componentDidMount() { this._props.setContentView?.(this); this.rawText = this.rawScript; const observer = new _global.ResizeObserver( action((entries: any) => { const area = document.querySelector('textarea'); if (area) { for (const {} of entries) { const getCaretCoordinates = require('textarea-caret'); const caret = getCaretCoordinates(area, this._selection); this.resetSuggestionPos(caret); } } }) ); observer.observe(document.getElementsByClassName('scriptingBox')[0]); } @action resetSuggestionPos(caret: any) { if (!this._suggestionRef.current || !this._scriptTextRef.current) return; const suggestionWidth = this._suggestionRef.current.offsetWidth; const scriptWidth = this._scriptTextRef.current.offsetWidth; const top = caret.top; const x = this.dataDoc.x; let left = caret.left; if (left + suggestionWidth > x + scriptWidth) { const diff = left + suggestionWidth - (x + scriptWidth); left = left - diff; } this._suggestionBoxX = left; this._suggestionBoxY = top; } componentWillUnmount() { this._overlayDisposer?.(); } protected createDashEventsTarget = (ele: HTMLDivElement, dropFunc: (e: Event, de: DragManager.DropEvent) => void) => { //used for stacking and masonry view if (ele) { this.dropDisposer?.(); this.dropDisposer = DragManager.MakeDropTarget(ele, dropFunc, this.layoutDoc); } }; // only included in buttons, transforms scripting UI to a button @action onFinish = () => (this.layoutDoc.layout_fieldKey = 'layout'); // displays error message @action onError = (error: any) => { this._errorMessage = error?.message ? error.message : error?.map((entry: any) => entry.messageText).join(' ') || ''; }; // checks if the script compiles using CompileScript method and inputting params @action onCompile = () => { const params: ScriptParam = {}; this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim())); const result = !this.rawText.trim() ? ({ compiled: false, errors: undefined } as any) : CompileScript(this.rawText, { editable: true, transformer: DocumentIconContainer.getTransformer(), params, typecheck: false, }); this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined; this.onError(result.compiled ? undefined : result.errors); return result.compiled; }; // checks if the script compiles and then runs the script @action onRun = () => { if (this.onCompile()) { const bindings: { [name: string]: any } = {}; this.paramsNames.forEach(key => (bindings[key] = this.dataDoc[key])); // binds vars so user doesnt have to refer to everything as this. ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ ...bindings, this: this.Document }, this.onError); } }; // checks if the script compiles and switches to applied UI @action onApply = () => { if (this.onCompile()) { this._applied = true; } }; @action onEdit = () => { this._errorMessage = ''; this._applied = false; this._function = false; }; @action onSave = () => { if (this.onCompile()) { this._function = true; } else { this._errorMessage = 'Can not save script, does not compile'; } }; @action onCreate = () => { this._errorMessage = ''; if (this.functionName.length === 0) { this._errorMessage = 'Must enter a function name'; return false; } if (this.functionName.indexOf(' ') > 0) { this._errorMessage = 'Name can not include spaces'; return false; } if (this.functionName.indexOf('.') > 0) { this._errorMessage = "Name can not include '.'"; return false; } this.dataDoc.name = this.functionName; this.dataDoc.description = this.functionDescription; //this.dataDoc.parameters = this.compileParams; this.dataDoc.script = this.rawScript; ScriptManager.Instance.addScript(this.dataDoc); this._scriptKeys = ScriptingGlobals.getGlobals(); this._scriptingDescriptions = ScriptingGlobals.getDescriptions(); this._scriptingParams = ScriptingGlobals.getParameters(); }; // overlays document numbers (ex. d32) over all documents when clicked on onFocus = () => { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); }; // sets field of the corresponding field key (param name) to be dropped document @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { if (de.complete.docDragData) { de.complete.docDragData.droppedDocuments.forEach(doc => (this.dataDoc[fieldKey] = doc)); e.stopPropagation(); return true; } return false; }; // deletes a param from all areas in which it is stored @action onDelete = (num: number) => { this.dataDoc[this.paramsNames[num]] = undefined; this.compileParams.splice(num, 1); return true; }; // sets field of the param name to the selected value in drop down box @action viewChanged = (e: React.ChangeEvent, name: string) => { //@ts-ignore const val = e.target.selectedOptions[0].value; this.dataDoc[name] = val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true'; }; // creates a copy of the script document onCopy = () => { const copy = Doc.MakeCopy(this.Document, true); copy.x = NumCast(this.Document.x) + NumCast(this.dataDoc._width); this._props.addDocument?.(copy); }; // adds option to create a copy to the context menu specificContextMenu = (): void => { const existingOptions = ContextMenu.Instance.findByDescription('Options...'); const options = existingOptions && 'subitems' in existingOptions ? existingOptions.subitems : []; options.push({ description: 'Create a Copy', event: this.onCopy, icon: 'copy' }); !existingOptions && ContextMenu.Instance.addItem({ description: 'Options...', subitems: options, icon: 'hand-point-right' }); }; renderFunctionInputs() { const descriptionInput =