aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTyler Schicke <tschicke@gmail.com>2019-07-22 00:10:01 -0400
committerTyler Schicke <tschicke@gmail.com>2019-07-22 00:10:01 -0400
commitfe188e93f2f8d61356bfdc3216273f72ae074e10 (patch)
tree3e87a601a45f697924569366126d1e91cde5b142 /src
parentb9a8ff80b504d02dbecd007d56379a44b0d4e721 (diff)
Added a bunch of REPL functionality
Diffstat (limited to 'src')
-rw-r--r--src/client/util/Scripting.ts69
-rw-r--r--src/client/views/OverlayView.scss25
-rw-r--r--src/client/views/OverlayView.tsx52
-rw-r--r--src/client/views/ScriptingRepl.scss39
-rw-r--r--src/client/views/ScriptingRepl.tsx228
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx2
7 files changed, 387 insertions, 30 deletions
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/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<OverlayWindowProps> {
@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<OverlayWindowProps> {
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<OverlayWindowProps> {
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 }}>
@@ -62,7 +84,10 @@ export class OverlayWindow extends React.Component<OverlayWindowProps> {
{this.props.overlayOptions.title || "Untitled"}
<button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
</div>
- {this.props.children}
+ <div className="overlayWindow-content">
+ {this.props.children}
+ </div>
+ <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
</div>
);
}
@@ -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 = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{ele}</OverlayWindow>;
+ 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>
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 (
+ <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) => {
- 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(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+
+ onBlur = () => {
+ this.overlayDisposer && this.overlayDisposer();
+ }
+
render() {
return (
<div className="scriptingRepl-outerContainer">
- <div className="scriptingRepl-commandsContainer">
- {this.commands.map(({ command, result }) => {
+ <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}>
+ {this.commands.map(({ command, result }, i) => {
return (
- <div className="scriptingRepl-resultContainer">
- <div className="scriptingRepl-commandString">{command}</div>
- <div className="scriptingRepl-commandResult">{String(result)}</div>
+ <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>
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<DocumentViewProps, Document>(Docu
this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc?
}, icon: "search"
});
- cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addElement(<ScriptingRepl />, { x: 100, y: 100 }) });
+ cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { 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" });