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