diff options
-rw-r--r-- | src/client/views/OverlayView.scss | 21 | ||||
-rw-r--r-- | src/client/views/OverlayView.tsx | 71 | ||||
-rw-r--r-- | src/client/views/ScriptingRepl.scss | 11 | ||||
-rw-r--r-- | src/client/views/ScriptingRepl.tsx | 60 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 3 | ||||
-rw-r--r-- | src/new_fields/ScriptField.ts | 5 |
6 files changed, 161 insertions, 10 deletions
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss new file mode 100644 index 000000000..9d0abc96d --- /dev/null +++ b/src/client/views/OverlayView.scss @@ -0,0 +1,21 @@ +.overlayWindow-outerDiv { + position: absolute; + border-radius: 5px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.overlayWindow-titleBar { + height: 30px; + background: darkslategray; + color: whitesmoke; + text-align: center; + cursor: move; +} + +.overlayWindow-closeButton { + float: right; + height: 30px; + width: 30px; +}
\ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index f8fc94274..6b72abebf 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react"; import { observable, action } from "mobx"; import { Utils } from "../../Utils"; +import './OverlayView.scss'; + export type OverlayDisposer = () => void; export type OverlayElementOptions = { @@ -10,13 +12,67 @@ export type OverlayElementOptions = { y: number; width?: number; height?: number; + title?: string; }; +export interface OverlayWindowProps { + children: JSX.Element; + overlayOptions: OverlayElementOptions; + onClick: () => void; +} + +@observer +export class OverlayWindow extends React.Component<OverlayWindowProps> { + @observable x: number; + @observable y: number; + @observable width?: number; + @observable height?: number; + constructor(props: OverlayWindowProps) { + super(props); + + const opts = props.overlayOptions; + this.x = opts.x; + this.y = opts.y; + this.width = opts.width; + this.height = opts.height; + } + + onPointerDown = (_: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + } + + @action + onPointerMove = (e: PointerEvent) => { + this.x += e.movementX; + this.y += e.movementY; + } + + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}> + <div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown} > + {this.props.overlayOptions.title || "Untitled"} + <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button> + </div> + {this.props.children} + </div> + ); + } +} + @observer export class OverlayView extends React.Component { public static Instance: OverlayView; @observable.shallow - private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = []; + private _elements: JSX.Element[] = []; constructor(props: any) { super(props); @@ -27,20 +83,19 @@ export class OverlayView extends React.Component { @action addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer { - const eleWithPosition = { ele, options, id: Utils.GenerateGuid() }; - this._elements.push(eleWithPosition); - return action(() => { - const index = this._elements.indexOf(eleWithPosition); + const remove = action(() => { + const index = this._elements.indexOf(ele); if (index !== -1) this._elements.splice(index, 1); }); + ele = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{ele}</OverlayWindow>; + this._elements.push(ele); + return remove; } render() { return ( <div> - {this._elements.map(({ ele, options: { x, y, width, height }, id }) => ( - <div key={id} style={{ position: "absolute", transform: `translate(${x}px, ${y}px)`, width, height }}>{ele}</div> - ))} + {this._elements} </div> ); } diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss new file mode 100644 index 000000000..1eedb52fa --- /dev/null +++ b/src/client/views/ScriptingRepl.scss @@ -0,0 +1,11 @@ +.scriptingRepl-outerContainer { + background-color: whitesmoke; +} + +.scriptingRepl-resultContainer { + padding-bottom: 5px; +} + +.scriptingRepl-commandInput { + width: 100%; +}
\ No newline at end of file diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx new file mode 100644 index 000000000..bd6fc9dfb --- /dev/null +++ b/src/client/views/ScriptingRepl.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import './ScriptingRepl.scss'; +import { Scripting, CompileScript } from '../util/Scripting'; + +@observer +export class ScriptingRepl extends React.Component { + @observable private commands: { command: string, result: any }[] = []; + + @observable private commandString: string = ""; + + private args: any = {}; + + @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + + const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" } }); + if (!script.compiled) { + return; + } + const result = script.run({ args: this.args }); + if (!result.success) { + return; + } + this.commands.push({ command: this.commandString, result: result.result }); + + this.commandString = ""; + } + } + + @action + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.commandString = e.target.value; + } + + render() { + return ( + <div className="scriptingRepl-outerContainer"> + <div className="scriptingRepl-commandsContainer"> + {this.commands.map(({ command, result }) => { + return ( + <div className="scriptingRepl-resultContainer"> + <div className="scriptingRepl-commandString">{command}</div> + <div className="scriptingRepl-commandResult">{String(result)}</div> + </div> + ); + })} + </div> + <input + className="scriptingRepl-commandInput" + value={this.commandString} + onChange={this.onChange} + onKeyDown={this.onKeyDown}></input> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fb8319934..52ba643e0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -35,6 +35,8 @@ import { list, object, createSimpleSchema } from 'serializr'; import { LinkManager } from '../../util/LinkManager'; import { RouteStore } from '../../../server/RouteStore'; import { FormattedTextBox } from './FormattedTextBox'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -555,6 +557,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addElement(<ScriptingRepl />, { x: 100, y: 100 }) }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index b5b1595cf..e8a1ea28a 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -5,6 +5,7 @@ import { serializable, createSimpleSchema, map, primitive, object, deserialize, import { Deserializable } from "../client/util/SerializationHelper"; import { Doc } from "../new_fields/Doc"; import { Plugins } from "./util"; +import { computedFn } from "mobx-utils"; function optional(propSchema: PropSchema) { return custom(value => { @@ -87,13 +88,13 @@ export class ScriptField extends ObjectField { @Deserializable("computed", deserializeScript) export class ComputedField extends ScriptField { //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc - value(doc: Doc) { + value = computedFn((doc: Doc) => { const val = this.script.run({ this: doc }); if (val.success) { return val.result; } return undefined; - } + }); } export namespace ComputedField { |