From 8605e1e0dd65ebaea18b5e0d6856fa0c05d285ff Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Tue, 16 Jul 2019 22:28:32 -0400
Subject: Added zoom to document to dv context menu

---
 src/client/views/nodes/DocumentView.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 970ef24d8..245dd319d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -556,6 +556,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }, icon: "search"
         });
         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(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
         cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
         cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
-- 
cgit v1.2.3-70-g09d2


From 65d5e3ca83b6376ec59ba4c01224c6fc3957fb15 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 17 Jul 2019 15:28:14 -0400
Subject: fixed title/caption.

---
 src/client/views/nodes/DocumentView.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 245dd319d..672c64256 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -654,6 +654,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             >
                 {!showTitle && !showCaption ? this.contents :
                     <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
+
+                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
+                            {this.contents}
+                        </div>
                         {!showTitle ? (null) :
                             <div style={{
                                 position: showTextTitle ? "relative" : "absolute", top: 0, textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
@@ -668,9 +672,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                                 <FormattedTextBox {...this.props} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
                             </div>
                         }
-                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
-                            {this.contents}
-                        </div>
                     </div>
                 }
             </div>
-- 
cgit v1.2.3-70-g09d2


From 35d9c83f8d54e6eee1d0fa7b977b5f2176094791 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 17 Jul 2019 15:46:49 -0400
Subject: from last

---
 src/client/views/nodes/DocumentView.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 672c64256..01db40fc7 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -655,7 +655,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 {!showTitle && !showCaption ? this.contents :
                     <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
 
-                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: showTextTitle ? "relative" : "absolute" }}>
+                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 25px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "25px" : undefined }}>
                             {this.contents}
                         </div>
                         {!showTitle ? (null) :
-- 
cgit v1.2.3-70-g09d2


From 5aa6bcdd4e23bb9e9d05181d0dc6b638e45e397b Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 17 Jul 2019 20:48:14 -0400
Subject: Moved DocServer.prepend and changed how corsProxy works

---
 src/Utils.ts                                       | 13 +++++
 src/client/DocServer.ts                            |  8 ----
 src/client/util/History.ts                         |  2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 +-
 src/client/util/SearchUtil.ts                      |  3 +-
 src/client/util/TooltipTextMenu.tsx                |  8 ++--
 src/client/views/MainView.tsx                      |  4 +-
 src/client/views/SearchBox.tsx                     |  3 +-
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  7 +--
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  5 +-
 src/client/views/nodes/ImageBox.tsx                |  6 +--
 src/client/views/nodes/VideoBox.tsx                |  4 +-
 src/client/views/pdf/PDFViewer.tsx                 | 55 +++++-----------------
 src/client/views/search/SearchBox.tsx              |  6 +--
 src/mobile/ImageUpload.tsx                         |  5 +-
 .../authentication/models/current_user_utils.ts    |  5 +-
 src/server/index.ts                                |  2 +-
 19 files changed, 65 insertions(+), 81 deletions(-)

(limited to 'src/client/views/nodes/DocumentView.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index e8a80bdc3..ac6d127cc 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,6 +2,7 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
+import { RouteStore } from './server/RouteStore';
 
 export class Utils {
 
@@ -27,6 +28,18 @@ export class Utils {
         return { scale, translateX, translateY };
     }
 
+    /**
+     * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
+     * requested extension
+     * @param extension the specified sub-path to append to the window origin
+     */
+    public static prepend(extension: string): string {
+        return window.location.origin + extension;
+    }
+    public static CorsProxy(url: string): string {
+        return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+    }
+
     public static CopyText(text: string) {
         var textArea = document.createElement("textarea");
         textArea.value = text;
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6737657c8..8c64d2b2f 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -47,14 +47,6 @@ export namespace DocServer {
         Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
         Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
     }
-    /**
-     * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
-     * requested extension
-     * @param extension the specified sub-path to append to the window origin
-     */
-    export function prepend(extension: string): string {
-        return window.location.origin + extension;
-    }
 
     function errorFunc(): never {
         throw new Error("Can't use DocServer without calling init first");
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index cbf5b3fc8..e9ff21b22 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -129,7 +129,7 @@ export namespace HistoryUtil {
 
     function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
         stringifiers[type] = state => {
-            let path = DocServer.prepend(`/${type}`);
+            let path = Utils.prepend(`/${type}`);
             if (customStringifier) {
                 path = customStringifier(state, path);
             }
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c096e9ceb..75b0b52a7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -98,12 +98,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
             runInAction(() => this.remaining++);
 
-            let prom = fetch(DocServer.prepend(RouteStore.upload), {
+            let prom = fetch(Utils.prepend(RouteStore.upload), {
                 method: 'POST',
                 body: formData
             }).then(async (res: Response) => {
                 (await res.json()).map(action((file: any) => {
-                    let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
+                    let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
                     docPromise.then(doc => {
                         doc && docs.push(doc) && runInAction(() => this.remaining--);
                     });
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index abf1a7c32..ee5a83710 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -2,6 +2,7 @@ import * as rp from 'request-promise';
 import { DocServer } from '../DocServer';
 import { Doc } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
+import { Utils } from '../../Utils';
 
 export namespace SearchUtil {
     export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
@@ -29,7 +30,7 @@ export namespace SearchUtil {
     export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
     export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
         query = query || "*"; //If we just have a filter query, search for * as the query
-        const result: IdSearchResult = JSON.parse(await rp.get(DocServer.prepend("/search"), {
+        const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
             qs: { ...options, q: query },
         }));
         if (!returnDocs) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 309d19016..a4c053de2 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,7 +19,7 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie
 import { DocumentManager } from "./DocumentManager";
 import { Id } from "../../new_fields/FieldSymbols";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { SelectionManager } from "./SelectionManager";
+import { Utils } from "../../Utils";
 
 //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
 export class TooltipTextMenu {
@@ -213,8 +213,8 @@ export class TooltipTextMenu {
                 let link = node && node.marks.find(m => m.type.name === "link");
                 if (link) {
                     let href: string = link.attrs.href;
-                    if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                        let docid = href.replace(DocServer.prepend("/doc/"), "");
+                    if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                        let docid = href.replace(Utils.prepend("/doc/"), "");
                         DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
                             if (f instanceof Doc) {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
@@ -254,7 +254,7 @@ export class TooltipTextMenu {
                                 if (docView && docView.props.ContainingCollectionView) {
                                     proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
                                 }
-                                linkDoc instanceof Doc && this.makeLink(DocServer.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
+                                linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
                             }),
                         },
                         hideSource: false
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce7369220..94a4835a1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -16,7 +16,7 @@ import { listSpec } from '../../new_fields/Schema';
 import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { SetupDrag } from '../util/DragManager';
@@ -433,7 +433,7 @@ export class MainView extends React.Component {
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={() => window.location.assign(DocServer.prepend(RouteStore.logout))}>Log Out</button></div>
+                <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
         ];
 
     }
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 1b9be841f..33cb63df5 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -13,6 +13,7 @@ import { Docs } from '../documents/Documents';
 import { SetupDrag } from '../util/DragManager';
 import { SearchItem } from './search/SearchItem';
 import "./SearchBox.scss";
+import { Utils } from '../../Utils';
 
 library.add(faSearch);
 library.add(faObjectGroup);
@@ -47,7 +48,7 @@ export class SearchBox extends React.Component {
 
     @action
     getResults = async (query: string) => {
-        let response = await rp.get(DocServer.prepend('/search'), {
+        let response = await rp.get(Utils.prepend('/search'), {
             qs: {
                 query
             }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a193ff677..ba7903419 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -465,7 +465,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             .off('click') //unbind the current click handler
             .click(action(function () {
                 stack.config.fixed = !stack.config.fixed;
-                // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+                // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
                 // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
             }));
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5d5189336..2ddefb3c0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -20,6 +20,7 @@ import { CollectionVideoView } from "./CollectionVideoView";
 import { CollectionView } from "./CollectionView";
 import React = require("react");
 import { MainView } from "../MainView";
+import { Utils } from "../../../Utils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -164,7 +165,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 } else {
                     let path = window.location.origin + "/doc/";
                     if (text.startsWith(path)) {
-                        let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+                        let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
                         DocServer.GetRefField(docid).then(f => {
                             if (f instanceof Doc) {
                                 if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
@@ -193,7 +194,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
                     let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
-                        .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+                        .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
                         .then(result => {
                             let type = result["content-type"];
                             if (type) {
@@ -219,7 +220,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     }).then(async (res: Response) => {
                         (await res.json()).map(action((file: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let path = DocServer.prepend(file);
+                            let path = Utils.prepend(file);
                             Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
                         }));
                     });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 245dd319d..af1cc079b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -557,13 +557,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
         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(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+        cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
         cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
         cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
         type User = { email: string, userDocumentId: string };
         let usersMenu: ContextMenuProps[] = [];
         try {
-            let stuff = await rp.get(DocServer.prepend(RouteStore.getUsers));
+            let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
             const users: User[] = JSON.parse(stuff);
             usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
                 description: email, event: async () => {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 066cc40e2..99801ecff 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -35,6 +35,7 @@ import "./FormattedTextBox.scss";
 import React = require("react");
 import { DateField } from '../../../new_fields/DateField';
 import { thisExpression } from 'babel-types';
+import { Utils } from '../../../Utils';
 
 library.add(faEdit);
 library.add(faSmile);
@@ -311,8 +312,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
             }
             if (href) {
-                if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
-                    this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+                if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+                    this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     if (this._linkClicked) {
                         DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
                             if (linkDoc instanceof Doc) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ebe3732e6..4ffd265d7 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -165,12 +165,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(DocServer.prepend(RouteStore.upload), {
+                const res = await fetch(Utils.prepend(RouteStore.upload), {
                     method: 'POST',
                     body: formData
                 });
                 const files = await res.json();
-                const url = DocServer.prepend(files[0]);
+                const url = Utils.prepend(files[0]);
                 // upload to server with known URL 
                 let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
                 audioDoc.embed = true;
@@ -327,7 +327,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
         let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
         let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
-        let paths: string[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"];
+        let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 8b8e500c4..0f2d18f6b 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -122,7 +122,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = DocServer.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend(RouteStore.dataUriToImage);
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -164,7 +164,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                     let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
                     VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
                         if (returnedFilename) {
-                            let url = DocServer.prepend(returnedFilename);
+                            let url = Utils.prepend(returnedFilename);
                             let imageSummary = Docs.Create.ImageDocument(url, {
                                 x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
                                 width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 699a1ffd7..b7ded7e06 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,23 +4,18 @@ import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
 import * as rp from "request-promise";
 import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
-import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
-import { emptyFunction } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
-import { DocumentManager } from "../../util/DocumentManager";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
-import { PDFBox, handleBackspace } from "../nodes/PDFBox";
+import { PDFBox } from "../nodes/PDFBox";
 import Page from "./Page";
 import "./PDFViewer.scss";
 import React = require("react");
-import PDFMenu from "./PDFMenu";
-import { UndoManager } from "../../util/UndoManager";
-import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { CompileScript, CompileResult } from "../../util/Scripting";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import Annotation from "./Annotation";
@@ -90,16 +85,12 @@ export class Viewer extends React.Component<IViewerProps> {
     private _annotationReactionDisposer?: IReactionDisposer;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _filterReactionDisposer?: IReactionDisposer;
-    private _activeReactionDisposer?: IReactionDisposer;
     private _viewer: React.RefObject<HTMLDivElement>;
     private _mainCont: React.RefObject<HTMLDivElement>;
     private _pdfViewer: any;
     // private _textContent: Pdfjs.TextContent[] = [];
     private _pdfFindController: any;
     private _searchString: string = "";
-    private _rendered: boolean = false;
-    private _pageIndex: number = -1;
-    private _matchIndex: number = 0;
 
     constructor(props: IViewerProps) {
         super(props);
@@ -135,23 +126,6 @@ export class Viewer extends React.Component<IViewerProps> {
             },
             { fireImmediately: true });
 
-        this._activeReactionDisposer = reaction(
-            () => this.props.parent.props.active(),
-            () => {
-                runInAction(() => {
-                    if (!this.props.parent.props.active()) {
-                        this._searching = false;
-                        this._pdfFindController = null;
-                        if (this._viewer.current) {
-                            let cns = this._viewer.current.childNodes;
-                            for (let i = cns.length - 1; i >= 0; i--) {
-                                cns.item(i).remove();
-                            }
-                        }
-                    }
-                });
-            }
-        );
 
         if (this.props.parent.props.ContainingCollectionView) {
             this._filterReactionDisposer = reaction(
@@ -346,7 +320,7 @@ export class Viewer extends React.Component<IViewerProps> {
             this._isPage[page] = "image";
             const address = this.props.url;
             try {
-                let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
+                let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`)));
                 runInAction(() => this._visibleElements[page] =
                     <img key={res.path} src={res.path} onError={handleError}
                         style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
@@ -476,7 +450,6 @@ export class Viewer extends React.Component<IViewerProps> {
                             phraseSearch: true,
                             query: searchString
                         });
-                    this._rendered = true;
                 });
                 container.addEventListener("pagerendered", () => {
                     console.log("rendered");
@@ -488,7 +461,6 @@ export class Viewer extends React.Component<IViewerProps> {
                             phraseSearch: true,
                             query: searchString
                         });
-                    this._rendered = true;
                 });
             }
         }
@@ -563,7 +535,6 @@ export class Viewer extends React.Component<IViewerProps> {
                     });
                     container.addEventListener("pagerendered", () => {
                         console.log("rendered");
-                        this._rendered = true;
                     });
                     this._pdfViewer.setDocument(this.props.pdf);
                     this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
@@ -703,17 +674,17 @@ class SimpleLinkService {
     externalLinkRel: any = null;
     pdf: any = null;
 
-    navigateTo(dest: any) { }
+    navigateTo() { }
 
-    getDestinationHash(dest: any) { return "#"; }
+    getDestinationHash() { return "#"; }
 
-    getAnchorUrl(hash: any) { return "#"; }
+    getAnchorUrl() { return "#"; }
 
-    setHash(hash: any) { }
+    setHash() { }
 
-    executeNamedAction(action: any) { }
+    executeNamedAction() { }
 
-    cachePageRef(pageNum: any, pageRef: any) { }
+    cachePageRef() { }
 
     get pagesCount() {
         return this.pdf ? this.pdf.numPages : 0;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 16c44225a..2214ac8af 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -9,12 +9,12 @@ import { Docs } from '../../documents/Documents';
 import { NumCast, Cast } from '../../../new_fields/Types';
 import { Doc } from '../../../new_fields/Doc';
 import { SearchItem } from './SearchItem';
-import { DocServer } from '../../DocServer';
 import * as rp from 'request-promise';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { SearchUtil } from '../../util/SearchUtil';
 import { RouteStore } from '../../../server/RouteStore';
 import { FilterBox } from './FilterBox';
+import { Utils } from '../../../Utils';
 
 
 @observer
@@ -74,7 +74,7 @@ export class SearchBox extends React.Component {
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = DocServer.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend(RouteStore.dataUriToImage);
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -154,7 +154,7 @@ export class SearchBox extends React.Component {
                         filteredDocs.forEach(doc => {
                             const index = this._resultsSet.get(doc);
                             const highlight = highlights[doc[Id]];
-                            const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []
+                            const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [];
                             if (index === undefined) {
                                 this._resultsSet.set(doc, this._results.length);
                                 this._results.push([doc, hlights]);
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index a8f94b746..33a615cbf 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,7 @@ import { listSpec } from '../new_fields/Schema';
 import { List } from '../new_fields/List';
 import { observer } from 'mobx-react';
 import { observable } from 'mobx';
+import { Utils } from '../Utils';
 
 
 
@@ -57,7 +58,7 @@ class Uploader extends React.Component {
 
                         this.status = "getting user document";
 
-                        const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+                        const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
                         if (!res) {
                             throw new Error("No user id returned");
                         }
@@ -104,6 +105,8 @@ class Uploader extends React.Component {
 }
 
 
+DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+
 ReactDOM.render((
     <Uploader />
 ),
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index e796ccb43..1c52a3f11 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,6 +12,7 @@ import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
 
 export class CurrentUserUtils {
     private static curr_email: string;
@@ -74,7 +75,7 @@ export class CurrentUserUtils {
     }
 
     public static loadCurrentUser() {
-        return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
+        return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
             if (response) {
                 const result: { id: string, email: string } = JSON.parse(response);
                 return result;
@@ -87,7 +88,7 @@ export class CurrentUserUtils {
     public static async loadUserDocument({ id, email }: { id: string, email: string }) {
         this.curr_id = id;
         this.curr_email = email;
-        await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
+        await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id) {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
diff --git a/src/server/index.ts b/src/server/index.ts
index b3859aa4f..6b4e59bfc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -417,7 +417,7 @@ app.get(RouteStore.reset, getReset);
 app.post(RouteStore.reset, postReset);
 
 app.use(RouteStore.corsProxy, (req, res) =>
-    req.pipe(request(req.url.substring(1))).pipe(res));
+    req.pipe(request(decodeURIComponent(req.url.substring(1)))).pipe(res));
 
 app.get(RouteStore.delete, (req, res) => {
     if (release) {
-- 
cgit v1.2.3-70-g09d2


From aa8a6f7a786410fe2f60cced3325eaee510adb09 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
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/client/views/nodes/DocumentView.tsx')

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


From fe188e93f2f8d61356bfdc3216273f72ae074e10 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tschicke@gmail.com>
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/client/views/nodes/DocumentView.tsx')

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" });
-- 
cgit v1.2.3-70-g09d2


From 33744cb0068d7dc470d0b39ca70344f256a9cad9 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tschicke@gmail.com>
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/client/views/nodes/DocumentView.tsx')

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<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.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200 }) });
+        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" });
-- 
cgit v1.2.3-70-g09d2


From 090d0b67dcc5148ecfbfac01cd9f1ddd3a52d8a7 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
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/client/views/nodes/DocumentView.tsx')

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/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<HTMLDivElement>();
+    _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 <CollectionSchemaPreview
+            Document={layoutDoc}
+            DataDocument={resolvedDataDoc}
+            showOverlays={this.overlays}
+            renderDepth={this.props.renderDepth}
+            width={width}
+            height={height}
+            getTransform={finalDxf}
+            CollectionView={this.props.CollectionView}
+            addDocument={this.props.addDocument}
+            moveDocument={this.props.moveDocument}
+            removeDocument={this.props.removeDocument}
+            active={this.props.active}
+            whenActiveChanged={this.props.whenActiveChanged}
+            addDocTab={this.props.addDocTab}
+            setPreviewScript={emptyFunction}
+            previewScript={undefined}>
+        </CollectionSchemaPreview>;
+    }
+    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 <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));
+                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)}
+                </div>;
+            }
+        });
+    }
+
+    _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 <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this.cursor, left: `${this.columnWidth + this.xMargin}px` }} >
+            <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
+    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 (
+            <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>
+        );
+    }
+}
\ 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<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;
@@ -664,10 +666,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) :
-- 
cgit v1.2.3-70-g09d2

