From 6da55377db662a8fda3a241e7c9d115d33baff83 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 19 Jul 2019 18:33:17 -0400 Subject: json to documents conversion function --- src/client/documents/Documents.ts | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3c248760b..5a6eff04c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -439,6 +439,93 @@ export namespace Docs { export namespace Get { + /** + * 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. + * + * @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; + export function DocumentHierarchyFromJson(input: string, title?: string): Opt { + // preliminary check - making a document out of null or undefined is meaningless + if (input === null || input === undefined) { + return undefined; + } + let parsed: any = input; + // if we've received a string, treat it like valid JSON and try to parse it into an object. + if (typeof input === "string") { + try { + parsed = JSON.parse(input); + } catch (e) { + // 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. + parsed = input; + } + } else if (!["object", "boolean", "number"].includes(typeof input)) { + // since the caller might also pass in the results of a JSON.parse() call, input + // might be an object, an array (still typeof object), a boolean or a number. + // anything else (a function, etc. that is passed in naively as any) is meaningless for this operation + return undefined; + } + let converted: Doc; + if (typeof parsed === "object" && !(parsed instanceof Array)) { + // JavaScript object: this gets converted directly to a document, which is a also a list of key value pairs + converted = convertObject(parsed); + } else { + // Array, a boolean, a string or a number: this gets stored as a field in a wrapper document, since no key value structure exists + (converted = new Doc).json = valueOf(parsed); + } + // if we passed in a title, assign it + title && (converted.title = title); + return converted; + } + + const convertObject = (object: any): Doc => { + // create the document that will store this object's data + let target = new Doc(); + // for each value of the document, recursively convert it to a document or other field + // and store the field at the appropriate key in the document + Object.keys(object).map(key => { + let result = valueOf(object[key]); + // if the result is undefined, ignore it + result && (target[key] = result); + }); + return target; + }; + + const convertList = (list: Array): List => { + // create the list (Field implementation) that will store this Array's data + let thisLevel = new List(); + // for each element in the list, recursively convert it to a document or other field + // and push the field to the list + list.map(item => { + let result = valueOf(item); + // if the result is undefined, ignore it + result && thisLevel.push(result); + }); + return thisLevel; + }; + + const valueOf = (data: any): Opt => { + if (data === null || data === undefined) { + return undefined; + } + if (typeof data === "object") { + // recursively convert the object or array to the appropriate field value and return it + return data instanceof Array ? convertList(data) : convertObject(data); + } else if (["string", "number", "boolean"].includes(typeof data)) { + // no real conversion necessary - just return the data, already a valid field, to be stored + return data; + } else { + // any other type cannot be stored in JSON, but ya never know + throw new Error(`How did ${data} end up in JSON?`); + } + }; + export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise)) | undefined = undefined; if (type.indexOf("image") !== -1) { -- cgit v1.2.3-70-g09d2 From 6da5737c72abe18c366889273a179a3bf94351be Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Jul 2019 19:23:01 -0400 Subject: Added caching to computed fields --- src/new_fields/ScriptField.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') 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 { -- cgit v1.2.3-70-g09d2 From aa8a6f7a786410fe2f60cced3325eaee510adb09 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Jul 2019 19:23:46 -0400 Subject: Added start of REPL --- src/client/views/OverlayView.scss | 21 ++++++++++ src/client/views/OverlayView.tsx | 71 +++++++++++++++++++++++++++++---- src/client/views/ScriptingRepl.scss | 11 +++++ src/client/views/ScriptingRepl.tsx | 60 ++++++++++++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 3 ++ 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/client/views/OverlayView.scss create mode 100644 src/client/views/ScriptingRepl.scss create mode 100644 src/client/views/ScriptingRepl.tsx (limited to 'src') 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 { + @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 ( +
+
+ {this.props.overlayOptions.title || "Untitled"} + +
+ {this.props.children} +
+ ); + } +} + @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 = {ele}; + this._elements.push(ele); + return remove; } render() { return (
- {this._elements.map(({ ele, options: { x, y, width, height }, id }) => ( -
{ele}
- ))} + {this._elements}
); } 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) => { + this.commandString = e.target.value; + } + + render() { + return ( +
+
+ {this.commands.map(({ command, result }) => { + return ( +
+
{command}
+
{String(result)}
+
+ ); + })} +
+ +
+ ); + } +} \ 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(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(, { 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" }); -- cgit v1.2.3-70-g09d2 From fe188e93f2f8d61356bfdc3216273f72ae074e10 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 22 Jul 2019 00:10:01 -0400 Subject: Added a bunch of REPL functionality --- src/client/util/Scripting.ts | 69 ++++++- src/client/views/OverlayView.scss | 25 ++- src/client/views/OverlayView.tsx | 52 ++++- src/client/views/ScriptingRepl.scss | 39 ++++ src/client/views/ScriptingRepl.tsx | 228 +++++++++++++++++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- 7 files changed, 387 insertions(+), 30 deletions(-) (limited to 'src') 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; + 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/OverlayView.scss b/src/client/views/OverlayView.scss index 9d0abc96d..4d1e8cf0b 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -1,21 +1,42 @@ .overlayWindow-outerDiv { - position: absolute; border-radius: 5px; overflow: hidden; display: flex; flex-direction: column; } +.overlayWindow-outerDiv, +.overlayView-wrapperDiv { + position: absolute; + z-index: 1; +} + .overlayWindow-titleBar { - height: 30px; + 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 6b72abebf..2f2579057 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -25,16 +25,16 @@ export interface OverlayWindowProps { export class OverlayWindow extends React.Component { @observable x: number; @observable y: number; - @observable width?: number; - @observable height?: 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; + this.width = opts.width || 200; + this.height = opts.height || 200; } onPointerDown = (_: React.PointerEvent) => { @@ -44,10 +44,27 @@ export class OverlayWindow extends React.Component { 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) => { @@ -55,6 +72,11 @@ export class OverlayWindow extends React.Component { document.removeEventListener("pointerup", this.onPointerUp); } + onResizerPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onResizerPointerMove); + document.removeEventListener("pointerup", this.onResizerPointerUp); + } + render() { return (
@@ -62,7 +84,10 @@ export class OverlayWindow extends React.Component { {this.props.overlayOptions.title || "Untitled"}
- {this.props.children} +
+ {this.props.children} +
+
); } @@ -87,11 +112,26 @@ export class OverlayView extends React.Component { const index = this._elements.indexOf(ele); if (index !== -1) this._elements.splice(index, 1); }); - ele = {ele}; + ele =
{ele}
; 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 = {contents}; + this._elements.push(contents); + return remove; + } + render() { return (
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss index 1eedb52fa..f1ef64193 100644 --- a/src/client/views/ScriptingRepl.scss +++ b/src/client/views/ScriptingRepl.scss @@ -1,5 +1,8 @@ .scriptingRepl-outerContainer { background-color: whitesmoke; + height: 100%; + display: flex; + flex-direction: column; } .scriptingRepl-resultContainer { @@ -8,4 +11,40 @@ .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 index bd6fc9dfb..6eabc7b70 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -2,32 +2,193 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; import './ScriptingRepl.scss'; -import { Scripting, CompileScript } from '../util/Scripting'; +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 ( +
+

${this.props.index}

+
+ ); + } +} + +@observer +export class DocumentIconContainer extends React.Component { + render() { + return DocumentManager.Instance.DocumentViews.map((dv, i) => ); + } +} + +@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 ? <>{this.props.name} : {name} : name; + if (this.collapsed) { + return ( +
+ {title} (+{Object.keys(val).length}) +
+ ); + } else { + return ( +
+
+ {title} +
+
+ {Object.keys(val).map(key => )} +
+
+ ); + } + } +} + +@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 ; + } else if (typeof val === "function") { + const name = "[Function]"; + const title = this.props.name ? <>{this.props.name} : {name} : name; + return
{title}
; + } else { + const name = String(val); + const title = this.props.name ? <>{this.props.name} : {name} : name; + return
{title}
; + } + } +} @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(); private args: any = {}; + getTransformer: ts.TransformerFactory = 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) => { - if (e.key === "Enter") { - e.stopPropagation(); + 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(); - const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" } }); - if (!script.compiled) { - return; + this.commandString = ""; + this.commandBuffer = ""; + this.historyIndex = -1; + break; } - const result = script.run({ args: this.args }); - if (!result.success) { - return; + 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; } - this.commands.push({ command: this.commandString, result: result.result }); + 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; + } - this.commandString = ""; + if (stopProp) { + e.stopPropagation(); + e.preventDefault(); } } @@ -36,21 +197,56 @@ export class ScriptingRepl extends React.Component { 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(, { x: 0, y: 0 }); + } + + onBlur = () => { + this.overlayDisposer && this.overlayDisposer(); + } + render() { return (
-
- {this.commands.map(({ command, result }) => { +
+ {this.commands.map(({ command, result }, i) => { return ( -
-
{command}
-
{String(result)}
+
+
{command ||
}
+
{}
); })}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 58218e641..652aca8f3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -502,7 +502,7 @@ 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}"); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 52ba643e0..7ff095573 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -557,7 +557,7 @@ export class DocumentView extends DocComponent(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(, { x: 100, y: 100 }) }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200 }) }); 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" }); -- cgit v1.2.3-70-g09d2 From 33744cb0068d7dc470d0b39ca70344f256a9cad9 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 22 Jul 2019 00:12:31 -0400 Subject: Added titles to overlay windows --- .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 ++-- src/client/views/nodes/DocumentView.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 652aca8f3..703873681 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -504,8 +504,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }} />; 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 7ff095573..1c03bfc81 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -557,7 +557,7 @@ export class DocumentView extends DocComponent(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.addWindow(, { x: 300, y: 100, width: 200, height: 200 }) }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(, { 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" }); -- cgit v1.2.3-70-g09d2 From 2899cbc887398688b82dd284ee482cdb136c6452 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 22 Jul 2019 00:38:50 -0400 Subject: json conversion tweaks --- src/Utils.ts | 16 +++++- src/client/documents/Documents.ts | 102 ++++++++++++++++++-------------------- src/new_fields/InkField.ts | 4 +- 3 files changed, 64 insertions(+), 58 deletions(-) (limited to 'src') 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 = Pick>; export type Predicate = (entry: [K, V]) => boolean; -export function deepCopy(source: Map, predicate?: Predicate) { +export function DeepCopy(source: Map, predicate?: Predicate) { let deepCopy = new Map(); let entries = source.entries(), next = entries.next(); while (!next.done) { @@ -157,4 +157,18 @@ export function deepCopy(source: Map, predicate?: Predicate) { 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 5a6eff04c..de049be5a 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"; @@ -439,91 +439,83 @@ 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; - export function DocumentHierarchyFromJson(input: string, title?: string): Opt { - // preliminary check - making a document out of null or undefined is meaningless - if (input === null || input === undefined) { - return undefined; - } - let parsed: any = input; - // if we've received a string, treat it like valid JSON and try to parse it into an object. - if (typeof input === "string") { - try { - parsed = JSON.parse(input); - } catch (e) { - // 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. - parsed = input; - } - } else if (!["object", "boolean", "number"].includes(typeof input)) { - // since the caller might also pass in the results of a JSON.parse() call, input - // might be an object, an array (still typeof object), a boolean or a number. - // anything else (a function, etc. that is passed in naively as any) is meaningless for this operation + export function DocumentHierarchyFromJson(input: any, title?: string): Opt { + 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)) { - // JavaScript object: this gets converted directly to a document, which is a also a list of key value pairs - converted = convertObject(parsed); + converted = convertObject(parsed, title); } else { - // Array, a boolean, a string or a number: this gets stored as a field in a wrapper document, since no key value structure exists - (converted = new Doc).json = valueOf(parsed); + (converted = new Doc).json = toField(parsed); } - // if we passed in a title, assign it title && (converted.title = title); return converted; } - const convertObject = (object: any): Doc => { - // create the document that will store this object's data - let target = new Doc(); - // for each value of the document, recursively convert it to a document or other field - // and store the field at the appropriate key in the document - Object.keys(object).map(key => { - let result = valueOf(object[key]); - // if the result is undefined, ignore it - result && (target[key] = result); - }); + /** + * 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; + 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): List => { - // create the list (Field implementation) that will store this Array's data - let thisLevel = new List(); - // for each element in the list, recursively convert it to a document or other field - // and push the field to the list - list.map(item => { - let result = valueOf(item); - // if the result is undefined, ignore it - result && thisLevel.push(result); - }); - return thisLevel; + let target = new List(), result: Opt; + list.map(item => (result = toField(item)) && target.push(result)); + return target; }; - const valueOf = (data: any): Opt => { + + const toField = (data: any, title?: string): Opt => { if (data === null || data === undefined) { return undefined; } - if (typeof data === "object") { - // recursively convert the object or array to the appropriate field value and return it - return data instanceof Array ? convertList(data) : convertObject(data); - } else if (["string", "number", "boolean"].includes(typeof data)) { - // no real conversion necessary - just return the data, already a valid field, to be stored + if (primitives.includes(typeof data)) { return data; - } else { - // any other type cannot be stored in JSON, but ya never know - throw new Error(`How did ${data} end up in JSON?`); } + 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> { 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]() { -- cgit v1.2.3-70-g09d2 From 3442776999d1d7b7bced9bbed601ead637a43cc7 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 10:24:37 -0400 Subject: merged stacking sections branch. --- src/client/views/DocumentDecorations.tsx | 10 +- .../views/collections/CollectionBaseView.tsx | 6 +- .../views/collections/CollectionStackingView.scss | 13 +- .../views/collections/CollectionStackingView.tsx | 247 --------------------- src/client/views/collections/CollectionView.tsx | 28 ++- src/client/views/nodes/WebBox.tsx | 16 -- src/new_fields/Doc.ts | 33 ++- 7 files changed, 64 insertions(+), 289 deletions(-) delete mode 100644 src/client/views/collections/CollectionStackingView.tsx (limited to 'src') 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/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 { @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 { const viewtype = this.collectionViewType; return (
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)} 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 deleted file mode 100644 index 6d9e942c9..000000000 --- a/src/client/views/collections/CollectionStackingView.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx"; -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 { emptyFunction, Utils } from "../../../Utils"; -import { ContextMenu } from "../ContextMenu"; -import { CollectionSchemaPreview } from "./CollectionSchemaView"; -import "./CollectionStackingView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -import { undoBatch } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; -import { DocumentType } from "../../documents/Documents"; -import { Transform } from "../../util/Transform"; -import { CursorProperty } from "csstype"; - -@observer -export class CollectionStackingView extends CollectionSubView(doc => doc) { - _masonryGridRef: HTMLDivElement | null = null; - _draggerRef = React.createRef(); - _heightDisposer?: IReactionDisposer; - _gridSize = 1; - _docXfs: any[] = []; - @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); } - @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } - @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } - @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); } - - 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) => - height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) - , { fireImmediately: true }); - } - componentWillUnmount() { - if (this._heightDisposer) this._heightDisposer(); - } - - @action - moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { - return this.props.removeDocument(doc) && addDocument(doc); - } - createRef = (ele: HTMLDivElement | null) => { - this._masonryGridRef = ele; - this.createDropTarget(ele!); - } - - overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; - } - - getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { - let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; - let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.getDocHeight(layoutDoc); - let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); - return - ; - } - getDocHeight(d: Doc) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - let aspect = nw && nh ? nh / nw : 1; - let wid = Math.min(d[WidthSym](), this.columnWidth); - 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); - return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); - } - getDocTransform(doc: Doc, dref: HTMLDivElement) { - 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); - let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); - return this.offsetTransform(doc, translate[0], translate[1]); - } - - @computed - get children() { - this._docXfs.length = 0; - return this.filteredChildren.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) { - let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); - let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; - this._docXfs.push({ dxf: dxf, width: width, height: height }); - return
- {this.getDisplayDoc(layoutDoc, d, dxf)} -
; - } else { - let dref = React.createRef(); - let dxf = () => this.getDocTransform(layoutDoc, dref.current!); - let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); - this._docXfs.push({ dxf: dxf, width: width, height: height }); - return
- {this.getDisplayDoc(layoutDoc, d, dxf)} -
; - } - }); - } - - _columnStart: number = 0; - columnDividerDown = (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - runInAction(() => this.cursor = "grabbing"); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - } - @action - onDividerMove = (e: PointerEvent): void => { - 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; - } - - @action - onDividerUp = (e: PointerEvent): void => { - runInAction(() => this.cursor = "grab"); - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); - } - - @computed get columnDragger() { - return
- -
; - } - 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 - drop = (e: Event, de: DragManager.DropEvent) => { - let targInd = -1; - let where = [de.x, de.y]; - if (de.data instanceof DragManager.DocumentDragData) { - this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); - if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { - targInd = i; - } - }); - } - if (super.drop(e, de)) { - let newDoc = de.data.droppedDocuments[0]; - let docs = this.childDocList; - if (docs) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - let srcInd = docs.indexOf(newDoc); - docs.splice(srcInd, 1); - docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); - } - } - return false; - } - @undoBatch - @action - onDrop = (e: React.DragEvent): void => { - let where = [e.clientX, e.clientY]; - let targInd = -1; - this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); - if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { - targInd = i; - } - }); - super.onDrop(e, {}, () => { - if (targInd !== -1) { - let newDoc = this.childDocs[this.childDocs.length - 1]; - let docs = this.childDocList; - if (docs) { - docs.splice(docs.length - 1, 1); - docs.splice(targInd, 0, newDoc); - } - } - }); - } - render() { - 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 ( -
e.stopPropagation()} > -
- {this.children} - {this.singleColumn ? (null) : this.columnDragger} -
-
- ); - } -} \ No newline at end of file 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 { @@ -35,7 +39,8 @@ export class CollectionView extends React.Component { case CollectionViewType.Schema: return (); case CollectionViewType.Docking: return (); case CollectionViewType.Tree: return (); - case CollectionViewType.Stacking: return (); + case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (); } + case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (); } case CollectionViewType.Freeform: default: return (); @@ -45,6 +50,7 @@ export class CollectionView extends React.Component { 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 { } 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/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 { 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 = 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, id?: string): Opt; export function MakeDelegate(doc: Opt, id?: string): Opt { - 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 -- cgit v1.2.3-70-g09d2 From 090d0b67dcc5148ecfbfac01cd9f1ddd3a52d8a7 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:16:57 -0400 Subject: fixed missing stacking view. made titles editable. --- src/client/views/EditableView.scss | 1 + src/client/views/EditableView.tsx | 1 + .../views/collections/CollectionStackingView.tsx | 247 +++++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 11 +- 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 src/client/views/collections/CollectionStackingView.tsx (limited to 'src') 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 { @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/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx new file mode 100644 index 000000000..6d9e942c9 --- /dev/null +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -0,0 +1,247 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx"; +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 { emptyFunction, Utils } from "../../../Utils"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionSchemaPreview } from "./CollectionSchemaView"; +import "./CollectionStackingView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; +import { DocumentType } from "../../documents/Documents"; +import { Transform } from "../../util/Transform"; +import { CursorProperty } from "csstype"; + +@observer +export class CollectionStackingView extends CollectionSubView(doc => doc) { + _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef(); + _heightDisposer?: IReactionDisposer; + _gridSize = 1; + _docXfs: any[] = []; + @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); } + @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } + @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } + @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); } + + 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) => + height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) + , { fireImmediately: true }); + } + componentWillUnmount() { + if (this._heightDisposer) this._heightDisposer(); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + return this.props.removeDocument(doc) && addDocument(doc); + } + createRef = (ele: HTMLDivElement | null) => { + this._masonryGridRef = ele; + this.createDropTarget(ele!); + } + + overlays = (doc: Doc) => { + return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; + } + + getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { + let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.getDocHeight(layoutDoc); + let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); + return + ; + } + getDocHeight(d: Doc) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + let aspect = nw && nh ? nh / nw : 1; + let wid = Math.min(d[WidthSym](), this.columnWidth); + 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); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); + } + getDocTransform(doc: Doc, dref: HTMLDivElement) { + 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); + let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); + return this.offsetTransform(doc, translate[0], translate[1]); + } + + @computed + get children() { + this._docXfs.length = 0; + return this.filteredChildren.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) { + let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); + let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return
+ {this.getDisplayDoc(layoutDoc, d, dxf)} +
; + } else { + let dref = React.createRef(); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); + let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return
+ {this.getDisplayDoc(layoutDoc, d, dxf)} +
; + } + }); + } + + _columnStart: number = 0; + columnDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + runInAction(() => this.cursor = "grabbing"); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + } + @action + onDividerMove = (e: PointerEvent): void => { + 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; + } + + @action + onDividerUp = (e: PointerEvent): void => { + runInAction(() => this.cursor = "grab"); + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + } + + @computed get columnDragger() { + return
+ +
; + } + 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 + drop = (e: Event, de: DragManager.DropEvent) => { + let targInd = -1; + let where = [de.x, de.y]; + if (de.data instanceof DragManager.DocumentDragData) { + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + } + if (super.drop(e, de)) { + let newDoc = de.data.droppedDocuments[0]; + let docs = this.childDocList; + if (docs) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + let srcInd = docs.indexOf(newDoc); + docs.splice(srcInd, 1); + docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); + } + } + return false; + } + @undoBatch + @action + onDrop = (e: React.DragEvent): void => { + let where = [e.clientX, e.clientY]; + let targInd = -1; + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + super.onDrop(e, {}, () => { + if (targInd !== -1) { + let newDoc = this.childDocs[this.childDocs.length - 1]; + let docs = this.childDocList; + if (docs) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + } + }); + } + render() { + 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 ( +
e.stopPropagation()} > +
+ {this.children} + {this.singleColumn ? (null) : this.columnDragger} +
+
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1c03bfc81..e0975f7bd 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -37,6 +37,7 @@ 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); @@ -287,6 +288,7 @@ export class DocumentView extends DocComponent(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; @@ -664,10 +666,17 @@ export class DocumentView extends DocComponent(Docu {!showTitle ? (null) :
- {this.props.Document[showTitle]} + StrCast(this.props.Document[showTitle])} + SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle] = value) ? true : true} + />
} {!showCaption ? (null) : -- cgit v1.2.3-70-g09d2 From a4820fade4149196ecbe8e30de31f7b8e6b5057f Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:28:27 -0400 Subject: what's up with StackingView - bad merge? --- .../views/collections/CollectionStackingView.tsx | 89 ++++++++++++---------- 1 file changed, 50 insertions(+), 39 deletions(-) (limited to 'src') 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(); _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(); + 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
+ return
{this.getDisplayDoc(layoutDoc, d, dxf)}
; } else { let dref = React.createRef(); 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
{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) {
; } - 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
+ {heading ?
{heading}
: (null)} +
+ {this.children(docList)} + {this.singleColumn ? (null) : this.columnDragger} +
; + } + render() { return ( -
e.stopPropagation()} > -
- {this.children} - {this.singleColumn ? (null) : this.columnDragger} -
+
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)}
); } -- cgit v1.2.3-70-g09d2 From 8db50c6ba0be83b85c896043da53e40c17523e90 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:45:32 -0400 Subject: more missing stuff .. bad merge? --- src/client/documents/Documents.ts | 3 ++- src/client/views/nodes/FormattedTextBox.tsx | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index de049be5a..7563fda20 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -55,7 +55,8 @@ export enum DocumentType { ICON = "icon", IMPORT = "import", LINK = "link", - LINKDOC = "linkdoc" + LINKDOC = "linkdoc", + TEMPLATE = "template" } export interface DocumentOptions { 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))); } ); -- cgit v1.2.3-70-g09d2 From 872d5b1ddd8868d316b0775317a73a04de8c240b Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 15:34:40 -0400 Subject: added duration for videos and changed the timer to 5 milliseconds. --- src/client/views/nodes/VideoBox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') 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(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(); } -- cgit v1.2.3-70-g09d2 From 3e3d592e677a30c7fb7ca721293ea09b231a97a3 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 22 Jul 2019 15:36:21 -0400 Subject: buxton templating improvments --- src/client/views/collections/CollectionSchemaView.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') 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 { 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(); } -- cgit v1.2.3-70-g09d2 From 291ae12b0c4587c4f656caac6d402413cc8ec76d Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 22 Jul 2019 15:41:55 -0400 Subject: added toggle detail context menu item --- src/client/views/nodes/DocumentView.tsx | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e0975f7bd..5d3363d3a 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -559,6 +559,9 @@ export class DocumentView extends DocComponent(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(, { 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" }); -- cgit v1.2.3-70-g09d2 From e87727dd111a5370a064e675b9b7c966cb84dfb0 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 15:46:53 -0400 Subject: fixed link following to video frames. --- src/client/views/nodes/DocumentView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e0975f7bd..9429f0d78 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -351,7 +351,7 @@ export class DocumentView extends DocComponent(Docu // @TODO: shouldn't always follow target context let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; - let linkedFwdPage = [first.length ? NumCast(first[0].linkedToPage, undefined) : undefined, undefined]; + let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; if (!linkedFwdDocs.some(l => l instanceof Promise)) { let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); -- cgit v1.2.3-70-g09d2 From c351d0d4f6f4e8492cade7d3056e516178f4a138 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 17:08:25 -0400 Subject: fixed seeking for html video player element --- .../views/collections/CollectionVideoView.tsx | 40 +++++++++++++++++----- src/client/views/nodes/VideoBox.tsx | 1 + 2 files changed, 33 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index d7d5773ba..31a8a93e0 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -7,6 +7,8 @@ import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from ". import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionVideoView.scss"; import React = require("react"); +import { InkingControl } from "../InkingControl"; +import { InkTool } from "../../../new_fields/InkField"; @observer @@ -19,18 +21,19 @@ export class CollectionVideoView extends React.Component { private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); let curTime = NumCast(this.props.Document.curPage); - return (VideoBox._showControls ? [] : [ -
- {"" + Math.round(curTime)} - {" " + Math.round((curTime - Math.trunc(curTime)) * 100)} -
, + return ([
+ {"" + Math.round(curTime)} + {" " + Math.round((curTime - Math.trunc(curTime)) * 100)} +
, + VideoBox._showControls ? (null) : [
{this._videoBox && this._videoBox.Playing ? "\"" : ">"}
,
F
- ]); + + ]]); } @action @@ -53,12 +56,33 @@ export class CollectionVideoView extends React.Component { } } + _isclick = 0; @action - onResetDown = () => { + onResetDown = (e: React.PointerEvent) => { if (this._videoBox) { this._videoBox.Pause(); - this.props.Document.curPage = 0; + e.stopPropagation(); + this._isclick = 0; + document.addEventListener("pointermove", this.onPointerMove, true); + document.addEventListener("pointerup", this.onPointerUp, true); + InkingControl.Instance.switchTool(InkTool.Eraser); + } + } + + @action + onPointerMove = (e: PointerEvent) => { + this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY); + if (this._videoBox) { + this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333)); } + e.stopImmediatePropagation(); + } + @action + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove, true); + document.removeEventListener("pointerup", this.onPointerUp, true); + InkingControl.Instance.switchTool(InkTool.None); + this._isclick < 10 && (this.props.Document.curPage = 0); } setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 83ad2a3b3..5624d41a9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -65,6 +65,7 @@ export class VideoBox extends DocComponent(VideoD @action public Seek(time: number) { this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true); + this.player && (this.player.currentTime = time); } @action public Pause = (update: boolean = true) => { -- cgit v1.2.3-70-g09d2