diff options
| author | andrewdkim <adkim414@gmail.com> | 2019-07-22 15:47:37 -0400 | 
|---|---|---|
| committer | andrewdkim <adkim414@gmail.com> | 2019-07-22 15:47:37 -0400 | 
| commit | 5c0defdb56b4c31bb4ab2670990a51b6f05d1a5e (patch) | |
| tree | 85f608ac6c5ce277f3ff3fa261ec8bb9fb22589d /src | |
| parent | 9e093b9652750cfd102864c551ed8564a04df92e (diff) | |
| parent | 291ae12b0c4587c4f656caac6d402413cc8ec76d (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into animationtimeline
Diffstat (limited to 'src')
23 files changed, 764 insertions, 110 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index ac6d127cc..8df67df5d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -146,7 +146,7 @@ export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;  export type Predicate<K, V> = (entry: [K, V]) => boolean; -export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) { +export function DeepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {      let deepCopy = new Map<K, V>();      let entries = source.entries(), next = entries.next();      while (!next.done) { @@ -157,4 +157,18 @@ export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {          next = entries.next();      }      return deepCopy; +} + +export namespace JSONUtils { + +    export function tryParse(source: string) { +        let results: any; +        try { +            results = JSON.parse(source); +        } catch (e) { +            results = source; +        } +        return results; +    } +  }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3c248760b..7563fda20 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -21,7 +21,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea";  import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";  import { IconBox } from "../views/nodes/IconBox";  import { Field, Doc, Opt } from "../../new_fields/Doc"; -import { OmitKeys } from "../../Utils"; +import { OmitKeys, JSONUtils } from "../../Utils";  import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";  import { HtmlField } from "../../new_fields/HtmlField";  import { List } from "../../new_fields/List"; @@ -55,7 +55,8 @@ export enum DocumentType {      ICON = "icon",      IMPORT = "import",      LINK = "link", -    LINKDOC = "linkdoc" +    LINKDOC = "linkdoc", +    TEMPLATE = "template"  }  export interface DocumentOptions { @@ -439,6 +440,85 @@ export namespace Docs {      export namespace Get { +        const primitives = ["string", "number", "boolean"]; + +        /** +         * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily +         * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. +         *  +         * After building a hierarchy within / below a top-level document, it then returns that top-level parent. +         *  +         * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the +         * string is invalid JSON, so we should assume that the input is the result of a JSON.parse() +         * call that returned a regular string value to be stored as a Field. +         *  +         * If we've received something other than a string, since the caller might also pass in the results of a +         * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number. +         * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation. +         *  +         * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, +         * lacking the key value structure, gets stored as a field in a wrapper document. +         *  +         * @param input for convenience and flexibility, either a valid JSON string to be parsed, +         * or the result of any JSON.parse() call. +         * @param title an optional title to give to the highest parent document in the hierarchy +         */ +        export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> { +            if (input === null || ![...primitives, "object"].includes(typeof input)) { +                return undefined; +            } +            let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input; +            let converted: Doc; +            if (typeof parsed === "object" && !(parsed instanceof Array)) { +                converted = convertObject(parsed, title); +            } else { +                (converted = new Doc).json = toField(parsed); +            } +            title && (converted.title = title); +            return converted; +        } + +        /** +         * For each value of the object, recursively convert it to its appropriate field value +         * and store the field at the appropriate key in the document if it is not undefined +         * @param object the object to convert +         * @returns the object mapped from JSON to field values, where each mapping  +         * might involve arbitrary recursion (since toField might itself call convertObject) +         */ +        const convertObject = (object: any, title?: string): Doc => { +            let target = new Doc(), result: Opt<Field>; +            Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result)); +            title && (target.title = title); +            return target; +        }; + +        /** +         * For each element in the list, recursively convert it to a document or other field  +         * and push the field to the list if it is not undefined +         * @param list the list to convert +         * @returns the list mapped from JSON to field values, where each mapping  +         * might involve arbitrary recursion (since toField might itself call convertList) +         */ +        const convertList = (list: Array<any>): List<Field> => { +            let target = new List(), result: Opt<Field>; +            list.map(item => (result = toField(item)) && target.push(result)); +            return target; +        }; + + +        const toField = (data: any, title?: string): Opt<Field> => { +            if (data === null || data === undefined) { +                return undefined; +            } +            if (primitives.includes(typeof data)) { +                return data; +            } +            if (typeof data === "object") { +                return data instanceof Array ? convertList(data) : convertObject(data, title); +            } +            throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); +        }; +          export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {              let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;              if (type.indexOf("image") !== -1) { diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 62c2cfe85..46dc320b0 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,5 +1,7 @@ -// import * as ts from "typescript" -let ts = (window as any).ts; +import * as ts from "typescript"; +export { ts }; +// export const ts = (window as any).ts; +  // // @ts-ignore  // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'  // // @ts-ignore @@ -55,13 +57,35 @@ export namespace Scripting {          }          scriptingGlobals[n] = obj;      } + +    export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) { +        return { ..._scriptingGlobals, ...(globals || {}) }; +    } + +    export function setScriptingGlobals(globals: { [key: string]: any }) { +        scriptingGlobals = globals; +    } + +    export function resetScriptingGlobals() { +        scriptingGlobals = _scriptingGlobals; +    } + +    // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]); +    export function printNodeType(node: any, indentation = "") { +        console.log(indentation + ts.SyntaxKind[node.kind]); +    } + +    export function getGlobals() { +        return Object.keys(scriptingGlobals); +    }  }  export function scriptingGlobal(constructor: { new(...args: any[]): any }) {      Scripting.addGlobal(constructor);  } -const scriptingGlobals: { [name: string]: any } = {}; +const _scriptingGlobals: { [name: string]: any } = {}; +let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;  function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {      const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); @@ -162,6 +186,8 @@ class ScriptingCompilerHost {      }  } +export type Traverser = (node: ts.Node, indentation: string) => boolean | void; +export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser };  export interface ScriptOptions {      requiredType?: string;      addReturn?: boolean; @@ -169,10 +195,23 @@ export interface ScriptOptions {      capturedVariables?: { [name: string]: Field };      typecheck?: boolean;      editable?: boolean; +    traverser?: TraverserParam; +    transformer?: ts.TransformerFactory<ts.SourceFile>; +    globals?: { [name: string]: any }; +} + +// function forEachNode(node:ts.Node, fn:(node:any) => void); +function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") { +    return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { +        forEachNode(n, onEnter, onExit, indentation + "    "); +    }) || (onExit && onExit(node, indentation));  }  export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {      const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; +    if (options.globals) { +        Scripting.setScriptingGlobals(options.globals); +    }      let host = new ScriptingCompilerHost;      let paramNames: string[] = [];      if ("this" in params || "this" in capturedVariables) { @@ -192,10 +231,27 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp          paramList.push(`${key}: ${capturedVariables[key].constructor.name}`);      }      let paramString = paramList.join(", "); +    if (options.traverser) { +        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); +        const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; +        const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; +        forEachNode(sourceFile, onEnter, onLeave); +    } +    if (options.transformer) { +        const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); +        const result = ts.transform(sourceFile, [options.transformer]); +        const transformed = result.transformed; +        const printer = ts.createPrinter({ +            newLine: ts.NewLineKind.LineFeed +        }); +        script = printer.printFile(transformed[0]); +        result.dispose(); +    }      let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {          ${addReturn ? `return ${script};` : script}      })`;      host.writeFile("file.ts", funcScript); +      if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);      let program = ts.createProgram(["file.ts"], {}, host);      let testResult = program.emit(); @@ -203,7 +259,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp      let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); -    return Run(outputText, paramNames, diagnostics, script, options); +    const result = Run(outputText, paramNames, diagnostics, script, options); + +    if (options.globals) { +        Scripting.resetScriptingGlobals(); +    } +    return result;  }  Scripting.addGlobal(CompileScript);
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index fb5104915..2f7bea365 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -28,6 +28,7 @@ import { RichTextField } from '../../new_fields/RichTextField';  import { LinkManager } from '../util/LinkManager';  import { ObjectField } from '../../new_fields/ObjectField';  import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { ImageBox } from './nodes/ImageBox';  const higflyout = require("@hig/flyout");  export const { anchorPoints } = higflyout;  export const Flyout = higflyout.default; @@ -85,8 +86,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  SelectionManager.DeselectAll();                  let fieldTemplate = fieldTemplateView.props.Document;                  let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; -                let metaKey = text.slice(1, text.length); -                Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate)); +                let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); +                let proto = Doc.GetProto(docTemplate); +                Doc.MakeTemplate(fieldTemplate, metaKey, proto); +                if (text.startsWith(">>")) { +                    proto.detailedLayout = proto.layout; +                    proto.miniLayout = ImageBox.LayoutString(metaKey); +                }              }              else {                  if (SelectionManager.SelectedDocuments().length > 0) { diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index dfa110f8d..a5150cd66 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -17,4 +17,5 @@  }  .editableView-input {      width: 100%; +    background: inherit;  }
\ No newline at end of file diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 989fb1be9..f2cdffd38 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -67,6 +67,7 @@ export class EditableView extends React.Component<EditableProps> {      @action      onClick = (e: React.MouseEvent) => { +        e.nativeEvent.stopPropagation();          if (!this.props.onClick || !this.props.onClick(e)) {              this._editing = true;          } diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss new file mode 100644 index 000000000..4d1e8cf0b --- /dev/null +++ b/src/client/views/OverlayView.scss @@ -0,0 +1,42 @@ +.overlayWindow-outerDiv { +    border-radius: 5px; +    overflow: hidden; +    display: flex; +    flex-direction: column; +} + +.overlayWindow-outerDiv, +.overlayView-wrapperDiv { +    position: absolute; +    z-index: 1; +} + +.overlayWindow-titleBar { +    flex: 0 1 30px; +    background: darkslategray; +    color: whitesmoke; +    text-align: center; +    cursor: move; +} + +.overlayWindow-content { +    flex: 1 1 auto; +    display: flex; +    flex-direction: column; +} + +.overlayWindow-closeButton { +    float: right; +    height: 30px; +    width: 30px; +} + +.overlayWindow-resizeDragger { +    background-color: red; +    position: absolute; +    right: 0px; +    bottom: 0px; +    width: 10px; +    height: 10px; +    cursor: nwse-resize; +}
\ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index f8fc94274..2f2579057 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,92 @@ 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 || 200; +        this.height = opts.height || 200; +    } + +    onPointerDown = (_: React.PointerEvent) => { +        document.removeEventListener("pointermove", this.onPointerMove); +        document.removeEventListener("pointerup", this.onPointerUp); +        document.addEventListener("pointermove", this.onPointerMove); +        document.addEventListener("pointerup", this.onPointerUp); +    } + +    onResizerPointerDown = (_: React.PointerEvent) => { +        document.removeEventListener("pointermove", this.onResizerPointerMove); +        document.removeEventListener("pointerup", this.onResizerPointerUp); +        document.addEventListener("pointermove", this.onResizerPointerMove); +        document.addEventListener("pointerup", this.onResizerPointerUp); +    } + +    @action +    onPointerMove = (e: PointerEvent) => { +        this.x += e.movementX; +        this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0); +        this.y += e.movementY; +        this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0); +    } + +    @action +    onResizerPointerMove = (e: PointerEvent) => { +        this.width += e.movementX; +        this.width = Math.max(this.width, 30); +        this.height += e.movementY; +        this.height = Math.max(this.height, 30); +    } + +    onPointerUp = (e: PointerEvent) => { +        document.removeEventListener("pointermove", this.onPointerMove); +        document.removeEventListener("pointerup", this.onPointerUp); +    } + +    onResizerPointerUp = (e: PointerEvent) => { +        document.removeEventListener("pointermove", this.onResizerPointerMove); +        document.removeEventListener("pointerup", this.onResizerPointerUp); +    } + +    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> +                <div className="overlayWindow-content"> +                    {this.props.children} +                </div> +                <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div> +            </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 +108,34 @@ 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 = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{ +            transform: `translate(${options.x}px, ${options.y}px)`, +            width: options.width, +            height: options.height +        }}>{ele}</div>; +        this._elements.push(ele); +        return remove; +    } + +    @action +    addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer { +        const remove = action(() => { +            const index = this._elements.indexOf(contents);              if (index !== -1) this._elements.splice(index, 1);          }); +        contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>; +        this._elements.push(contents); +        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..f1ef64193 --- /dev/null +++ b/src/client/views/ScriptingRepl.scss @@ -0,0 +1,50 @@ +.scriptingRepl-outerContainer { +    background-color: whitesmoke; +    height: 100%; +    display: flex; +    flex-direction: column; +} + +.scriptingRepl-resultContainer { +    padding-bottom: 5px; +} + +.scriptingRepl-commandInput { +    width: 100%; +} + +.scriptingRepl-commandResult, +.scriptingRepl-commandString { +    overflow-wrap: break-word; +} + +.scriptingRepl-commandsContainer { +    flex: 1 1 auto; +    overflow-y: scroll; +} + +.documentIcon-outerDiv { +    background-color: white; +    border-width: 1px; +    border-style: solid; +    border-radius: 25%; +    padding: 2px; +} + +.scriptingObject-icon { +    padding: 3px; +    cursor: pointer; +} + +.scriptingObject-iconCollapsed { +    padding-left: 4px; +    padding-right: 5px; +} + +.scriptingObject-fields { +    padding-left: 10px; +} + +.scriptingObject-leaf { +    margin-left: 15px; +}
\ 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..6eabc7b70 --- /dev/null +++ b/src/client/views/ScriptingRepl.tsx @@ -0,0 +1,256 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import './ScriptingRepl.scss'; +import { Scripting, CompileScript, ts } from '../util/Scripting'; +import { DocumentManager } from '../util/DocumentManager'; +import { DocumentView } from './nodes/DocumentView'; +import { OverlayView } from './OverlayView'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; + +library.add(faCaretDown); +library.add(faCaretRight); + +@observer +export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { +    render() { +        this.props.view.props.ScreenToLocalTransform(); +        this.props.view.props.Document.width; +        this.props.view.props.Document.height; +        const screenCoords = this.props.view.screenRect(); + +        return ( +            <div className="documentIcon-outerDiv" style={{ +                position: "absolute", +                transform: `translate(${screenCoords.left + screenCoords.width / 2}px, ${screenCoords.top}px)`, +            }}> +                <p >${this.props.index}</p> +            </div> +        ); +    } +} + +@observer +export class DocumentIconContainer extends React.Component { +    render() { +        return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); +    } +} + +@observer +export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { +    @observable collapsed = true; + +    @action +    toggle = () => { +        this.collapsed = !this.collapsed; +        this.props.scrollToBottom(); +    } + +    render() { +        const val = this.props.value; +        const proto = Object.getPrototypeOf(val); +        const name = (proto && proto.constructor && proto.constructor.name) || String(val); +        const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; +        if (this.collapsed) { +            return ( +                <div className="scriptingObject-collapsed"> +                    <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length}) +                </div> +            ); +        } else { +            return ( +                <div className="scriptingObject-open"> +                    <div> +                        <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title} +                    </div> +                    <div className="scriptingObject-fields"> +                        {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)} +                    </div> +                </div> +            ); +        } +    } +} + +@observer +export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> { +    render() { +        const val = this.props.name ? this.props.value[this.props.name] : this.props.value; +        if (typeof val === "object") { +            return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />; +        } else if (typeof val === "function") { +            const name = "[Function]"; +            const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; +            return <div className="scriptingObject-leaf">{title}</div>; +        } else { +            const name = String(val); +            const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; +            return <div className="scriptingObject-leaf">{title}</div>; +        } +    } +} + +@observer +export class ScriptingRepl extends React.Component { +    @observable private commands: { command: string, result: any }[] = []; + +    @observable private commandString: string = ""; +    private commandBuffer: string = ""; + +    @observable private historyIndex: number = -1; + +    private commandsRef = React.createRef<HTMLDivElement>(); + +    private args: any = {}; + +    getTransformer: ts.TransformerFactory<ts.SourceFile> = context => { +        const knownVars: { [name: string]: number } = {}; +        const usedDocuments: number[] = []; +        Scripting.getGlobals().forEach(global => knownVars[global] = 1); +        return root => { +            function visit(node: ts.Node) { +                node = ts.visitEachChild(node, visit, context); + +                if (ts.isIdentifier(node)) { +                    const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; +                    const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; +                    if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { +                        const match = node.text.match(/\$([0-9]+)/); +                        if (match) { +                            const m = parseInt(match[1]); +                            usedDocuments.push(m); +                        } else { +                            return ts.createPropertyAccess(ts.createIdentifier("args"), node); +                        } +                    } +                } + +                return node; +            } +            return ts.visitNode(root, visit); +        }; +    } + +    @action +    onKeyDown = (e: React.KeyboardEvent) => { +        let stopProp = true; +        switch (e.key) { +            case "Enter": { +                const docGlobals: { [name: string]: any } = {}; +                DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`$${i}`] = dv.props.Document); +                const globals = Scripting.makeMutableGlobalsCopy(docGlobals); +                const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer, globals }); +                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.maybeScrollToBottom(); + +                this.commandString = ""; +                this.commandBuffer = ""; +                this.historyIndex = -1; +                break; +            } +            case "ArrowUp": { +                if (this.historyIndex < this.commands.length - 1) { +                    this.historyIndex++; +                    if (this.historyIndex === 0) { +                        this.commandBuffer = this.commandString; +                    } +                    this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command; +                } +                break; +            } +            case "ArrowDown": { +                if (this.historyIndex >= 0) { +                    this.historyIndex--; +                    if (this.historyIndex === -1) { +                        this.commandString = this.commandBuffer; +                        this.commandBuffer = ""; +                    } else { +                        this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command; +                    } +                } +                break; +            } +            default: +                stopProp = false; +                break; +        } + +        if (stopProp) { +            e.stopPropagation(); +            e.preventDefault(); +        } +    } + +    @action +    onChange = (e: React.ChangeEvent<HTMLInputElement>) => { +        this.commandString = e.target.value; +    } + +    private shouldScroll: boolean = false; +    private maybeScrollToBottom = () => { +        const ele = this.commandsRef.current; +        if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) { +            this.shouldScroll = true; +            this.forceUpdate(); +        } +    } + +    private scrollToBottom() { +        const ele = this.commandsRef.current; +        ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight }); +    } + +    componentDidUpdate() { +        if (this.shouldScroll) { +            this.shouldScroll = false; +            this.scrollToBottom(); +        } +    } + +    overlayDisposer?: () => void; +    onFocus = () => { +        if (this.overlayDisposer) { +            this.overlayDisposer(); +        } +        this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); +    } + +    onBlur = () => { +        this.overlayDisposer && this.overlayDisposer(); +    } + +    render() { +        return ( +            <div className="scriptingRepl-outerContainer"> +                <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}> +                    {this.commands.map(({ command, result }, i) => { +                        return ( +                            <div className="scriptingRepl-resultContainer" key={i}> +                                <div className="scriptingRepl-commandString">{command || <br />}</div> +                                <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div> +                            </div> +                        ); +                    })} +                </div> +                <input +                    className="scriptingRepl-commandInput" +                    onFocus={this.onFocus} +                    onBlur={this.onBlur} +                    value={this.commandString} +                    onChange={this.onChange} +                    onKeyDown={this.onKeyDown}></input> +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index eba69b448..72faf52c4 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -18,7 +18,8 @@ export enum CollectionViewType {      Schema,      Docking,      Tree, -    Stacking +    Stacking, +    Masonry  }  export interface CollectionRenderProps { @@ -78,7 +79,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {      @action.bound      addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { -        let self = this;          var curPage = NumCast(this.props.Document.curPage, -1);          Doc.GetProto(doc).page = curPage;          if (curPage >= 0) { @@ -146,7 +146,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {          const viewtype = this.collectionViewType;          return (              <div id="collectionBaseView" -                style={{ boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }} +                style={{ overflow: "auto", boxShadow: `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }}                  className={this.props.className || "collectionView-cont"}                  onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>                  {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f72b1aa07..2cf50e551 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -31,6 +31,8 @@ import { CollectionVideoView } from "./CollectionVideoView";  import { CollectionView } from "./CollectionView";  import { undoBatch } from "../../util/UndoManager";  import { timesSeries } from "async"; +import { ImageBox } from "../nodes/ImageBox"; +import { ComputedField } from "../../../new_fields/ScriptField";  library.add(faCog); @@ -446,8 +448,12 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre      drop = (e: Event, de: DragManager.DropEvent) => {          if (de.data instanceof DragManager.DocumentDragData) {              let docDrag = de.data; +            let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } });              this.props.childDocs && this.props.childDocs.map(otherdoc => { -                Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]); +                let doc = docDrag.draggedDocuments[0]; +                let target = Doc.GetProto(otherdoc); +                target.layout = target.detailedLayout = Doc.MakeDelegate(doc); +                computed.compiled && (target.miniLayout = new ComputedField(computed));              });              e.stopPropagation();          } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 7e886304d..7ebf5f77c 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,7 +1,9 @@  @import "../globalCssVariables";  .collectionStackingView { +    height: 100%; +    width: 100%; +    position: absolute;      overflow-y: auto; -      .collectionStackingView-docView-container {          width: 45%;          margin: 5% 2.5%; @@ -71,4 +73,13 @@          grid-column-end: span 1;          height: 100%;      } +    .collectionStackingView-sectionHeader {     +        width: 90%; +        background: gray; +        text-align: center; +        margin-left: 5%; +        margin-right: 5%; +        color: white; +        margin-top: 10px; +    }  }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6d9e942c9..0e5f9a321 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,9 +4,8 @@ import { action, computed, IReactionDisposer, reaction, untracked, observable, r  import { observer } from "mobx-react";  import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; +import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types";  import { emptyFunction, Utils } from "../../../Utils"; -import { ContextMenu } from "../ContextMenu";  import { CollectionSchemaPreview } from "./CollectionSchemaView";  import "./CollectionStackingView.scss";  import { CollectionSubView } from "./CollectionSubView"; @@ -21,8 +20,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      _masonryGridRef: HTMLDivElement | null = null;      _draggerRef = React.createRef<HTMLDivElement>();      _heightDisposer?: IReactionDisposer; -    _gridSize = 1;      _docXfs: any[] = []; +    _columnStart: number = 0;      @observable private cursor: CursorProperty = "grab";      @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }      @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } @@ -31,15 +30,25 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }      @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } +    @computed get Sections() { +        let sectionFilter = StrCast(this.props.Document.sectionFilter); +        let fields = new Map<object, Doc[]>(); +        sectionFilter && this.filteredChildren.map(d => { +            let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object; +            if (!fields.has(sectionValue)) fields.set(sectionValue, [d]); +            else fields.get(sectionValue)!.push(d); +        }); +        return fields; +    }      componentDidMount() {          this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],              () => this.singleColumn && -                (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => +                (this.props.Document.height = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) =>                      height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin))              , { fireImmediately: true });      }      componentWillUnmount() { -        if (this._heightDisposer) this._heightDisposer(); +        this._heightDisposer && this._heightDisposer();      }      @action @@ -87,7 +96,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          return (nw && nh) ? wid * aspect : d[HeightSym]();      } -      offsetTransform(doc: Doc, translateX: number, translateY: number) {          let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);          let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); @@ -97,6 +105,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);          return this.offsetTransform(doc, translateX, translateY);      } +      getSingleDocTransform(doc: Doc, ind: number, width: number) {          let localY = this.filteredChildren.reduce((height, d, i) =>              height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin); @@ -104,24 +113,24 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          return this.offsetTransform(doc, translate[0], translate[1]);      } -    @computed -    get children() { +    children(docs: Doc[]) {          this._docXfs.length = 0; -        return this.filteredChildren.map((d, i) => { +        return docs.map((d, i) => {              let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc);              let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth;              let height = () => this.getDocHeight(layoutDoc);              if (this.singleColumn) { +                //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.                  let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); -                let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; +                let rowHgtPcnt = height();                  this._docXfs.push({ dxf: dxf, width: width, height: height }); -                return <div className="collectionStackingView-columnDoc" key={d[Id]} style={{ width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}%` }} > +                return <div className="collectionStackingView-columnDoc" key={d[Id]} style={{ width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}` }} >                      {this.getDisplayDoc(layoutDoc, d, dxf)}                  </div>;              } else {                  let dref = React.createRef<HTMLDivElement>();                  let dxf = () => this.getDocTransform(layoutDoc, dref.current!); -                let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); +                let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);                  this._docXfs.push({ dxf: dxf, width: width, height: height });                  return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >                      {this.getDisplayDoc(layoutDoc, d, dxf)} @@ -130,7 +139,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          });      } -    _columnStart: number = 0;      columnDividerDown = (e: React.PointerEvent) => {          e.stopPropagation();          e.preventDefault(); @@ -144,7 +152,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {          let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];          let delta = dragPos - this._columnStart;          this._columnStart = dragPos; -          this.props.Document.columnWidth = this.columnWidth + delta;      } @@ -160,14 +167,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {              <FontAwesomeIcon icon={"arrows-alt-h"} />          </div>;      } -    onContextMenu = (e: React.MouseEvent): void => { -        if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 -            ContextMenu.Instance.addItem({ -                description: "Toggle multi-column", -                event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf" -            }); -        } -    }      @undoBatch      @action @@ -219,28 +218,40 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {              }          });      } -    render() { +    section(heading: string, docList: Doc[]) {          let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,              Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));          let templatecols = "";          for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; +        return <div key={heading}> +            {heading ? <div key={`${heading}`} className="collectionStackingView-sectionHeader">{heading}</div> : (null)} +            <div key={`${heading}-stack`} className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`} +                style={{ +                    padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, +                    margin: "auto", +                    width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`, +                    height: 'max-content', +                    position: "relative", +                    gridGap: this.gridGap, +                    gridTemplateColumns: this.singleColumn ? undefined : templatecols, +                    gridAutoRows: this.singleColumn ? undefined : "0px" +                }} +            > +                {this.children(docList)} +                {this.singleColumn ? (null) : this.columnDragger} +            </div></div>; +    } +    render() {          return ( -            <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > -                <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`} -                    style={{ -                        padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`, -                        margin: "auto", -                        width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`, -                        height: "100%", -                        position: "relative", -                        gridGap: this.gridGap, -                        gridTemplateColumns: this.singleColumn ? undefined : templatecols, -                        gridAutoRows: this.singleColumn ? undefined : `${this._gridSize}px` -                    }} -                > -                    {this.children} -                    {this.singleColumn ? (null) : this.columnDragger} -                </div> +            <div className="collectionStackingView" +                ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > +                {/* {sectionFilter as boolean ? [ +                    ["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())], +                    ["width = height", this.filteredChildren.filter(f => Math.abs(f[WidthSym]() - f[HeightSym]()) < 1)], +                    ["height > width", this.filteredChildren.filter(f => f[WidthSym]() + 1 <= f[HeightSym]())]]. */} +                {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()). +                    map(section => this.section(section[0].toString(), section[1] as Doc[])) : +                    this.section("", this.filteredChildren)}              </div>          );      } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 56750668d..045c8531e 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,11 +1,10 @@  import { library } from '@fortawesome/fontawesome-svg-core'; -import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV } from '@fortawesome/free-solid-svg-icons';  import { observer } from "mobx-react";  import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCast, WidthSym, HeightSym } from '../../../new_fields/Doc';  import { Id } from '../../../new_fields/FieldSymbols';  import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { Docs } from '../../documents/Documents';  import { undoBatch } from '../../util/UndoManager';  import { ContextMenu } from "../ContextMenu";  import { ContextMenuProps } from '../ContextMenuItem'; @@ -16,6 +15,8 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV  import { CollectionSchemaView } from "./CollectionSchemaView";  import { CollectionStackingView } from './CollectionStackingView';  import { CollectionTreeView } from "./CollectionTreeView"; +import { StrCast, PromiseValue } from '../../../new_fields/Types'; +import { DocumentType } from '../../documents/Documents';  export const COLLECTION_BORDER_WIDTH = 2;  library.add(faTh); @@ -24,6 +25,9 @@ library.add(faSquare);  library.add(faProjectDiagram);  library.add(faSignature);  library.add(faThList); +library.add(faColumns); +library.add(faEllipsisV); +library.add(faImage);  @observer  export class CollectionView extends React.Component<FieldViewProps> { @@ -35,7 +39,8 @@ export class CollectionView extends React.Component<FieldViewProps> {              case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);              case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);              case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); -            case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />); +            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView {...props} CollectionView={this} />); } +            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView {...props} CollectionView={this} />); }              case CollectionViewType.Freeform:              default:                  return (<CollectionFreeFormView {...props} CollectionView={this} />); @@ -45,6 +50,7 @@ export class CollectionView extends React.Component<FieldViewProps> {      get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } +    static _applyCount: number = 0;      onContextMenu = (e: React.MouseEvent): void => {          if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7              let subItems: ContextMenuProps[] = []; @@ -54,15 +60,19 @@ export class CollectionView extends React.Component<FieldViewProps> {              }              subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });              subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); -            subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" }); +            subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "ellipsis-v" }); +            subItems.push({ description: "Masonry", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Masonry), icon: "columns" });              ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });              ContextMenu.Instance.addItem({                  description: "Apply Template", event: undoBatch(() => {                      let otherdoc = new Doc(); -                    otherdoc.width = 100; -                    otherdoc.height = 50; -                    Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")"; -                    Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); +                    otherdoc.width = this.props.Document[WidthSym](); +                    otherdoc.height = this.props.Document[HeightSym](); +                    otherdoc.title = this.props.Document.title + "(..." + CollectionView._applyCount++ + ")"; // previously "applied" +                    otherdoc.layout = Doc.MakeDelegate(this.props.Document); +                    otherdoc.miniLayout = StrCast(this.props.Document.miniLayout); +                    otherdoc.detailedLayout = otherdoc.layout; +                    otherdoc.type = DocumentType.TEMPLATE;                      this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight");                  }), icon: "project-diagram"              }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4669ff142..4a085bb70 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -503,10 +503,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                          overlayDisposer();                          setTimeout(() => docs.map(d => d.transition = undefined), 1200);                      }} />; -                    overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options); +                    overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);                  }; -                addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined); -                addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); +                addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); +                addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");              }          });      } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fb8319934..5d3363d3a 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -35,6 +35,9 @@ 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'; +import { EditableView } from '../EditableView';  const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?  library.add(fa.faTrash); @@ -285,6 +288,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu      }      onClick = async (e: React.MouseEvent) => { +        if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.          e.stopPropagation();          let altKey = e.altKey;          let ctrlKey = e.ctrlKey; @@ -555,6 +559,10 @@ 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"          }); +        if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { +            cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); +        } +        cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });          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" }); @@ -661,10 +669,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu                          {!showTitle ? (null) :                              <div style={{                                  position: showTextTitle ? "relative" : "absolute", top: 0, textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre", +                                pointerEvents: "all",                                  overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",                                  transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`                              }}> -                                <span>{this.props.Document[showTitle]}</span> +                                <EditableView +                                    contents={this.props.Document[showTitle]} +                                    display={"block"} +                                    height={72} +                                    GetValue={() => StrCast(this.props.Document[showTitle])} +                                    SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle] = value) ? true : true} +                                />                              </div>                          }                          {!showCaption ? (null) : diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 99801ecff..0a79677e2 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -233,10 +233,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe                  return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;              },              field2 => { -                if (StrCast(this.props.Document.layout).indexOf("\"" + this.props.fieldKey + "\"") !== -1) { // bcz: UGH!  why is this needed... something is happening out of order.  test with making a collection, then adding a text note and converting that to a template field. -                    this._editorView && !this._applyingChange && -                        this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); -                } +                this._editorView && !this._applyingChange && +                    this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2)));              }          ); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 0f2d18f6b..83ad2a3b3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -52,13 +52,14 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD              this.Document.nativeHeight = this.Document.nativeWidth / aspect;              this.Document.height = FieldValue(this.Document.width, 0) / aspect;          } +        if (!this.Document.duration) this.Document.duration = this.player!.duration;      }      @action public Play = (update: boolean = true) => {          this.Playing = true;          update && this.player && this.player.play();          update && this._youtubePlayer && this._youtubePlayer.playVideo(); -        !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 500)); +        !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));          this.updateTimecode();      } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index f0a9ec6d8..162ac1d98 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -9,22 +9,6 @@ import React = require("react");  import { InkTool } from "../../../new_fields/InkField";  import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -export function onYouTubeIframeAPIReady() { -    console.log("player"); -    return; -    let player = new YT.Player('player', { -        events: { -            'onReady': onPlayerReady -        } -    }); -} -// must cast as any to set property on window -const _global = (window /* browser */ || global /* node */) as any; -_global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady; - -function onPlayerReady(event: any) { -    event.target.playVideo(); -}  @observer  export class WebBox extends React.Component<FieldViewProps> { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 59e61023f..2ad6ae5f0 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -10,6 +10,7 @@ import { RefField, FieldId } from "./RefField";  import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";  import { scriptingGlobal } from "../client/util/Scripting";  import { List } from "./List"; +import { DocumentType } from "../client/documents/Documents";  import { ComputedField } from "./ScriptField";  export namespace Field { @@ -317,7 +318,7 @@ export namespace Doc {          if (extensionDoc === undefined) {              setTimeout(() => {                  let docExtensionForField = new Doc(doc[Id] + fieldKey, true); -                docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey; +                docExtensionForField.title = doc.title + ":" + fieldKey + ".ext";                  docExtensionForField.extendsDoc = doc;                  let proto: Doc | undefined = doc;                  while (proto && !Doc.IsPrototype(proto)) { @@ -345,20 +346,23 @@ export namespace Doc {          // ... which means we change the layout to be an expanded view of the template layout.            // This allows the view override the template's properties and be referenceable as its own document. -        let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]]; +        let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]];          if (expandedTemplateLayout instanceof Doc) {              return expandedTemplateLayout;          }          if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) {              setTimeout(() => { -                templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc); -                (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title; -                (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc; +                let expandedDoc = Doc.MakeDelegate(templateLayoutDoc); +                expandedDoc.title = templateLayoutDoc.title + "[" + StrCast(dataDoc.title).match(/\.\.\.[0-9]*/) + "]"; +                expandedDoc.isExpandedTemplate = templateLayoutDoc; +                dataDoc[templateLayoutDoc[Id]] = expandedDoc;              }, 0);          } -        return templateLayoutDoc; +        return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.      } +    let _pendingExpansions: Map<string, boolean> = new Map(); +      export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {          const copy = new Doc;          Object.keys(doc).forEach(key => { @@ -386,12 +390,12 @@ export namespace Doc {      export function MakeDelegate(doc: Doc, id?: string): Doc;      export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>;      export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> { -        if (!doc) { -            return undefined; +        if (doc) { +            const delegate = new Doc(id, true); +            delegate.proto = doc; +            return delegate;          } -        const delegate = new Doc(id, true); -        delegate.proto = doc; -        return delegate; +        return undefined;      }      export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) { @@ -421,4 +425,11 @@ export namespace Doc {          fieldTemplate.showTitle = "title";          setTimeout(() => fieldTemplate.proto = proto);      } + +    export async function ToggleDetailLayout(d: Doc) { +        let miniLayout = await PromiseValue(d.miniLayout); +        let detailLayout = await PromiseValue(d.detailedLayout); +        d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout); +        if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined; +    }  }
\ No newline at end of file diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 4e3b7abe0..39c6c8ce3 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -2,7 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper";  import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";  import { ObjectField } from "./ObjectField";  import { Copy, ToScriptString } from "./FieldSymbols"; -import { deepCopy } from "../Utils"; +import { DeepCopy } from "../Utils";  export enum InkTool {      None, @@ -39,7 +39,7 @@ export class InkField extends ObjectField {      }      [Copy]() { -        return new InkField(deepCopy(this.inkData)); +        return new InkField(DeepCopy(this.inkData));      }      [ToScriptString]() { 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 {  | 
