diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/.DS_Store | bin | 6148 -> 8196 bytes | |||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 1 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 96 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 10 | ||||
-rw-r--r-- | src/client/util/DropConverter.ts | 3 | ||||
-rw-r--r-- | src/client/util/Import & Export/DirectoryImportBox.tsx | 36 | ||||
-rw-r--r-- | src/client/util/LinkManager.ts | 3 | ||||
-rw-r--r-- | src/client/util/ScriptManager.ts | 104 | ||||
-rw-r--r-- | src/client/util/Scripting.ts | 66 | ||||
-rw-r--r-- | src/client/views/EditableView.tsx | 16 | ||||
-rw-r--r-- | src/client/views/GestureOverlay.tsx | 4 | ||||
-rw-r--r-- | src/client/views/OverlayView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ScriptingBox.scss | 202 | ||||
-rw-r--r-- | src/client/views/nodes/ScriptingBox.tsx | 797 | ||||
-rw-r--r-- | src/fields/ScriptField.ts | 2 | ||||
-rw-r--r-- | src/server/database.ts | 1 |
17 files changed, 1209 insertions, 136 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 5b35884bd..d2050d4be 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 06d35038a..7ba21b2f6 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -34,5 +34,6 @@ export enum DocumentType { COMPARISON = "comparison", // before/after view with slider (view of 2 images) LINKDB = "linkdb", // database of links ??? why do we have this + SCRIPTDB = "scriptdb", // database of scripts RECOMMENDATION = "recommendation", // view of a recommendation }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 8d867348f..03355b487 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,54 +1,53 @@ -import { CollectionView } from "../views/collections/CollectionView"; -import { CollectionViewType } from "../views/collections/CollectionView"; -import { AudioBox } from "../views/nodes/AudioBox"; -import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; -import { ImageBox } from "../views/nodes/ImageBox"; -import { KeyValueBox } from "../views/nodes/KeyValueBox"; -import { PDFBox } from "../views/nodes/PDFBox"; -import { ScriptingBox } from "../views/nodes/ScriptingBox"; -import { VideoBox } from "../views/nodes/VideoBox"; -import { WebBox } from "../views/nodes/WebBox"; -import { OmitKeys, JSONUtils, Utils } from "../../Utils"; -import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast, HeightSym, WidthSym } from "../../fields/Doc"; -import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField"; +import { runInAction } from "mobx"; +import { extname } from "path"; +import { DateField } from "../../fields/DateField"; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; import { HtmlField } from "../../fields/HtmlField"; +import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; -import { Cast, NumCast, StrCast, FieldValue } from "../../fields/Types"; +import { ProxyField } from "../../fields/Proxy"; +import { RichTextField } from "../../fields/RichTextField"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { ComputedField, ScriptField } from "../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { MessageStore } from "../../server/Message"; +import { OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; -import { DateField } from "../../fields/DateField"; -import { YoutubeBox } from "../apis/youtube/YoutubeBox"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; -import { DocumentManager } from "../util/DocumentManager"; -import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox"; import { Scripting } from "../util/Scripting"; -import { LabelBox } from "../views/nodes/LabelBox"; -import { SliderBox } from "../views/nodes/SliderBox"; -import { FontIconBox } from "../views/nodes/FontIconBox"; -import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; -import { PresBox } from "../views/nodes/PresBox"; -import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { ProxyField } from "../../fields/Proxy"; +import { UndoManager } from "../util/UndoManager"; import { DocumentType } from "./DocumentTypes"; -import { RecommendationsBox } from "../views/RecommendationsBox"; -import { PresElementBox } from "../views/presentationview/PresElementBox"; -import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; -import { QueryBox } from "../views/nodes/QueryBox"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { ContextMenu } from "../views/ContextMenu"; +import { ContextMenuProps } from "../views/ContextMenuItem"; +import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; +import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; +import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; -import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../views/InkingStroke"; -import { InkField } from "../../fields/InkField"; -import { RichTextField } from "../../fields/RichTextField"; -import { extname } from "path"; -import { MessageStore } from "../../server/Message"; -import { ContextMenuProps } from "../views/ContextMenuItem"; -import { ContextMenu } from "../views/ContextMenu"; +import { FontIconBox } from "../views/nodes/FontIconBox"; +import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; +import { ImageBox } from "../views/nodes/ImageBox"; +import { KeyValueBox } from "../views/nodes/KeyValueBox"; +import { LabelBox } from "../views/nodes/LabelBox"; import { LinkBox } from "../views/nodes/LinkBox"; +import { PDFBox } from "../views/nodes/PDFBox"; +import { PresBox } from "../views/nodes/PresBox"; +import { QueryBox } from "../views/nodes/QueryBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; -import { ComparisonBox } from "../views/nodes/ComparisonBox"; -import { runInAction } from "mobx"; -import { UndoManager } from "../util/UndoManager"; +import { ScriptingBox } from "../views/nodes/ScriptingBox"; +import { SliderBox } from "../views/nodes/SliderBox"; +import { VideoBox } from "../views/nodes/VideoBox"; +import { WebBox } from "../views/nodes/WebBox"; +import { PresElementBox } from "../views/presentationview/PresElementBox"; +import { RecommendationsBox } from "../views/RecommendationsBox"; +import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; +import { YoutubeBox } from "../apis/youtube/YoutubeBox"; +import { DocumentManager } from "../util/DocumentManager"; +import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; const path = require('path'); export interface DocumentOptions { @@ -262,6 +261,11 @@ export namespace Docs { layout: { view: EmptyBox, dataField: defaultDataKey }, options: { childDropAction: "alias", title: "Global Link Database" } }], + [DocumentType.SCRIPTDB, { + data: new List<Doc>(), + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Script Database" } + }], [DocumentType.SCRIPTING, { layout: { view: ScriptingBox, dataField: defaultDataKey } }], @@ -361,6 +365,13 @@ export namespace Docs { } /** + * A collection of all scripts in the database + */ + export function MainScriptDocument() { + return Prototypes.get(DocumentType.SCRIPTDB); + } + + /** * This is a convenience method that is used to initialize * prototype documents for the first time. * @@ -724,6 +735,11 @@ export namespace Docs { } export function ButtonDocument(options?: DocumentOptions) { + // const btn = InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); + // btn.layoutKey = "layout_onClick"; + // btn.height = 250; + // btn.width = 200; + // btn.layout_onClick = ScriptingBox.LayoutString("onClick"); return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b0cea9947..49af892c9 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -703,6 +703,7 @@ export class CurrentUserUtils { this.setupDefaultPresentation(doc); // presentation that's initially triggered await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); + doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); @@ -735,6 +736,9 @@ export class CurrentUserUtils { } } -Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); }); -Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); }); -Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); });
\ No newline at end of file +Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); }, + "initializes the Mobile inking document", "(userDoc: Doc)"); +Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); }, + "initializes the Mobile upload document", "(userDoc: Doc)"); +Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); }, + "creates a new workspace when called");
\ No newline at end of file diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 752c1cfc5..ea1769d85 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -76,4 +76,5 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data.droppedDocuments[i] = dbox; }); } -Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); });
\ No newline at end of file +Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }, + "converts the dropped data to buttons", "(dragData: any)");
\ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 25c556697..af6c57e68 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,33 +1,33 @@ -import "fs"; -import React = require("react"); -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { action, observable, runInAction, computed, reaction, IReactionDisposer } from "mobx"; -import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; -import Measure, { ContentRect } from "react-measure"; import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCloudUploadAlt, faPlus, faTag } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { BatchedArray } from "array-batcher"; +import "fs"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; -import { Utils } from "../../../Utils"; -import { DocumentManager } from "../DocumentManager"; +import * as path from 'path'; +import Measure, { ContentRect } from "react-measure"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; -import { Cast, BoolCast, NumCast } from "../../../fields/Types"; import { listSpec } from "../../../fields/Schema"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import "./DirectoryImportBox.scss"; -import { Networking } from "../../Network"; -import { BatchedArray } from "array-batcher"; -import * as path from 'path'; +import { BoolCast, Cast, NumCast } from "../../../fields/Types"; import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes"; +import { Utils } from "../../../Utils"; +import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { Networking } from "../../Network"; +import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; +import { DocumentManager } from "../DocumentManager"; +import "./DirectoryImportBox.scss"; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; +import React = require("react"); const unsupported = ["text/html", "text/plain"]; @observer -export default class DirectoryImportBox extends React.Component<FieldViewProps> { +export class DirectoryImportBox extends React.Component<FieldViewProps> { private selector = React.createRef<HTMLInputElement>(); @observable private top = 0; @observable private left = 0; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 95528e25a..47b2541bd 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -205,4 +205,5 @@ export class LinkManager { } } -Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); });
\ No newline at end of file +Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, + "creates a link to inputted document", "(doc: any)");
\ No newline at end of file diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts new file mode 100644 index 000000000..785e63d9a --- /dev/null +++ b/src/client/util/ScriptManager.ts @@ -0,0 +1,104 @@ +import { Doc, DocListCast } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { Scripting } from "./Scripting"; +import { StrCast, Cast } from "../../fields/Types"; +import { listSpec } from "../../fields/Schema"; +import { Docs } from "../documents/Documents"; + +export class ScriptManager { + + static _initialized = false; + private static _instance: ScriptManager; + public static get Instance(): ScriptManager { + return this._instance || (this._instance = new this()); + } + private constructor() { + if (!ScriptManager._initialized) { + ScriptManager._initialized = true; + this.getAllScripts().forEach(scriptDoc => ScriptManager.addScriptToGlobals(scriptDoc)); + } + } + + public get ScriptManagerDoc(): Doc | undefined { + return Docs.Prototypes.MainScriptDocument(); + } + public getAllScripts(): Doc[] { + const sdoc = ScriptManager.Instance.ScriptManagerDoc; + if (sdoc) { + const docs = DocListCast(sdoc.data); + return docs; + } + return []; + } + + public addScript(scriptDoc: Doc): boolean { + + console.log("in add script method"); + + const scriptList = this.getAllScripts(); + scriptList.push(scriptDoc); + if (ScriptManager.Instance.ScriptManagerDoc) { + ScriptManager.Instance.ScriptManagerDoc.data = new List<Doc>(scriptList); + ScriptManager.addScriptToGlobals(scriptDoc); + console.log("script added"); + return true; + } + return false; + } + + public deleteScript(scriptDoc: Doc): boolean { + + console.log("in delete script method"); + + if (scriptDoc.name) { + Scripting.removeGlobal(StrCast(scriptDoc.name)); + } + const scriptList = this.getAllScripts(); + const index = scriptList.indexOf(scriptDoc); + if (index > -1) { + scriptList.splice(index, 1); + if (ScriptManager.Instance.ScriptManagerDoc) { + ScriptManager.Instance.ScriptManagerDoc.data = new List<Doc>(scriptList); + return true; + } + } + return false; + } + + public static addScriptToGlobals(scriptDoc: Doc): void { + + Scripting.removeGlobal(StrCast(scriptDoc.name)); + + const params = Cast(scriptDoc["data-params"], listSpec("string"), []); + console.log(params); + const paramNames = params.reduce((o: string, p: string) => { + if (params.indexOf(p) === params.length - 1) { + o = o + p.split(":")[0].trim(); + } else { + o = o + p.split(":")[0].trim() + ","; + } + return o; + }, "" as string); + + const f = new Function(paramNames, StrCast(scriptDoc.script)); + + console.log(scriptDoc.script); + + Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false }); + + let parameters = "("; + params.forEach((element: string, i: number) => { + if (i === params.length - 1) { + parameters = parameters + element + ")"; + } else { + parameters = parameters + element + ", "; + } + }); + + if (parameters === "(") { + Scripting.addGlobal(f, StrCast(scriptDoc.description)); + } else { + Scripting.addGlobal(f, StrCast(scriptDoc.description), parameters); + } + } +}
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index ab577315c..e6cf50de3 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -10,6 +10,8 @@ export { ts }; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; import { Doc, Field } from '../../fields/Doc'; +import { Cast } from "../../fields/Types"; +import { listSpec } from "../../fields/Schema"; export interface ScriptSucccess { success: true; @@ -49,19 +51,34 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is export namespace Scripting { export function addGlobal(global: { name: string }): void; export function addGlobal(name: string, global: any): void; - export function addGlobal(nameOrGlobal: any, global?: any) { - let n: string; + + export function addGlobal(global: { name: string }, decription?: string, params?: string): void; + + export function addGlobal(first: any, second?: any, third?: string) { + let n: any; let obj: any; - if (global !== undefined && typeof nameOrGlobal === "string") { - n = nameOrGlobal; - obj = global; - } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") { - n = nameOrGlobal.name; - obj = nameOrGlobal; + + if (second !== undefined) { + if (typeof first === "string") { + n = first; + obj = second; + } else { + obj = first; + n = first.name; + _scriptingDescriptions[n] = second; + if (third !== undefined) { + _scriptingParams[n] = third; + } + } + } else if (first && typeof first.name === "string") { + n = first.name; + obj = first; } else { throw new Error("Must either register an object with a name, or give a name and an object"); } - if (_scriptingGlobals.hasOwnProperty(n)) { + if (n === undefined || n === "undefined") { + return false; + } else if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } _scriptingGlobals[n] = obj; @@ -75,6 +92,20 @@ export namespace Scripting { scriptingGlobals = globals; } + export function removeGlobal(name: string) { + if (getGlobals().includes(name)) { + delete _scriptingGlobals[name]; + if (_scriptingDescriptions[name]){ + delete _scriptingDescriptions[name]; + } + if (_scriptingParams[name]){ + delete _scriptingParams[name]; + } + return true; + } + return false; + } + export function resetScriptingGlobals() { scriptingGlobals = _scriptingGlobals; } @@ -85,7 +116,19 @@ export namespace Scripting { } export function getGlobals() { - return Object.keys(scriptingGlobals); + return Object.keys(_scriptingGlobals); + } + + export function getGlobalObj() { + return _scriptingGlobals; + } + + export function getDescriptions(){ + return _scriptingDescriptions; + } + + export function getParameters(){ + return _scriptingParams; } } @@ -95,6 +138,8 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) { const _scriptingGlobals: { [name: string]: any } = {}; let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; +const _scriptingDescriptions: { [name: string]: any } = {}; +const _scriptingParams: { [name: string]: any } = {}; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); @@ -133,6 +178,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } return { success: true, result }; } catch (error) { + if (batch) { batch.end(); } diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index e0e205df9..fafc30625 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -5,6 +5,7 @@ import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import "./EditableView.scss"; +import { DragManager } from '../util/DragManager'; export interface EditableProps { /** @@ -48,6 +49,8 @@ export interface EditableProps { HeadingObject?: SchemaHeaderField | undefined; toggle?: () => void; color?: string | undefined; + onDrop?: any; + placeholder?: string; } /** @@ -77,6 +80,13 @@ export class EditableView extends React.Component<EditableProps> { } } + @action + componentDidMount() { + if (this._ref.current && this.props.onDrop) { + DragManager.MakeDropTarget(this._ref.current, this.props.onDrop.bind(this)); + } + } + _didShow = false; @action @@ -168,6 +178,7 @@ export class EditableView extends React.Component<EditableProps> { onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize }} + placeholder={this.props.placeholder} />; } else { this.props.autosuggestProps?.resetValue(); @@ -175,8 +186,9 @@ export class EditableView extends React.Component<EditableProps> { <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} - onClick={this.onClick}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span> + onClick={this.onClick} placeholder={this.props.placeholder}> + + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> </div> ); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index d239a1d6f..372e42468 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -914,7 +914,7 @@ Scripting.addGlobal(function resetPen() { SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); SetActiveInkWidth(GestureOverlay.Instance.SavedWidth ?? "2"); }); -}); +}, "resets the pen tool"); Scripting.addGlobal(function createText(text: any, x: any, y: any) { GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text); -});
\ No newline at end of file +}, "creates a text document with inputted text and coordinates", "(text: any, x: any, y: any)");
\ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index cfa869fb2..f6e5e1705 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -12,7 +12,6 @@ import './OverlayView.scss'; import { Scripting } from "../util/Scripting"; import { ScriptingRepl } from './ScriptingRepl'; import { DragManager } from "../util/DragManager"; -import { listSpec } from "../../fields/Schema"; import { List } from "../../fields/List"; export type OverlayDisposer = () => void; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6f5a3dfe4..8c0b0a1c8 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -859,5 +859,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { </div >); } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }); +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + "opens up the inputted document on the right side of the screen", "(doc: any)"); Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss index 43695f00d..d7fb7ca88 100644 --- a/src/client/views/nodes/ScriptingBox.scss +++ b/src/client/views/nodes/ScriptingBox.scss @@ -5,31 +5,209 @@ flex-direction: column; background-color: rgb(241, 239, 235); padding: 10px; + + .boxed { + border: 1px solid black; + background-color: rgb(212, 198, 179); + width: auto; + height: auto; + font-size: 12px; + position: absolute; + z-index: 100; + padding: 5px; + white-space: nowrap; + overflow: hidden; + } + .scriptingBox-inputDiv { display: flex; flex-direction: column; - height: calc(100% - 30px); + height: 100%; + max-height: 100%; + overflow: hidden; + table-layout: fixed; + + white-space: nowrap; + + .scriptingBox-wrapper { + width: 100%; + height: 100%; + max-height: calc(100%-30px); + display: flex; + flex-direction: row; + overflow: scroll; + justify-content: center; + + .descriptor { + overflow: hidden; + } + + .scriptingBox-textArea { + flex: 70; + height: 100%; + max-width: 95%; + min-width: none; + box-sizing: border-box; + resize: none; + padding: 7px; + overflow-y: scroll; + overflow-x: hidden; + + body { + font-family: Arial, Helvetica, sans-serif; + border: 1px solid red; + } + + .rta { + position: relative; + width: 100%; + height: 100%; + margin-bottom: 60px !important; + overflow-y: scroll; + overflow-x: hidden; + overflow: hidden; + } + + .rta__textarea { + width: 100%; + height: 100%; + font-size: 10px; + } + + .rta__autocomplete { + position: absolute; + display: block; + margin-top: 1em; + } + + .rta__autocomplete--top { + margin-top: 0; + margin-bottom: 1em; + max-height: 100px; + } + + .rta__list { + margin: 0; + padding: 0; + background: #fff; + border: 1px solid #dfe2e5; + border-radius: 3px; + box-shadow: 0 0 5px rgba(27, 31, 35, 0.1); + list-style: none; + overflow-y: scroll; + overflow-x: hidden; + } + + .rta__entity { + background: white; + width: 100%; + text-align: left; + outline: none; + overflow-y: scroll; + } + + .rta__entity:hover { + cursor: pointer; + } + + .rta__entity>* { + padding-left: 4px; + padding-right: 4px; + } + + .rta__entity--selected { + color: #fff; + text-decoration: none; + background: #0366d6; + } + } + + .scriptingBox-plist { + flex: 30; + width: 30%; + height: 100%; + box-sizing: border-box; + resize: none; + padding: 2px; + overflow-y: scroll; + + .scriptingBox-pborder { + background-color: rgb(241, 239, 235); + } + + .scriptingBox-viewBase { + display: flex; + + .scriptingBox-viewPicker { + font-size: 75%; + //text-transform: uppercase; + letter-spacing: 2px; + background: rgb(238, 238, 238); + color: grey; + outline-color: black; + border: none; + padding: 12px 10px 11px 10px; + } + + .scriptingBox-viewPicker:active { + outline-color: black; + } + + .commandEntry-outerDiv { + pointer-events: all; + background-color: gray; + display: flex; + flex-direction: row; + } + } + } + + .scriptingBox-paramNames { + flex: 60; + width: 60%; + box-sizing: border-box; + resize: none; + padding: 7px; + overflow-y: clip; + } + + .scriptingBox-paramInputs { + flex: 40; + width: 40%; + box-sizing: border-box; + resize: none; + padding: 2px; + overflow-y: hidden; + } + } + .scriptingBox-errorMessage { overflow: auto; + background: "red"; + background-color: "red"; + height: 45px; } + .scripting-params { - background: "beige"; - } - .scriptingBox-textArea { - width: 100%; - height: 100%; - box-sizing: border-box; - resize: none; - padding: 7px; + background: rgb(241, 239, 235); + outline-style: solid; + outline-color: black; } } .scriptingBox-toolbar { width: 100%; height: 30px; + overflow: hidden; + .scriptingBox-button { - width: 50% + font-size: xx-small; + width: 50%; + resize: auto; } - } -} + .scriptingBox-button-third { + width: 33%; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 0944edf60..0ae57ca52 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,19 +1,27 @@ -import { action, observable, computed } from "mobx"; +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 { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; +import { List } from "../../../fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { StrCast, ScriptCast, Cast } from "../../../fields/Types"; +import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { returnEmptyString } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; import { InteractionUtils } from "../../util/InteractionUtils"; -import { CompileScript, isCompileError, ScriptParam } from "../../util/Scripting"; +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 "./ScriptingBox.scss"; import { OverlayView } from "../OverlayView"; import { DocumentIconContainer } from "./DocumentIcon"; -import { List } from "../../../fields/List"; +import "./ScriptingBox.scss"; +const _global = (window /* browser */ || global /* node */) as any; const ScriptingSchema = createSchema({}); type ScriptingDocument = makeInterface<[typeof ScriptingSchema, typeof documentSchema]>; @@ -21,78 +29,779 @@ const ScriptingDocument = makeInterface(ScriptingSchema, documentSchema); @observer export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScriptingDocument>(ScriptingDocument) { + + private dropDisposer?: DragManager.DragDropDisposer; protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); } - - _overlayDisposer?: () => void; + private _overlayDisposer?: () => void; @observable private _errorMessage: string = ""; + @observable private _applied: boolean = false; + @observable private _function: boolean = false; + @observable private _hovered: boolean = false; + @observable private _spaced: boolean = false; + + @observable private _scriptKeys: any = Scripting.getGlobals(); + @observable private _scriptGlobals: any = Scripting.getGlobalObj(); + @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 _selectionEnd: any = 0; + + @observable private _paramSuggestion: boolean = false; + @observable private _scriptSuggestedParams: any = ""; + @observable private _scriptParamsText: any = ""; + + // 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"), []); } - @computed get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], StrCast(this.layoutDoc[this.props.fieldKey + "-rawScript"])); } - @computed get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), Cast(this.layoutDoc[this.props.fieldKey + "-params"], listSpec("string"), [])); } set rawScript(value) { this.dataDoc[this.props.fieldKey + "-rawScript"] = value; } - set compileParams(value) { this.dataDoc[this.props.fieldKey + "-params"] = 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<string>(value); } + + getValue(result: any, descrip: boolean) { + let value = ""; + if (typeof result === "object") { + let text = ""; + if (descrip) { + text = result[1]; + } else { + text = result[2]; + } + if (text !== undefined) { + value = text; + } else { + value = ""; + } + } else { + value = ""; + } + return value; + } @action componentDidMount() { - this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript || this.rawScript; + 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]); } - componentWillUnmount() { this._overlayDisposer?.(); } + @action + resetSuggestionPos(caret: any) { + if (!this._suggestionRef.current || !this._scriptTextRef.current) return; + console.log('(top, left, height) = (%s, %s, %s)', caret.top, caret.left, caret.height); + let top = caret.top; + let left = caret.left; + const x = this.dataDoc.x; + const suggestionWidth = this._suggestionRef.current.offsetWidth; + const scriptWidth = this._scriptTextRef.current.offsetWidth; + if ((left + suggestionWidth) > (x + scriptWidth)) { + const diff = (left + suggestionWidth) - (x + scriptWidth); + left = left - diff; + } + + runInAction(() => { + 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"; + this.rootDoc._height = 50; + this.rootDoc._width = 100; + this.dataDoc.documentText = this.rawScript; + } + + // 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 = this.compileParams.reduce((o: ScriptParam, p: string) => { o[p] = "any"; return o; }, {} as ScriptParam); + 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._errorMessage = isCompileError(result) ? result.errors.map(e => e.messageText).join("\n") : ""; - return this.dataDoc[this.props.fieldKey] = result.compiled ? new ScriptField(result) : undefined; + this.dataDoc.documentText = this.rawScript; + this.dataDoc.data = result.compiled ? new ScriptField(result) : undefined; + this.onError(result.compiled ? undefined : result.errors); + if (result.compiled) { + return true; + } else { + return false; + } } + // checks if the script compiles and then runs the script @action onRun = () => { - this.onCompile()?.script.run({}, err => this._errorMessage = err.map((e: any) => e.messageText).join("\n")); + 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.<var> + ScriptCast(this.dataDoc.data, 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._scriptGlobals = Scripting.getGlobalObj(); + 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(<DocumentIconContainer />, { 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 + this.dataDoc[name] = e.target.selectedOptions[0].value; + } + + // 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 = + <textarea + className="scriptingBox-textarea" + onChange={e => this.functionDescription = e.target.value} + placeholder="enter description here" + value={this.functionDescription} + style={{ maxWidth: "100%", height: "40%", width: "100%", resize: "none" }} + />; + const nameInput = + <textarea + className="scriptingBox-textarea" + onChange={e => this.functionName = e.target.value} + placeholder="enter name here" + value={this.functionName} + style={{ maxWidth: "100%", height: "40%", width: "100%", resize: "none" }} + />; + + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} > + <div className="scriptingBox-wrapper" style={{ maxWidth: "100%" }}> + <div className="container" style={{ maxWidth: "100%" }}> + <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function name: </div> + <div style={{ maxWidth: "100%" }}> {nameInput}</div> + <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function description: </div> + <div style={{ maxWidth: "100%" }}>{descriptionInput}</div> + </div> + </div> + {this.renderErrorMessage()} + </div>; + } + + + + renderErrorMessage() { + return !this._errorMessage ? (null) : <div className="scriptingBox-errorMessage"> {this._errorMessage} </div>; + } + + // rendering when a doc's value can be set in applied UI + renderDoc(parameter: string) { + return <div className="scriptingBox-paramInputs" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} + ref={ele => ele && this.createDashEventsTarget(ele, (e, de) => this.onDrop(e, de, parameter))} > + <EditableView display={"block"} maxHeight={72} height={35} fontSize={14} + contents={this.dataDoc[parameter]?.title ?? "undefined"} + GetValue={() => this.dataDoc[parameter]?.title ?? "undefined"} + SetValue={action((value: string) => { + const script = CompileScript(value, { + addReturn: true, + typecheck: false, + transformer: DocumentIconContainer.getTransformer() + }); + const results = script.compiled && script.run(); + if (results && results.success) { + this._errorMessage = ""; + this.dataDoc[parameter] = results.result; + return true; + } + this._errorMessage = "invalid document"; + return false; + })} + /> + </div>; + } + + // rendering when a string's value can be set in applied UI + renderString(parameter: string) { + return <div className="scriptingBox-paramInputs" style={{ overflowY: "hidden" }}> + <EditableView display={"block"} maxHeight={72} height={35} fontSize={14} + contents={this.dataDoc[parameter] ?? "undefined"} + GetValue={() => StrCast(this.dataDoc[parameter]) ?? "undefined"} + SetValue={action((value: string) => { + if (value && value !== " ") { + this._errorMessage = ""; + this.dataDoc[parameter] = value; + return true; + } + return false; + })} + /> + </div>; + } + + // rendering when a number's value can be set in applied UI + renderNumber(parameter: string) { + return <div className="scriptingBox-paramInputs"> + <EditableView display={"block"} maxHeight={72} height={35} fontSize={14} + contents={this.dataDoc[parameter] ?? "undefined"} + GetValue={() => StrCast(this.dataDoc[parameter]) ?? "undefined"} + SetValue={action((value: string) => { + if (value && value !== " ") { + if (parseInt(value)) { + this._errorMessage = ""; + this.dataDoc[parameter] = parseInt(value); + return true; + } + this._errorMessage = "not a number"; + } + return false; + })} + /> + </div>; + } + + // rendering when an enum's value can be set in applied UI (drop down box) + renderEnum(parameter: string, types: string[]) { + return <div className="scriptingBox-paramInputs"> + <div className="scriptingBox-viewBase"> + <div className="commandEntry-outerDiv"> + <select className="scriptingBox-viewPicker" + onPointerDown={e => e.stopPropagation()} + onChange={e => this.viewChanged(e, parameter)} + value={this.dataDoc[parameter]}> + + {types.map(type => + <option className="scriptingBox-viewOption" value={type.trim()}> {type.trim()} </option> + )} + </select> + </div> + </div> + </div>; + } + + // rendering when a boolean's value can be set in applied UI (drop down box) + renderBoolean(parameter: string) { + return <div className="scriptingBox-paramInputs"> + <div className="scriptingBox-viewBase"> + <div className="commandEntry-outerDiv"> + <select className="scriptingBox-viewPicker" + onPointerDown={e => e.stopPropagation()} + onChange={e => this.viewChanged(e, parameter)} + value={this.dataDoc[parameter]}> + <option className="scriptingBox-viewOption" value={"true"}>true </option> + <option className="scriptingBox-viewOption" value={"false"}>false</option> + </select> + </div> + </div> + </div>; + } + + // setting a parameter (checking type and name before it is added) + compileParam(value: string, whichParam?: number) { + if (value.includes(":")) { + const ptype = value.split(":")[1].trim(); + const pname = value.split(":")[0].trim(); + if (ptype === "Doc" || ptype === "string" || ptype === "number" || ptype === "boolean" || ptype.split("|")[1]) { + if ((whichParam !== undefined && pname === this.paramsNames[whichParam]) || !this.paramsNames.includes(pname)) { + this._errorMessage = ""; + if (whichParam !== undefined) { + this.compileParams[whichParam] = value; + } else { + this.compileParams = [...value.split(";").filter(s => s), ...this.compileParams]; + } + return true; + } + this._errorMessage = "this name has already been used"; + } else { + this._errorMessage = "this type is not supported"; + } + } else { + this._errorMessage = "must set type of parameter"; + } + return false; + } + + @action + handleToken(str: string) { + this._currWord = str; + this._suggestions = []; + this._scriptKeys.forEach((element: string) => { + if (element.toLowerCase().indexOf(this._currWord.toLowerCase()) >= 0) { + this._suggestions.push(StrCast(element)); + } + }); + return (this._suggestions); + } + + @action + handleFunc(pos: number) { + const scriptString = this.rawScript.slice(0, pos - 2); + this._currWord = scriptString.split(" ")[scriptString.split(" ").length - 1]; + this._suggestions = []; + const params = StrCast(this._scriptingParams[this._currWord]); + this._suggestions.push(params); + return (this._suggestions); + } + + + getDescription(value: string) { + const descrip = this._scriptingDescriptions[value]; + let display = ""; + if (descrip !== undefined) { + if (descrip.length > 0) { + display = descrip; + } + } + return display; + } + + getParams(value: string) { + const params = this._scriptingParams[value]; + let display = ""; + if (params !== undefined) { + if (params.length > 0) { + display = params; + } + } + return display; + } + + setHovered(bool: boolean) { + this._hovered = bool; + } + + returnParam(item: string) { + const params = item.split(","); + let value = ""; + let first = true; + params.forEach((element) => { + if (first) { + value = element.split(":")[0].trim(); + first = false; + } else { + value = value + ", " + element.split(":")[0].trim(); + } + }); + return value; + } + + getSuggestedParams(pos: number) { + const firstScript = this.rawScript.slice(0, pos); + const indexP = firstScript.lastIndexOf("."); + const indexS = firstScript.lastIndexOf(" "); + let func = ""; + if (indexP > indexS) { + func = firstScript.slice(indexP + 1, firstScript.length + 1); + } else { + func = firstScript.slice(indexS + 1, firstScript.length + 1); + } + if (this._scriptingParams[func]) { + return this._scriptingParams[func]; + } else { + return ""; + } + } + + @action + suggestionPos = () => { + const getCaretCoordinates = require('textarea-caret'); + const This = this; + //if (!This._applied && !This._function) { + document.querySelector('textarea')?.addEventListener("input", function () { + const caret = getCaretCoordinates(this, this.selectionEnd); + This._selection = this; + This._selectionEnd = this.selectionEnd; + This.resetSuggestionPos(caret); + }); + //} + } + + @action + keyHandler(e: any, pos: number) { + if (this._lastChar === "Enter") { + this.rawScript = this.rawScript + " "; + } + console.log(e.key); + if (e.key === "(") { + this.suggestionPos(); + + this._scriptParamsText = this.getSuggestedParams(pos); + this._scriptSuggestedParams = this.getSuggestedParams(pos); + + if (this._scriptParamsText !== undefined && this._scriptParamsText.length > 0) { + if (this.rawScript[pos - 2] !== "(") { + this._paramSuggestion = true; + } + } + } else if (e.key === ")") { + this._paramSuggestion = false; + } else { + if (e.key === "Backspace") { + if (this._lastChar === "(") { + this._paramSuggestion = false; + } else if (this._lastChar === ")") { + if (this.rawScript.slice(0, this.rawScript.length - 1).split("(").length - 1 > this.rawScript.slice(0, this.rawScript.length - 1).split(")").length - 1) { + if (this._scriptParamsText.length > 0) { + this._paramSuggestion = true; + } + } + } + } else { + if (this.rawScript.split("(").length - 1 <= this.rawScript.split(")").length - 1) { + this._paramSuggestion = false; + } + } + } + if (e.key === "Backspace") { + this._lastChar = this.rawScript[this.rawScript.length - 2]; + console.log("last char: " + this._lastChar); + } else { + this._lastChar = e.key; + } + + if (this._paramSuggestion) { + const parameters = this._scriptParamsText.split(","); + const index = this.rawScript.lastIndexOf("("); + const enteredParams = this.rawScript.slice(index, this.rawScript.length); + const splitEntered = enteredParams.split(","); + const numEntered = splitEntered.length; + + parameters.forEach((element: string, i: number) => { + if (i !== parameters.length - 1) { + parameters[i] = element + ","; + } + }); + + console.log("numEntered: " + numEntered); + + let first = ""; + let last = ""; + + parameters.forEach((element: string, i: number) => { + if (i < numEntered - 1) { + first = first + element; + } else if (i > numEntered - 1) { + last = last + element; + } + }); + + this._scriptSuggestedParams = <div> {first} <b>{parameters[numEntered - 1]}</b> {last} </div>; + } + } + + @action + handlePosChange(number: any) { + this.caretPos = number; + if (this.caretPos === 0) { + this.rawScript = " " + this.rawScript; + } else if (this._spaced) { + this._spaced = false; + if (this.rawScript[this.caretPos - 1] === " ") { + this.rawScript = this.rawScript.slice(0, this.caretPos - 1) + + this.rawScript.slice(this.caretPos, this.rawScript.length); + } + } + } + + caretPos = 0; + textarea: any; + @computed({ keepAlive: true }) get renderScriptingBox() { + + trace(); + return <div style={{ width: this.compileParams.length > 0 ? "70%" : "100%" }} ref={this._scriptTextRef}> + <ReactTextareaAutocomplete className="ScriptingBox-textarea" style={{ resize: "none", height: "100%" }} + minChar={1} + placeholder="write your script here" + onFocus={this.onFocus} + onBlur={() => this._overlayDisposer?.()} + onChange={e => this.rawScript = e.target.value} + value={this.rawScript} + movePopupAsYouType={true} + loadingComponent={() => <span>Loading</span>} + + trigger={{ + " ": { + dataProvider: (token: any) => this.handleToken(token), + component: ({ entity: value }) => this.renderFuncListElement(value), + output: (item: any, trigger) => { + this._spaced = true; + return trigger + item.trim(); + }, + }, + ".": { + dataProvider: (token: any) => this.handleToken(token), + component: ({ entity: value }) => this.renderFuncListElement(value), + output: (item: any, trigger) => { + this._spaced = true; + return trigger + item.trim(); + }, + } + }} + onKeyDown={(e) => this.keyHandler(e, this.caretPos)} + onCaretPositionChange={(number: any) => this.handlePosChange(number)} + /> + </div>; + } + + renderFuncListElement(value: string) { + return <div> + <div style={{ fontSize: "14px" }} + onMouseEnter={() => this.setHovered(true)} + onMouseLeave={() => this.setHovered(false)}> + {value} + </div> + <div key="desc" style={{ fontSize: "10px" }}>{this.getDescription(value)}</div> + <div key="params" style={{ fontSize: "10px" }}>{this.getParams(value)}</div> + </div>; + } + + // inputs for scripting div (script box, params box, and params column) + @computed({ keepAlive: true }) get renderScriptingInputs() { + + // should there be a border? style={{ borderStyle: "groove", borderBlockWidth: "1px" }} + // params box on bottom + const parameterInput = <div className="scriptingBox-params"> + <EditableView display={"block"} maxHeight={72} height={35} fontSize={22} + contents={""} + GetValue={returnEmptyString} + SetValue={value => value && value !== " " ? this.compileParam(value) : false} + placeholder={"enter parameters here"} + /> + </div>; + + // params column on right side (list) + const definedParameters = !this.compileParams.length ? (null) : + <div className="scriptingBox-plist" style={{ width: "30%" }}> + {this.compileParams.map((parameter, i) => + <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} > + <EditableView display={"block"} maxHeight={72} height={35} fontSize={12} background-color={"beige"} + contents={parameter} + GetValue={() => parameter} + SetValue={value => value && value !== " " ? this.compileParam(value, i) : this.onDelete(i)} + /> + </div> + )} + </div>; + + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} > + <div className="scriptingBox-wrapper"> + {this.renderScriptingBox} + {definedParameters} + </div> + {parameterInput} + {this.renderErrorMessage()} + </div>; + } + + // toolbar (with compile and apply buttons) for scripting UI + renderScriptingTools() { + const buttonStyle = "scriptingBox-button" + (this.rootDoc.layoutKey === "layout_onClick" ? "third" : ""); + return <div className="scriptingBox-toolbar"> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onApply(); e.stopPropagation(); }}>Apply</button> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onSave(); e.stopPropagation(); }}>Save</button> + + {this.rootDoc.layoutKey !== "layout_onClick" ? (null) : + <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>} + </div>; + } + + // inputs UI for params which allows you to set values for each displayed in a list + renderParamsInputs() { + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} > + {!this.compileParams.length || !this.paramsNames ? (null) : + <div className="scriptingBox-plist" style={{ overflowY: "scroll" }}> + {this.paramsNames.map((parameter: string, i: number) => + <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} > + <div className="scriptingBox-wrapper" style={{ maxHeight: "40px" }}> + <div className="scriptingBox-paramNames" > {`${parameter}:${this.paramsTypes[i]} = `} </div> + {this.paramsTypes[i] === "boolean" ? this.renderBoolean(parameter) : (null)} + {this.paramsTypes[i] === "string" ? this.renderString(parameter) : (null)} + {this.paramsTypes[i] === "number" ? this.renderNumber(parameter) : (null)} + {this.paramsTypes[i] === "Doc" ? this.renderDoc(parameter) : (null)} + {this.paramsTypes[i]?.split("|")[1] ? this.renderEnum(parameter, this.paramsTypes[i].split("|")) : (null)} + </div> + </div>)} + </div>} + {this.renderErrorMessage()} + </div>; + } + + // toolbar (with edit and run buttons and error message) for params UI + renderParamsTools() { + const buttonStyle = "scriptingBox-button" + (this.rootDoc.layoutKey === "layout_onClick" ? "third" : ""); + return <div className="scriptingBox-toolbar"> + <button className={buttonStyle} onPointerDown={e => { this.onEdit(); e.stopPropagation(); }}>Edit</button> + <button className={buttonStyle} onPointerDown={e => { this.onRun(); e.stopPropagation(); }}>Run</button> + {this.rootDoc.layoutKey !== "layout_onClick" ? (null) : + <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>} + </div>; + } + + // toolbar (with edit and run buttons and error message) for params UI + renderFunctionTools() { + const buttonStyle = "scriptingBox-button" + (this.rootDoc.layoutKey === "layout_onClick" ? "third" : ""); + return <div className="scriptingBox-toolbar"> + <button className={buttonStyle} onPointerDown={e => { this.onEdit(); e.stopPropagation(); }}>Edit</button> + <button className={buttonStyle} onPointerDown={e => { this.onCreate(); e.stopPropagation(); }}>Create Function</button> + {this.rootDoc.layoutKey !== "layout_onClick" ? (null) : + <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>} + </div>; + } + + // renders script UI if _applied = false and params UI if _applied = true render() { - const params = <EditableView - contents={this.compileParams.join(" ")} - display={"block"} - maxHeight={72} - height={35} - fontSize={28} - GetValue={() => ""} - SetValue={value => { this.compileParams = new List<string>(value.split(" ").filter(s => s !== " ")); return true; }} - />; return ( - <div className="scriptingBox-outerDiv" - onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> - <div className="scriptingBox-inputDiv" - onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} > - <textarea className="scriptingBox-textarea" - placeholder="write your script here" - onChange={e => this.rawScript = e.target.value} - value={this.rawScript} - onFocus={this.onFocus} - onBlur={e => this._overlayDisposer?.()} /> - <div className="scriptingBox-errorMessage" style={{ background: this._errorMessage ? "red" : "" }}>{this._errorMessage}</div> - <div className="scriptingBox-params" >{params}</div> - </div> - {this.rootDoc.layout === "layout" ? <div></div> : (null)} - <div className="scriptingBox-toolbar"> - <button className="scriptingBox-button" onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button> - <button className="scriptingBox-button" onPointerDown={e => { this.onRun(); e.stopPropagation(); }}>Run</button> + <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} + onPointerUp={!this._function ? this.suggestionPos : undefined}> + <div className="scriptingBox-outerDiv" + onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> + {this._paramSuggestion ? <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: "inline" }}> {this._scriptSuggestedParams} </div> : null} + {!this._applied && !this._function ? this.renderScriptingInputs : null} + {this._applied && !this._function ? this.renderParamsInputs() : null} + {!this._applied && this._function ? this.renderFunctionInputs() : null} + + {!this._applied && !this._function ? this.renderScriptingTools() : null} + {this._applied && !this._function ? this.renderParamsTools() : null} + {!this._applied && this._function ? this.renderFunctionTools() : null} </div> </div> ); } -} +}
\ No newline at end of file diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index fc7f9ca80..11b3b0524 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -161,7 +161,7 @@ export class ComputedField extends ScriptField { Scripting.addGlobal(function getIndexVal(list: any[], index: number) { return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); -}); +}, "returns the value at a given index of a list", "(list: any[], index: number)"); export namespace ComputedField { let useComputed = true; diff --git a/src/server/database.ts b/src/server/database.ts index a5f23c4b1..b017f1e3c 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -8,6 +8,7 @@ import { IDatabase, DocumentsCollection } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; import * as mongoose from 'mongoose'; import { Upload } from './SharedMediaTypes'; +import { timeout } from 'async'; export namespace Database { |