import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import "@webscopeio/react-textarea-autocomplete/style.css"; import { action, computed, observable, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast, ScriptCast, StrCast, BoolCast } from "../../../fields/Types"; import { returnEmptyString } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; import { InteractionUtils } from "../../util/InteractionUtils"; import { CompileScript, Scripting, ScriptParam } from "../../util/Scripting"; import { ScriptManager } from "../../util/ScriptManager"; import { ContextMenu } from "../ContextMenu"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { OverlayView } from "../OverlayView"; import { DocumentIconContainer } from "./DocumentIcon"; import "./ScriptingBox.scss"; import { TraceMobx } from "../../../fields/util"; const _global = (window /* browser */ || global /* node */) as any; const ScriptingSchema = createSchema({}); type ScriptingDocument = makeInterface<[typeof ScriptingSchema, typeof documentSchema]>; const ScriptingDocument = makeInterface(ScriptingSchema, documentSchema); @observer export class ScriptingBox extends ViewBoxAnnotatableComponent(ScriptingDocument) { private dropDisposer?: DragManager.DragDropDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined; 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 = Scripting.getGlobals(); @observable private _scriptingDescriptions: any = Scripting.getDescriptions(); @observable private _scriptingParams: any = Scripting.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); } // 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 StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], ""); } @computed({ keepAlive: true }) get functionName() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionName"], ""); } @computed({ keepAlive: true }) get functionDescription() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionDescription"], ""); } @computed({ keepAlive: true }) get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), []); } set rawScript(value) { this.dataDoc[this.props.fieldKey + "-rawScript"] = value; } set functionName(value) { this.dataDoc[this.props.fieldKey + "-functionName"] = value; } set functionDescription(value) { this.dataDoc[this.props.fieldKey + "-functionDescription"] = value; } set compileParams(value) { this.dataDoc[this.props.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 ""; } } @action componentDidMount() { this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript ?? 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.rootDoc.layoutKey = "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 = CompileScript(this.rawScript, { editable: true, transformer: DocumentIconContainer.getTransformer(), params, typecheck: false }); this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result) : 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 self. ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, 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 = Scripting.getGlobals(); this._scriptingDescriptions = Scripting.getDescriptions(); this._scriptingParams = Scripting.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) => { this.dataDoc[fieldKey] = de.complete.docDragData?.droppedDocuments[0]; e.stopPropagation(); } // 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.rootDoc, true); copy.x = NumCast(this.dataDoc.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 =