aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vscode/launch.json42
-rw-r--r--deploy/debug/repl.html14
-rw-r--r--deploy/debug/viewer.html1
-rw-r--r--package.json8
-rw-r--r--solr/conf/schema.xml2
-rw-r--r--src/Utils.ts15
-rw-r--r--src/client/DocServer.ts41
-rw-r--r--src/client/documents/Documents.ts74
-rw-r--r--src/client/goldenLayout.d.ts3
-rw-r--r--src/client/goldenLayout.js2
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts9
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx6
-rw-r--r--src/client/northstar/model/ModelHelpers.ts9
-rw-r--r--src/client/util/DocumentManager.ts78
-rw-r--r--src/client/util/DragManager.ts38
-rw-r--r--src/client/util/History.ts4
-rw-r--r--src/client/util/RichTextSchema.tsx16
-rw-r--r--src/client/util/Scripting.ts10
-rw-r--r--src/client/util/SearchUtil.ts5
-rw-r--r--src/client/util/SelectionManager.ts51
-rw-r--r--src/client/util/SerializationHelper.ts14
-rw-r--r--src/client/util/TooltipTextMenu.tsx8
-rw-r--r--src/client/util/type_decls.d161
-rw-r--r--src/client/views/ContextMenu.scss86
-rw-r--r--src/client/views/ContextMenu.tsx26
-rw-r--r--src/client/views/ContextMenuItem.tsx59
-rw-r--r--src/client/views/DocumentDecorations.tsx142
-rw-r--r--src/client/views/EditableView.tsx10
-rw-r--r--src/client/views/InkingCanvas.scss41
-rw-r--r--src/client/views/InkingCanvas.tsx16
-rw-r--r--src/client/views/InkingControl.tsx5
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/Main.scss20
-rw-r--r--src/client/views/Main.tsx279
-rw-r--r--src/client/views/MainOverlayTextBox.tsx38
-rw-r--r--src/client/views/MainView.tsx322
-rw-r--r--src/client/views/PresentationView.tsx31
-rw-r--r--src/client/views/PreviewCursor.tsx8
-rw-r--r--src/client/views/SearchBox.tsx43
-rw-r--r--src/client/views/SearchItem.tsx8
-rw-r--r--src/client/views/TemplateMenu.tsx17
-rw-r--r--src/client/views/Templates.tsx51
-rw-r--r--src/client/views/collections/CollectionBaseView.scss11
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx46
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx77
-rw-r--r--src/client/views/collections/CollectionPDFView.scss39
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx6
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss86
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx286
-rw-r--r--src/client/views/collections/CollectionStackingView.scss41
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx185
-rw-r--r--src/client/views/collections/CollectionSubView.tsx88
-rw-r--r--src/client/views/collections/CollectionTreeView.scss79
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx277
-rw-r--r--src/client/views/collections/CollectionVideoView.scss2
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx49
-rw-r--r--src/client/views/collections/CollectionView.tsx43
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss16
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx48
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx30
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx48
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx13
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx89
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx220
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx198
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx26
-rw-r--r--src/client/views/nodes/DocumentView.tsx278
-rw-r--r--src/client/views/nodes/FieldView.tsx34
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx197
-rw-r--r--src/client/views/nodes/IconBox.scss1
-rw-r--r--src/client/views/nodes/IconBox.tsx1
-rw-r--r--src/client/views/nodes/ImageBox.scss6
-rw-r--r--src/client/views/nodes/ImageBox.tsx145
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx55
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx34
-rw-r--r--src/client/views/nodes/LinkBox.tsx2
-rw-r--r--src/client/views/nodes/LinkMenu.tsx7
-rw-r--r--src/client/views/nodes/PDFBox.scss4
-rw-r--r--src/client/views/nodes/PDFBox.tsx133
-rw-r--r--src/client/views/nodes/VideoBox.tsx102
-rw-r--r--src/client/views/nodes/WebBox.tsx9
-rw-r--r--src/debug/Repl.tsx66
-rw-r--r--src/debug/Viewer.tsx353
-rw-r--r--src/fields/ScriptField.ts174
-rw-r--r--src/mobile/ImageUpload.scss25
-rw-r--r--src/mobile/ImageUpload.tsx123
-rw-r--r--src/new_fields/CursorField.ts16
-rw-r--r--src/new_fields/DateField.ts7
-rw-r--r--src/new_fields/Doc.ts135
-rw-r--r--src/new_fields/FieldSymbols.ts10
-rw-r--r--src/new_fields/HtmlField.ts7
-rw-r--r--src/new_fields/IconField.ts7
-rw-r--r--src/new_fields/InkField.ts7
-rw-r--r--src/new_fields/List.ts16
-rw-r--r--src/new_fields/ObjectField.ts9
-rw-r--r--src/new_fields/Proxy.ts9
-rw-r--r--src/new_fields/RefField.ts5
-rw-r--r--src/new_fields/RichTextField.ts7
-rw-r--r--src/new_fields/Schema.ts48
-rw-r--r--src/new_fields/Types.ts16
-rw-r--r--src/new_fields/URLField.ts16
-rw-r--r--src/new_fields/util.ts41
-rw-r--r--src/server/RouteStore.ts1
-rw-r--r--src/server/authentication/models/current_user_utils.ts73
-rw-r--r--src/server/database.ts124
-rw-r--r--src/server/downsize.ts40
-rw-r--r--src/server/index.ts86
-rw-r--r--src/server/public/files/.gitignore3
-rw-r--r--src/server/remapUrl.ts59
-rw-r--r--src/server/updateSearch.ts101
-rw-r--r--test/test.ts127
-rw-r--r--webpack.config.js1
114 files changed, 4247 insertions, 2307 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e92a4949a..3b6549767 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -14,6 +14,24 @@
"webRoot": "${workspaceFolder}",
},
{
+ "type": "firefox",
+ "request": "launch",
+ "name": "Launch Firefox against localhost",
+ "sourceMaps": "client",
+ "reAttach": true,
+ "url": "http://localhost:1050/login",
+ "webRoot": "${workspaceFolder}",
+ },
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against Dash server",
+ "sourceMaps": true,
+ "breakOnLoad": true,
+ "url": "http://dash-web.eastus2.cloudapp.azure.com:1050/login",
+ "webRoot": "${workspaceFolder}",
+ },
+ {
"type": "node",
"request": "attach",
"name": "Typescript Server",
@@ -25,6 +43,20 @@
{
"type": "node",
"request": "launch",
+ "name": "Current TS File",
+ "runtimeExecutable": "npx",
+ "runtimeArgs": [
+ "ts-node-dev",
+ "--nolazy",
+ "--inspect",
+ "--",
+ "${relativeFile}"
+ ],
+ "port": 9229
+ },
+ {
+ "type": "node",
+ "request": "launch",
"name": "Mocha Tests",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
@@ -56,15 +88,5 @@
"internalConsoleOptions": "openOnSessionStart",
"protocol": "inspector"
},
- {
- "type": "node",
- "request": "launch",
- "name": "Launch via NPM",
- "runtimeExecutable": "npm",
- "runtimeArgs": [
- "start"
- ],
- "port": 9229
- }
]
} \ No newline at end of file
diff --git a/deploy/debug/repl.html b/deploy/debug/repl.html
new file mode 100644
index 000000000..8ab07ec49
--- /dev/null
+++ b/deploy/debug/repl.html
@@ -0,0 +1,14 @@
+<html>
+
+<head>
+ <title>Debug REPL</title>
+ <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
+</head>
+
+<body>
+ <div id="root"></div>
+ <script src="../repl.js"></script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/deploy/debug/viewer.html b/deploy/debug/viewer.html
index 3785a6602..8c265ccb8 100644
--- a/deploy/debug/viewer.html
+++ b/deploy/debug/viewer.html
@@ -3,6 +3,7 @@
<head>
<title>Document Debugger</title>
<link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
</head>
<body>
diff --git a/package.json b/package.json
index 147f59c25..438572b57 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"css-loader": "^2.1.1",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^1.0.2",
+ "jsdom": "^15.1.1",
"mocha": "^5.2.0",
"sass-loader": "^7.1.0",
"scss-loader": "0.0.1",
@@ -88,6 +89,7 @@
"@types/react-table": "^6.7.22",
"@types/request": "^2.48.1",
"@types/request-promise": "^4.1.42",
+ "@types/sharp": "^0.22.2",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/typescript": "^2.0.0",
@@ -117,6 +119,8 @@
"golden-layout": "^1.5.9",
"html-to-image": "^0.1.0",
"i": "^0.3.6",
+ "image-data-uri": "^2.0.0",
+ "imagesloaded": "^4.1.4",
"jsonwebtoken": "^8.5.0",
"jsx-to-string": "^1.4.0",
"lodash": "^4.17.11",
@@ -124,6 +128,7 @@
"mobx": "^5.9.0",
"mobx-react": "^5.3.5",
"mobx-react-devtools": "^6.1.1",
+ "mobx-utils": "^5.4.0",
"mongodb": "^3.1.13",
"mongoose": "^5.4.18",
"node-sass": "^4.12.0",
@@ -131,6 +136,7 @@
"nodemon": "^1.18.10",
"normalize.css": "^8.0.1",
"npm": "^6.9.0",
+ "p-limit": "^2.2.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"prosemirror-commands": "^1.0.7",
@@ -163,8 +169,10 @@
"react-split-pane": "^0.1.85",
"react-table": "^6.9.2",
"request": "^2.88.0",
+ "request-image-size": "^2.1.0",
"request-promise": "^4.2.4",
"serializr": "^1.5.1",
+ "sharp": "^0.22.1",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"solr-node": "^1.1.3",
diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml
index 9217e015b..30e8daa65 100644
--- a/solr/conf/schema.xml
+++ b/solr/conf/schema.xml
@@ -8,7 +8,7 @@
<filter class="solr.StopFilterFactory" words="stopwords.txt"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.PorterStemFilterFactory"/>
- <filter class="solr.EdgeNGramFilterFactory" minGramSize="3" maxGramSize="12"/>
+ <filter class="solr.NGramFilterFactory" minGramSize="3" maxGramSize="12"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.StandardTokenizerFactory"/>
diff --git a/src/Utils.ts b/src/Utils.ts
index 24878a368..e8a80bdc3 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -15,7 +15,7 @@ export class Utils {
return v5(seed, v5.URL);
}
- public static GetScreenTransform(ele: HTMLElement): { scale: number, translateX: number, translateY: number } {
+ public static GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
}
@@ -39,6 +39,19 @@ export class Utils {
document.body.removeChild(textArea);
}
+ public static GetClipboardText(): string {
+ var textArea = document.createElement("textarea");
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try { document.execCommand('paste'); } catch (err) { }
+
+ const val = textArea.value;
+ document.body.removeChild(textArea);
+ return val;
+ }
+
public static loggingEnabled: Boolean = false;
public static logFilter: number | undefined = undefined;
private static log(prefix: string, messageName: string, message: any, receiving: boolean) {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index a288d394a..cbcf751ee 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,15 +1,22 @@
import * as OpenSocket from 'socket.io-client';
import { MessageStore } from "./../server/Message";
import { Opt } from '../new_fields/Doc';
-import { Utils } from '../Utils';
+import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
-import { RefField, HandleUpdate, Id } from '../new_fields/RefField';
+import { RefField } from '../new_fields/RefField';
+import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
export namespace DocServer {
const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
const GUID: string = Utils.GenerateGuid();
+ export function makeReadOnly() {
+ _CreateField = emptyFunction;
+ _UpdateField = emptyFunction;
+ _respondToUpdate = emptyFunction;
+ }
+
export function prepend(extension: string): string {
return window.location.origin + extension;
}
@@ -21,12 +28,10 @@ export namespace DocServer {
export async function GetRefField(id: string): Promise<Opt<RefField>> {
let cached = _cache[id];
if (cached === undefined) {
- const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => {
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => {
const field = SerializationHelper.Deserialize(fieldJson);
- if (_cache[id] !== undefined && !(_cache[id] instanceof Promise)) {
- id;
- }
if (field !== undefined) {
+ await field.proto;
_cache[id] = field;
} else {
delete _cache[id];
@@ -65,6 +70,7 @@ export namespace DocServer {
fieldMap[field.id] = SerializationHelper.Deserialize(field);
}
}
+
return fieldMap;
});
requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
@@ -78,26 +84,40 @@ export namespace DocServer {
}
map[id] = field;
});
+ await Promise.all(requestedIds.map(async id => {
+ const field = fields[id];
+ if (field) {
+ await (field as any).proto;
+ }
+ }));
const otherFields = await Promise.all(promises);
waitingIds.forEach((id, index) => map[id] = otherFields[index]);
return map;
}
- export function UpdateField(id: string, diff: any) {
+ let _UpdateField = (id: string, diff: any) => {
if (id === updatingId) {
return;
}
Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ };
+
+ export function UpdateField(id: string, diff: any) {
+ _UpdateField(id, diff);
}
- export function CreateField(field: RefField) {
+ let _CreateField = (field: RefField) => {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ };
+
+ export function CreateField(field: RefField) {
+ _CreateField(field);
}
let updatingId: string | undefined;
- function respondToUpdate(diff: any) {
+ let _respondToUpdate = (diff: any) => {
const id = diff.id;
if (id === undefined) {
return;
@@ -119,6 +139,9 @@ export namespace DocServer {
} else {
update(field);
}
+ };
+ function respondToUpdate(diff: any) {
+ _respondToUpdate(diff);
}
function connected() {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ed260d42e..2ae127e21 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -26,14 +26,17 @@ import { OmitKeys } from "../../Utils";
import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
-import { Cast } from "../../new_fields/Types";
+import { Cast, NumCast } from "../../new_fields/Types";
import { IconField } from "../../new_fields/IconField";
import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
-import { StrokeData, InkField } from "../../new_fields/InkField";
+import { InkField } from "../../new_fields/InkField";
import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
-import { schema } from "prosemirror-schema-basic";
+import { UndoManager } from "../util/UndoManager";
+import { RouteStore } from "../../server/RouteStore";
+var requestImageSize = require('request-image-size');
+var path = require('path');
export interface DocumentOptions {
x?: number;
@@ -60,10 +63,41 @@ export interface DocumentOptions {
borderRounding?: number;
schemaColumns?: List<string>;
dockingConfig?: string;
+ dbDoc?: Doc;
// [key: string]: Opt<Field>;
}
const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+export namespace DocUtils {
+ export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) {
+ let protoSrc = source.proto ? source.proto : source;
+ let protoTarg = target.proto ? target.proto : target;
+ UndoManager.RunInBatch(() => {
+ let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+ let linkDocProto = Doc.GetProto(linkDoc);
+ linkDocProto.title = source.title + " to " + target.title;
+ linkDocProto.linkDescription = "";
+ linkDocProto.linkTags = "Default";
+
+ linkDocProto.linkedTo = target;
+ linkDocProto.linkedFrom = source;
+ linkDocProto.linkedToPage = target.curPage;
+ linkDocProto.linkedFromPage = source.curPage;
+ linkDocProto.linkedToContext = targetContext;
+
+ let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
+ let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
+ !linkedFrom && (protoTarg.linkedFromDocs = linkedFrom = new List<Doc>());
+ !linkedTo && (protoSrc.linkedToDocs = linkedTo = new List<Doc>());
+ linkedFrom.push(linkDoc);
+ linkedTo.push(linkDoc);
+ return linkDoc;
+ }, "make link");
+ }
+
+
+}
+
export namespace Docs {
let textProto: Doc;
let histoProto: Doc;
@@ -109,8 +143,8 @@ export namespace Docs {
deleg.data = value;
return Doc.assign(deleg, options);
}
- function SetDelegateOptions<U extends Field>(doc: Doc, options: DocumentOptions) {
- const deleg = Doc.MakeDelegate(doc);
+ function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) {
+ const deleg = Doc.MakeDelegate(doc, id);
return Doc.assign(deleg, options);
}
@@ -167,7 +201,7 @@ export namespace Docs {
return audioProto;
}
- function CreateInstance(proto: Doc, data: Field, options: DocumentOptions) {
+ function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
protoProps.author = CurrentUserUtils.email;
@@ -177,11 +211,22 @@ export namespace Docs {
}
protoProps.isPrototype = true;
- return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps);
+ return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId);
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- return CreateInstance(imageProto, new ImageField(new URL(url)), options);
+ let inst = CreateInstance(imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options });
+ requestImageSize(window.origin + RouteStore.corsProxy + "/" + url)
+ .then((size: any) => {
+ let aspect = size.height / size.width;
+ if (!inst.proto!.nativeWidth) {
+ inst.proto!.nativeWidth = size.width;
+ }
+ inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
+ inst.height = NumCast(inst.width) * aspect;
+ })
+ .catch((err: any) => console.log(err));
+ return inst;
// let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
// [new URL(url), ImageField]);
// doc.SetText(KeyStore.Caption, "my caption...");
@@ -209,7 +254,7 @@ export namespace Docs {
return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
}
- export async function DBDocument(url: string, options: DocumentOptions = {}) {
+ export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
let schemaName = options.title ? options.title : "-no schema-";
let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
if (ctlog && ctlog.schemas) {
@@ -231,7 +276,7 @@ export namespace Docs {
new AttributeTransformationModel(atmod, AggregateFunction.None),
new AttributeTransformationModel(atmod, AggregateFunction.Count),
new AttributeTransformationModel(atmod, AggregateFunction.Count));
- docs.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ docs.push(Docs.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
}
}));
});
@@ -246,7 +291,7 @@ export namespace Docs {
return CreateInstance(webProto, new HtmlField(html), options);
}
export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return CreateInstance(kvpProto, document, options);
+ return CreateInstance(kvpProto, document, { title: document.title + ".kvp", ...options });
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
if (!makePrototype) {
@@ -260,8 +305,11 @@ export namespace Docs {
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
}
- export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions) {
- return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config });
+ export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+ }
+ export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
+ return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
export function CaptionDocument(doc: Doc) {
diff --git a/src/client/goldenLayout.d.ts b/src/client/goldenLayout.d.ts
new file mode 100644
index 000000000..b50240563
--- /dev/null
+++ b/src/client/goldenLayout.d.ts
@@ -0,0 +1,3 @@
+
+declare const GoldenLayout: any;
+export = GoldenLayout; \ No newline at end of file
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index ab2bcefed..54c9c6068 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -4466,7 +4466,7 @@
}
if (this.contentItems.length > 0) {
- initialItem = this.contentItems[this.config.activeItemIndex || 0];
+ initialItem = this.contentItems[Math.min(this.contentItems.length - 1, this.config.activeItemIndex || 0)];
if (!initialItem) {
throw new Error('Configured activeItemIndex out of bounds');
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index aabc77bb2..e6f32272e 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -3,10 +3,11 @@ import { custom, serializable } from "serializr";
import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel";
import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation";
-import { ObjectField, Copy } from "../../../new_fields/ObjectField";
+import { ObjectField } from "../../../new_fields/ObjectField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { OmitKeys } from "../../../Utils";
import { Deserializable } from "../../util/SerializationHelper";
+import { Copy, ToScriptString } from "../../../new_fields/FieldSymbols";
function serialize(field: HistogramField) {
let obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
@@ -52,7 +53,11 @@ export class HistogramField extends ObjectField {
[Copy]() {
let y = this.HistoOp;
- let z = this.HistoOp["Copy"];
+ let z = this.HistoOp.Copy;
return new HistogramField(HistogramOperation.Duplicate(this.HistoOp));
}
+
+ [ToScriptString]() {
+ return this.toString();
+ }
} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index eb1ad69b7..d7732ee86 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -19,7 +19,7 @@ import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
import { StyleConstants } from "../utils/StyleContants";
import { Cast } from "../../../new_fields/Types";
import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
@observer
@@ -125,9 +125,11 @@ export class HistogramBox extends React.Component<FieldViewProps> {
let mapped = brushingDocs.map((brush, i) => {
brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
let brushed = DocListCast(brush.brushingDocs);
+ if (!brushed.length)
+ return null;
return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
});
- this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped);
+ runInAction(() => this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped.filter(m => m) as { l: Doc, b: Doc }[]));
}
}, { fireImmediately: true });
reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts
index 80bb71224..88e6e72b8 100644
--- a/src/client/northstar/model/ModelHelpers.ts
+++ b/src/client/northstar/model/ModelHelpers.ts
@@ -32,12 +32,9 @@ export class ModelHelpers {
public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number {
return Array.from(histogramResult.aggregateParameters!).findIndex((value, i, set) => {
- if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters)
- return true;
- if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters)
- return true;
- if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters)
- return true;
+ if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters) return true;
+ if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters) return true;
+ if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters) return true;
return false;
});
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 51f5fbe9f..ff0c1560b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,11 +1,14 @@
import { computed, observable } from 'mobx';
import { DocumentView } from '../views/nodes/DocumentView';
-import { Doc, DocListCast } from '../../new_fields/Doc';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
import { undoBatch } from './UndoManager';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { Id } from '../../new_fields/RefField';
+import { CollectionView } from '../views/collections/CollectionView';
+import { CollectionPDFView } from '../views/collections/CollectionPDFView';
+import { CollectionVideoView } from '../views/collections/CollectionVideoView';
+import { Id } from '../../new_fields/FieldSymbols';
export class DocumentManager {
@@ -27,31 +30,33 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentViewById(id: string): DocumentView | null {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
let toReturn: DocumentView | null = null;
+ let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
- //gets document view that is in a freeform canvas collection
- DocumentManager.Instance.DocumentViews.map(view => {
- if (view.props.Document[Id] === id) {
- toReturn = view;
- return;
- }
- });
- if (!toReturn) {
+ for (let i = 0; i < passes.length; i++) {
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document.proto;
- if (doc && doc[Id] === id) {
+ if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
toReturn = view;
+ return;
}
});
+ if (!toReturn) {
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document.proto;
+ if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) {
+ toReturn = view;
+ }
+ });
+ }
}
return toReturn;
}
- public getDocumentView(toFind: Doc): DocumentView | null {
- return this.getDocumentViewById(toFind[Id]);
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ return this.getDocumentViewById(toFind[Id], preferredCollection);
}
public getDocumentViews(toFind: Doc): DocumentView[] {
@@ -110,26 +115,51 @@ export class DocumentManager {
}
@undoBatch
- public jumpToDocument = async (doc: Doc): Promise<void> => {
- const page = NumCast(doc.page, undefined);
+ public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
+ let doc = Doc.GetProto(docDelegate);
const contextDoc = await Cast(doc.annotationOn, Doc);
if (contextDoc) {
+ const page = NumCast(doc.page, linkPage || 0);
const curPage = NumCast(contextDoc.curPage, page);
if (page !== curPage) contextDoc.curPage = page;
}
- let docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
+
+ let docView: DocumentView | null;
+ // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
+ if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
+ docView.props.Document.libraryBrush = true;
+ if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
docView.props.focus(docView.props.Document);
} else {
if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(doc));
+ if (docContext) {
+ let targetContextView: DocumentView | null;
+ if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
+ docContext.panTransformType = "Ease";
+ targetContextView.props.focus(docDelegate);
+ } else {
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext);
+ setTimeout(() => {
+ this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
+ }, 10);
+ }
+ } else {
+ const actualDoc = Doc.MakeAlias(docDelegate);
+ actualDoc.libraryBrush = true;
+ if (linkPage !== undefined) actualDoc.curPage = linkPage;
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+ }
} else {
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
+ let contextView: DocumentView | null;
+ docDelegate.libraryBrush = true;
+ if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc);
+ contextView.props.focus(docDelegate);
} else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
+ (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+ setTimeout(() => {
+ this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
+ }, 10);
}
}
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 29f0bc557..ab00fabe1 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,13 +1,14 @@
-import { action, runInAction } from "mobx";
+import { action, runInAction, observable } from "mobx";
import { Doc, DocListCastAsync } from "../../new_fields/Doc";
import { Cast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import * as globalCssVariables from "../views/globalCssVariables.scss";
import { URLField } from "../../new_fields/URLField";
+import { SelectionManager } from "./SelectionManager";
export type dropActionType = "alias" | "copy" | undefined;
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) {
let onRowMove = async (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
@@ -17,6 +18,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
var dragData = new DragManager.DocumentDragData([await docFunc()]);
dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
+ dragData.options = options;
+ dragData.dontHideOnDrop = dontHideOnDrop;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
};
let onRowUp = (): void => {
@@ -27,7 +30,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
// if (this.props.isSelected() || this.props.isTopMost) {
if (e.button === 0) {
e.stopPropagation();
- if (e.shiftKey) {
+ if (e.shiftKey && CollectionDockingView.Instance) {
CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e);
} else {
document.addEventListener("pointermove", onRowMove);
@@ -42,7 +45,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
- let draggedFromDocs: Doc[] = []
+ let draggedFromDocs: Doc[] = [];
if (srcTarg) {
let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
@@ -182,10 +185,10 @@ export namespace DragManager {
export class EmbedDragData {
constructor(embeddableSourceDoc: Doc) {
this.embeddableSourceDoc = embeddableSourceDoc;
- this.urlField = Cast(embeddableSourceDoc.data, URLField)!;
+ this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
}
embeddableSourceDoc: Doc;
- urlField: URLField;
+ urlField?: URLField;
[id: string]: any;
}
@@ -200,13 +203,14 @@ export namespace DragManager {
export let AbortDrag: () => void = emptyFunction;
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+ eles = eles.filter(e => e);
if (!dragDiv) {
dragDiv = document.createElement("div");
dragDiv.className = "dragManager-dragDiv";
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
-
+ SelectionManager.SetIsDragging(true);
let scaleXs: number[] = [];
let scaleYs: number[] = [];
let xs: number[] = [];
@@ -256,6 +260,13 @@ export namespace DragManager {
// pdfBox.replaceChild(img, pdfBox.children[0])
// }
// }
+ let set = dragElement.getElementsByTagName('*');
+ for (let i = 0; i < set.length; i++)
+ if (set[i].hasAttribute("style")) {
+ let s = set[i];
+ (s as any).style.pointerEvents = "none";
+ }
+
dragDiv.appendChild(dragElement);
return dragElement;
@@ -274,12 +285,11 @@ export namespace DragManager {
let lastX = downX;
let lastY = downY;
const moveHandler = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
+ e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
- if (e.shiftKey) {
+ if (e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
CollectionDockingView.Instance.StartOtherDrag(docs, {
pageX: e.pageX,
@@ -294,12 +304,12 @@ export namespace DragManager {
lastX = e.pageX;
lastY = e.pageY;
dragElements.map((dragElement, i) => (dragElement.style.transform =
- `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px)
- scale(${scaleXs[i]}, ${scaleYs[i]})`)
+ `translate(${(xs[i] += moveX)}px, ${(ys[i] += moveY)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
};
let hideDragElements = () => {
+ SelectionManager.SetIsDragging(false);
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => (ele.hidden = false));
};
@@ -325,7 +335,7 @@ export namespace DragManager {
}
function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
- let removed = dragEles.map(dragEle => {
+ let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
// let parent = dragEle.parentElement;
// if (parent) parent.removeChild(dragEle);
let ret = [dragEle, dragEle.style.width, dragEle.style.height];
@@ -351,7 +361,7 @@ export namespace DragManager {
x: e.x,
y: e.y,
data: dragData,
- mods: e.ctrlKey ? "Control" : ""
+ mods: e.altKey ? "AltKey" : ""
}
})
);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 92d2b2b44..545ea8629 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,7 +1,7 @@
import { Doc, Opt, Field } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
-import { Main } from "../views/Main";
import { RouteStore } from "../../server/RouteStore";
+import { MainView } from "../views/MainView";
export namespace HistoryUtil {
export interface DocInitializerList {
@@ -114,7 +114,7 @@ export namespace HistoryUtil {
const field = await DocServer.GetRefField(url.docId);
await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
if (field instanceof Doc) {
- Main.Instance.openWorkspace(field, true);
+ MainView.Instance.openWorkspace(field, true);
}
}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 742ac5434..2b82a09a2 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -97,13 +97,13 @@ export const nodes: { [index: string]: NodeSpec } = {
title: dom.getAttribute("title"),
alt: dom.getAttribute("alt"),
width: Math.min(100, Number(dom.getAttribute("width"))),
- }
+ };
}
}],
// TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
toDOM(node) {
- const attrs = { style: `width: ${node.attrs.width}` }
- return ["img", { ...node.attrs, ...attrs }]
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
}
},
@@ -401,7 +401,7 @@ export class ImageResizeView {
const currentX = e.pageX;
const diffInPx = currentX - startX;
self._outer.style.width = `${startWidth + diffInPx}`;
- }
+ };
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
@@ -410,11 +410,11 @@ export class ImageResizeView {
view.state.tr.setNodeMarkup(getPos(), null,
{ src: node.attrs.src, width: self._outer.style.width })
.setSelection(view.state.selection));
- }
+ };
- document.addEventListener("pointermove", onpointermove)
- document.addEventListener("pointerup", onpointerup)
- }
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
this._outer.appendChild(this._handle);
this._outer.appendChild(this._img);
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index e45f61c11..40e2ad6bb 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -12,6 +12,7 @@ import { Doc, Field } from '../../new_fields/Doc';
import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
import { List } from '../../new_fields/List';
import { RichTextField } from '../../new_fields/RichTextField';
+import { ScriptField, ComputedField } from '../../fields/ScriptField';
export interface ScriptSucccess {
success: true;
@@ -41,11 +42,11 @@ export type CompileResult = CompiledScript | CompileError;
function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult {
const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error);
- if (errors || !script) {
+ if ((options.typecheck !== false && errors) || !script) {
return { compiled: false, errors: diagnostics };
}
- let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField];
+ let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
@@ -131,10 +132,11 @@ export interface ScriptOptions {
addReturn?: boolean;
params?: { [name: string]: string };
capturedVariables?: { [name: string]: Field };
+ typecheck?: boolean;
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
- const { requiredType = "", addReturn = false, params = {}, capturedVariables = {} } = options;
+ const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
let host = new ScriptingCompilerHost;
let paramNames: string[] = [];
if ("this" in params || "this" in capturedVariables) {
@@ -158,7 +160,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
${addReturn ? `return ${script};` : script}
})`;
host.writeFile("file.ts", funcScript);
- host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+ if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
let program = ts.createProgram(["file.ts"], {}, host);
let testResult = program.emit();
let outputText = host.readFile("file.js");
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 4ccff0d1b..28ec8ca14 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -1,7 +1,7 @@
import * as rp from 'request-promise';
import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
-import { Id } from '../../new_fields/RefField';
+import { Id } from '../../new_fields/FieldSymbols';
export namespace SearchUtil {
export function Search(query: string, returnDocs: true): Promise<Doc[]>;
@@ -20,6 +20,7 @@ export namespace SearchUtil {
export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> {
const proto = await Doc.GetT(doc, "proto", Doc, true);
const protoId = (proto || doc)[Id];
- return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
+ return Search(`proto_i:"${protoId}"`, true);
+ // return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 8c92c2023..b26032b04 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,4 +1,4 @@
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
@@ -6,44 +6,44 @@ import { NumCast } from "../../new_fields/Types";
export namespace SelectionManager {
class Manager {
- @observable
- SelectedDocuments: Array<DocumentView> = [];
+ @observable IsDragging: boolean = false;
+ @observable SelectedDocuments: Array<DocumentView> = [];
@action
- SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+ SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
if (!ctrlPressed) {
this.DeselectAll();
}
- if (manager.SelectedDocuments.indexOf(doc) === -1) {
- manager.SelectedDocuments.push(doc);
- doc.props.whenActiveChanged(true);
+ if (manager.SelectedDocuments.indexOf(docView) === -1) {
+ manager.SelectedDocuments.push(docView);
+ docView.props.whenActiveChanged(true);
+ }
+ }
+ @action
+ DeselectDoc(docView: DocumentView): void {
+ let ind = manager.SelectedDocuments.indexOf(docView);
+ if (ind !== -1) {
+ manager.SelectedDocuments.splice(ind, 1);
+ docView.props.whenActiveChanged(false);
}
}
-
@action
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
FormattedTextBox.InputBoxOverlay = undefined;
}
- @action
- ReselectAll() {
- let sdocs = manager.SelectedDocuments.map(d => d);
- manager.SelectedDocuments = [];
- return sdocs;
- }
- @action
- ReselectAll2(sdocs: DocumentView[]) {
- sdocs.map(s => SelectionManager.SelectDoc(s, true));
- }
}
const manager = new Manager();
- export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
- manager.SelectDoc(doc, ctrlPressed);
+ export function DeselectDoc(docView: DocumentView): void {
+ manager.DeselectDoc(docView);
+ }
+ export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
+ manager.SelectDoc(docView, ctrlPressed);
}
export function IsSelected(doc: DocumentView): boolean {
@@ -62,14 +62,13 @@ export namespace SelectionManager {
if (found) manager.SelectDoc(found, false);
}
- export function ReselectAll() {
- let sdocs = manager.ReselectAll();
- setTimeout(() => manager.ReselectAll2(sdocs), 0);
- }
+ export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
+ export function GetIsDragging() { return manager.IsDragging; }
+
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
- export function ViewsSortedVertically(): DocumentView[] {
+ export function ViewsSortedHorizontally(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
@@ -77,7 +76,7 @@ export namespace SelectionManager {
});
return sorted;
}
- export function ViewsSortedHorizontally(): DocumentView[] {
+ export function ViewsSortedVertically(): DocumentView[] {
let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 7ded85e43..a7246d7c4 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -45,13 +45,17 @@ export namespace SerializationHelper {
throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
}
- const value = deserialize(serializationTypes[obj.__type], obj);
+ const type = serializationTypes[obj.__type];
+ const value = deserialize(type.ctor, obj);
+ if (type.afterDeserialize) {
+ type.afterDeserialize(value);
+ }
serializing -= 1;
return value;
}
}
-let serializationTypes: { [name: string]: any } = {};
+let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void } } = {};
let reverseMap: { [ctor: string]: string } = {};
export interface DeserializableOpts {
@@ -59,9 +63,9 @@ export interface DeserializableOpts {
withFields(fields: string[]): Function;
}
-export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(name: string, afterDeserialize?: (obj: any) => void): DeserializableOpts;
export function Deserializable(constructor: { new(...args: any[]): any }): void;
-export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
function addToMap(name: string, ctor: { new(...args: any[]): any }) {
const schema = getDefaultModelSchema(ctor) as any;
if (schema.targetClass !== ctor) {
@@ -69,7 +73,7 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin
setDefaultModelSchema(ctor, newSchema);
}
if (!(name in serializationTypes)) {
- serializationTypes[name] = ctor;
+ serializationTypes[name] = { ctor, afterDeserialize };
reverseMap[ctor.name] = name;
} else {
throw new Error(`Name ${name} has already been registered as deserializable`);
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index b860f6758..ab6cd246a 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -23,12 +23,12 @@ const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
import { View } from "@react-pdf/renderer";
import { DragManager } from "./DragManager";
import { Doc, Opt, Field } from "../../new_fields/Doc";
-import { Id } from "../../new_fields/RefField";
import { Utils } from "../northstar/utils/Utils";
import { DocServer } from "../DocServer";
import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DocumentManager } from "./DocumentManager";
+import { Id } from "../../new_fields/FieldSymbols";
const SVG = "http://www.w3.org/2000/svg";
@@ -196,7 +196,6 @@ export class TooltipTextMenu {
let node = this.view.state.selection.$from.nodeAfter;
let link = node && node.marks.find(m => m.type.name === "link");
if (link) {
- console.log("Link to : " + link.attrs.href);
let href: string = link.attrs.href;
if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
let docid = href.replace(DocServer.prepend("/doc/"), "");
@@ -205,10 +204,11 @@ export class TooltipTextMenu {
if (DocumentManager.Instance.getDocumentView(f)) {
DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
}
- else CollectionDockingView.Instance.AddRightSplit(f);
+ else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f);
}
}));
}
+ // TODO This should have an else to handle external links
e.stopPropagation();
e.preventDefault();
}
@@ -227,7 +227,7 @@ export class TooltipTextMenu {
{
handlers: {
dragComplete: action(() => {
- let m = dragData.droppedDocuments as Doc[];
+ let m = dragData.droppedDocuments;
this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
}),
},
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 47c3481b2..2cbe1dd40 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -119,107 +119,88 @@ interface URL {
username: string;
toJSON(): string;
}
-
-declare type FieldId = string;
-
-declare abstract class Field {
- Id: FieldId;
- abstract ToScriptString(): string;
- abstract TrySetValue(value: any): boolean;
- abstract GetValue(): any;
- abstract Copy(): Field;
+interface PromiseLike<T> {
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): PromiseLike<TResult1 | TResult2>;
}
-
-declare abstract class BasicField<T> extends Field {
- constructor(data: T);
- Data: T;
- TrySetValue(value: any): boolean;
- GetValue(): any;
+interface Promise<T> {
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
+ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
-declare class TextField extends BasicField<string>{
- constructor();
- constructor(data: string);
- ToScriptString(): string;
- Copy(): Field;
-}
-declare class ImageField extends BasicField<URL>{
+declare const Update: unique symbol;
+declare const Self: unique symbol;
+declare const SelfProxy: unique symbol;
+declare const HandleUpdate: unique symbol;
+declare const Id: unique symbol;
+declare const OnUpdate: unique symbol;
+declare const Parent: unique symbol;
+declare const Copy: unique symbol;
+declare const ToScriptString: unique symbol;
+
+declare abstract class RefField {
+ readonly [Id]: FieldId;
+
constructor();
- constructor(data: URL);
- ToScriptString(): string;
- Copy(): Field;
}
-declare class HtmlField extends BasicField<string>{
- constructor();
- constructor(data: string);
- ToScriptString(): string;
- Copy(): Field;
+
+declare type FieldId = string;
+
+declare abstract class ObjectField {
+ abstract [Copy](): ObjectField;
}
-declare class NumberField extends BasicField<number>{
- constructor();
- constructor(data: number);
- ToScriptString(): string;
- Copy(): Field;
+
+declare abstract class URLField extends ObjectField {
+ readonly url: URL;
+
+ constructor(url: string);
+ constructor(url: URL);
}
-declare class WebField extends BasicField<URL>{
+
+declare class AudioField extends URLField { [Copy](): ObjectField; }
+declare class VideoField extends URLField { [Copy](): ObjectField; }
+declare class ImageField extends URLField { [Copy](): ObjectField; }
+declare class WebField extends URLField { [Copy](): ObjectField; }
+declare class PdfField extends URLField { [Copy](): ObjectField; }
+
+declare const ComputedField: any;
+declare const CompileScript: any;
+
+// @ts-ignore
+declare type Extract<T, U> = T extends U ? T : never;
+declare type Field = number | string | boolean | ObjectField | RefField;
+declare type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
+declare type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
+
+declare type Opt<T> = T | undefined;
+declare class Doc extends RefField {
constructor();
- constructor(data: URL);
- ToScriptString(): string;
- Copy(): Field;
+
+ [key: string]: FieldResult;
+ // [ToScriptString](): string;
}
-declare class ListField<T> extends BasicField<T[]>{
- constructor();
- constructor(data: T[]);
- ToScriptString(): string;
- Copy(): Field;
-}
-declare class Key extends Field {
- constructor(name:string);
- Name: string;
- TrySetValue(value: any): boolean;
- GetValue(): any;
- Copy(): Field;
- ToScriptString(): string;
-}
-declare type FIELD_WAITING = null;
-declare type Opt<T> = T | undefined;
-declare type FieldValue<T> = Opt<T> | FIELD_WAITING;
-// @ts-ignore
-declare class Document extends Field {
- TrySetValue(value: any): boolean;
- GetValue(): any;
- Copy(): Field;
- ToScriptString(): string;
-
- Width(): number;
- Height(): number;
- Scale(): number;
- Title: string;
-
- Get(key: Key): FieldValue<Field>;
- GetAsync(key: Key, callback: (field: Field) => void): boolean;
- GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void;
- GetT<T extends Field>(key: Key, ctor: { new(): T }): FieldValue<T>;
- GetOrCreate<T extends Field>(key: Key, ctor: { new(): T }): T;
- GetData<T, U extends Field & { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T;
- GetHtml(key: Key, defaultVal: string): string;
- GetNumber(key: Key, defaultVal: number): number;
- GetText(key: Key, defaultVal: string): string;
- GetList<T extends Field>(key: Key, defaultVal: T[]): T[];
- Set(key: Key, field: Field | undefined): void;
- SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }): void;
- SetText(key: Key, value: string): void;
- SetNumber(key: Key, value: number): void;
- GetPrototype(): FieldValue<Document>;
- GetAllPrototypes(): Document[];
- MakeDelegate(): Document;
-}
-
-declare const KeyStore: {
- [name: string]: Key;
+
+declare class ListImpl<T extends Field> extends ObjectField {
+ constructor(fields?: T[]);
+ [index: number]: T | (T extends RefField ? Promise<T> : never);
+ [Copy](): ObjectField;
}
// @ts-ignore
declare const console: any;
-declare const Documents: any;
+interface DocumentOptions { }
+
+declare const Docs: {
+ ImageDocument(url: string, options?: DocumentOptions): Doc;
+ VideoDocument(url: string, options?: DocumentOptions): Doc;
+ // HistogramDocument(url:string, options?:DocumentOptions);
+ TextDocument(options?: DocumentOptions): Doc;
+ PdfDocument(url: string, options?: DocumentOptions): Doc;
+ WebDocument(url: string, options?: DocumentOptions): Doc;
+ HtmlDocument(html: string, options?: DocumentOptions): Doc;
+ KVPDocument(document: Doc, options?: DocumentOptions): Doc;
+ FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc;
+ SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc;
+ TreeDocument(documents: Doc[], options?: DocumentOptions): Doc;
+ StackingDocument(documents: Doc[], options?: DocumentOptions): Doc;
+};
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index fe884ca85..7e066d53a 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -1,55 +1,73 @@
@import "globalCssVariables";
+
.contextMenu-cont {
- position: absolute;
- display: flex;
- z-index: $contextMenu-zindex;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
- flex-direction: column;
+ position: absolute;
+ display: flex;
+ z-index: $contextMenu-zindex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
}
.contextMenu-item:first-child {
- background: $intermediate-color;
- color: $light-color;
+ background: $intermediate-color;
+ color: $light-color;
}
.contextMenu-item:first-child::placeholder {
- color: $light-color;
+ color: $light-color;
}
.contextMenu-item:first-child:hover {
- background: $intermediate-color;
- color: $light-color;
+ background: $intermediate-color;
+ color: $light-color;
+}
+
+.contextMenu-subMenu-cont {
+ position: absolute;
+ display: flex;
+ z-index: 1000;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ flex-direction: column;
}
.contextMenu-item {
- width: auto;
- height: auto;
- background: $light-color-secondary;
- display: flex;
- justify-content: left;
- align-items: center;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- transition: all 0.1s;
- border-width: 0.11px;
- border-style: none;
- border-color: $intermediate-color;
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- font-size: 13px;
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: #DDDDDD;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color; // rgb(187, 186, 186);
+ border-bottom-style: solid;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 20px;
}
.contextMenu-item:hover {
- transition: all 0.1s;
- background: $lighter-alt-accent;
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
}
.contextMenu-description {
- text-align: left;
- width: 8vw;
+ font-size: 20px;
+ text-align: left;
+ display: inline; //need this?
}
+
+.icon-background {
+ pointer-events: none;
+ background-color: #DDDDDD;
+ width: 35px;
+ text-align: center;
+ font-size: 22px;
+} \ No newline at end of file
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 615a928ad..da374455e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,14 +1,20 @@
import React = require("react");
import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
import { observable, action } from "mobx";
-import { observer } from "mobx-react";
-import "./ContextMenu.scss";
+import { observer } from "mobx-react"
+import "./ContextMenu.scss"
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons';
+
+library.add(faSearch);
+library.add(faCircle);
@observer
export class ContextMenu extends React.Component {
static Instance: ContextMenu;
- @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault() }];
+ @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
@observable private _display: string = "none";
@@ -75,6 +81,11 @@ export class ContextMenu extends React.Component {
return false;
}
+ @action
+ closeMenu = () => {
+ this.clearItems();
+ }
+
render() {
let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
{ left: this._pageX, bottom: this._pageY, display: this._display };
@@ -82,9 +93,14 @@ export class ContextMenu extends React.Component {
return (
<div className="contextMenu-cont" style={style} ref={this.ref}>
- <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input>
+ <span>
+ <span className="icon-background">
+ <FontAwesomeIcon icon="search" size="lg" />
+ </span>
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
+ </span>
{this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
- map(prop => <ContextMenuItem {...prop} key={prop.description} />)}
+ map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)}
</div>
);
}
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 70813f0dd..fcda0db89 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,25 +1,72 @@
import React = require("react");
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-export interface ContextMenuProps {
+export interface OriginalMenuProps {
description: string;
event: (e: React.MouseEvent<HTMLDivElement>) => void;
+ icon?: IconProp; //maybe should be optional (icon?)
+ closeMenu?: () => void;
}
export interface SubmenuProps {
description: string;
subitems: ContextMenuProps[];
+ closeMenu?: () => void;
}
export interface ContextMenuItemProps {
type: ContextMenuProps | SubmenuProps;
}
+export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
+@observer
export class ContextMenuItem extends React.Component<ContextMenuProps> {
+ @observable private _items: Array<ContextMenuProps> = [];
+ @observable private overItem = false;
+
+ constructor(props: ContextMenuProps | SubmenuProps) {
+ super(props);
+ if ("subitems" in this.props) {
+ this.props.subitems.forEach(i => this._items.push(i));
+ }
+ }
+
+ handleEvent = (e: React.MouseEvent<HTMLDivElement>) => {
+ if ("event" in this.props) {
+ this.props.event(e);
+ this.props.closeMenu && this.props.closeMenu();
+ }
+ }
+
render() {
- return (
- <div className="contextMenu-item" onClick={this.props.event}>
- <div className="contextMenu-description">{this.props.description}</div>
- </div>
- );
+ if ("event" in this.props) {
+ return (
+ <div className="contextMenu-item" onClick={this.handleEvent}>
+ <span className="icon-background">
+ {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : <FontAwesomeIcon icon="circle" size="sm" />}
+ </span>
+ <div className="contextMenu-description">
+ {this.props.description}
+ </div>
+ </div>
+ );
+ }
+ else {
+ let submenu = !this.overItem ? (null) :
+ <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
+ {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
+ </div>;
+ return (
+ <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} onMouseLeave={action(() => this.overItem = false)}>
+ <div className="contextMenu-description">
+ {this.props.description}
+ </div>
+ {submenu}
+ </div>
+ );
+ }
}
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 80310941d..389c1897d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,34 +1,33 @@
-import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, StrCast, BoolCast } from "../../new_fields/Types";
import { emptyFunction, Utils } from "../../Utils";
+import { Docs } from "../documents/Documents";
+import { DocumentManager } from "../util/DocumentManager";
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
+import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
+import { CollectionView } from "./collections/CollectionView";
import './DocumentDecorations.scss';
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
+import { FieldView } from "./nodes/FieldView";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { IconBox } from "./nodes/IconBox";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
-import React = require("react");
import { Template, Templates } from "./Templates";
-import { CompileScript } from "../util/Scripting";
-import { IconBox } from "./nodes/IconBox";
-import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
-import { Doc, FieldResult } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
-import { Docs } from "../documents/Documents";
-import { List } from "../../new_fields/List";
+import React = require("react");
+import { URLField } from '../../new_fields/URLField';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-import { faLink } from '@fortawesome/free-solid-svg-icons';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionView } from "./collections/CollectionView";
-import { DocumentManager } from "../util/DocumentManager";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
-import { FieldView } from "./nodes/FieldView";
-import { URLField } from "../../new_fields/URLField";
library.add(faLink);
@@ -252,22 +251,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
}
if (!this._removeIcon) {
- if (selectedDocs.length === 1)
- this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
- else
- if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
- let docViews = SelectionManager.ViewsSortedVertically();
- let topDocView = docViews[0];
- let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
- if (ind !== -1) {
- topDocView.templates.splice(ind, 1);
- topDocView.props.Document.subBulletDocs = undefined;
- } else {
- topDocView.addTemplate(Templates.Bullet);
- topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document));
- }
+ if (selectedDocs.length === 1) {
+ this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].toggleMinimized());
+ } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
+ let docViews = SelectionManager.ViewsSortedVertically();
+ let topDocView = docViews[0];
+ let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
+ if (ind !== -1) {
+ topDocView.templates.splice(ind, 1);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ topDocView.addTemplate(Templates.Bullet);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
}
+ }
}
this._removeIcon = false;
}
@@ -279,16 +277,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let doc = selected[0].props.Document;
let iconDoc = Docs.IconDocument(layoutString);
iconDoc.isButton = true;
- iconDoc.proto!.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title);
+ iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
- iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
- iconDoc.isMinimized = false;
+ //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
+ iconDoc.proto!.isMinimized = false;
iconDoc.width = Number(MINIMIZED_ICON_SIZE);
iconDoc.height = Number(MINIMIZED_ICON_SIZE);
iconDoc.x = NumCast(doc.x);
iconDoc.y = NumCast(doc.y) - 24;
- iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
- doc.minimizedDoc = iconDoc;
+ iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!));
+ selected.length === 1 && (doc.minimizedDoc = iconDoc);
selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
return iconDoc;
}
@@ -468,31 +466,46 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.SelectedDocuments().forEach(element => {
const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
- if (rect.width !== 0) {
+ if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) {
let doc = PositionDocument(element.props.Document);
- let width = FieldValue(doc.width, 0);
- let nwidth = FieldValue(doc.nativeWidth, 0);
- let nheight = FieldValue(doc.nativeHeight, 0);
- let height = FieldValue(doc.height, nwidth ? nheight / nwidth * width : 0);
- let x = FieldValue(doc.x, 0);
- let y = FieldValue(doc.y, 0);
+ let nwidth = doc.nativeWidth || 0;
+ let nheight = doc.nativeHeight || 0;
+ let zoomBasis = NumCast(doc.zoomBasis, 1);
+ let width = (doc.width || 0) / zoomBasis;
+ let height = (doc.height || (nheight / nwidth * width)) / zoomBasis;
let scale = width / rect.width;
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- x += dX * (actualdW - width);
- y += dY * (actualdH - height);
- doc.x = x;
- doc.y = y;
- var nativeWidth = FieldValue(doc.nativeWidth, 0);
- var nativeHeight = FieldValue(doc.nativeHeight, 0);
- if (nativeWidth > 0 && nativeHeight > 0) {
+ doc.x = (doc.x || 0) + dX * (actualdW - width);
+ doc.y = (doc.y || 0) + dY * (actualdH - height);
+ let proto = Doc.GetProto(doc);
+ let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect, false) && nwidth && nheight);
+ if (fixedAspect && (!nwidth || !nheight)) {
+ proto.nativeWidth = doc.width;
+ proto.nativeHeight = doc.height;
+ proto.ignoreAspect = true;
+ }
+ if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
- actualdH = nativeHeight / nativeWidth * actualdW;
+ if (!fixedAspect) proto.nativeWidth = zoomBasis * actualdW / (doc.width || 1) * NumCast(proto.nativeWidth);
+ doc.width = zoomBasis * actualdW;
+ // doc.zoomBasis = zoomBasis * width / actualdW;
+ if (fixedAspect) doc.height = nheight / nwidth * doc.width;
+ else doc.height = zoomBasis * actualdH;
+ proto.nativeHeight = (doc.height || 0) / doc.width * NumCast(proto.nativeWidth);
}
- else actualdW = nativeWidth / nativeHeight * actualdH;
+ else {
+ if (!fixedAspect) proto.nativeHeight = zoomBasis * actualdH / (doc.height || 1) * NumCast(proto.nativeHeight);
+ doc.height = zoomBasis * actualdH;
+ //doc.zoomBasis = zoomBasis * height / actualdH;
+ if (fixedAspect) doc.width = nwidth / nheight * doc.height;
+ else doc.width = zoomBasis * actualdW;
+ proto.nativeWidth = (doc.width || 0) / doc.height * NumCast(proto.nativeHeight);
+ }
+ } else {
+ doc.width = zoomBasis * actualdW;
+ doc.height = zoomBasis * actualdH;
}
- doc.width = actualdW;
- doc.height = actualdH;
}
});
}
@@ -502,7 +515,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
this._resizing = "";
this.Interacting = false;
- SelectionManager.ReselectAll();
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
@@ -536,7 +548,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
considerEmbed = () => {
let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
- let canEmbed = thisDoc.data && Cast(thisDoc.data, URLField);
+ let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
if (!canEmbed) return (null);
return (
<div className="linkButtonWrapper">
@@ -574,11 +586,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
- if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- return 0;
- });
+ let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => {
+ // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1;
+ // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ // return 0;
+ // });
let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
let temps = doc.props.Document.templates;
if (temps instanceof List) {
@@ -586,9 +598,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (temp !== Templates.Bullet.Layout || i === 0) {
res.push(temp);
}
- })
+ });
}
- return res
+ return res;
}, [] as string[]);
let checked = false;
docTemps.forEach(temp => {
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 78143ccda..c946d68e1 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -22,7 +22,7 @@ export interface EditableProps {
* The contents to render when not editing
*/
contents: any;
- height: number;
+ height?: number;
display?: string;
oneLine?: boolean;
}
@@ -53,6 +53,12 @@ export class EditableView extends React.Component<EditableProps> {
}
}
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.editing = true;
+ e.stopPropagation();
+ }
+
render() {
if (this.editing) {
return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
@@ -60,7 +66,7 @@ export class EditableView extends React.Component<EditableProps> {
} else {
return (
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
- onClick={action(() => this.editing = true)} >
+ onClick={this.onClick} >
<span>{this.props.contents}</span>
</div>
);
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 2c550051c..d95398f17 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,35 +1,50 @@
@import "globalCssVariables";
.inkingCanvas {
- opacity:0.99;
+ opacity: 0.99;
+
+ .jsx-parser {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
+ }
}
-.inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect {
+
+.inkingCanvas-paths-ink,
+.inkingCanvas-paths-markers,
+.inkingCanvas-noSelect,
+.inkingCanvas-canSelect {
position: absolute;
top: 0;
- left:0;
+ left: 0;
width: 8192px;
height: 8192px;
- cursor:"crosshair";
+ cursor: "crosshair";
pointer-events: auto;
-
+
}
-.inkingCanvas-canSelect,
-.inkingCanvas-noSelect {
- top:-50000px;
- left:-50000px;
+
+.inkingCanvas-canSelect,
+.inkingCanvas-noSelect {
+ top: -50000px;
+ left: -50000px;
width: 100000px;
height: 100000px;
}
-.inkingCanvas-noSelect {
+
+.inkingCanvas-noSelect {
pointer-events: none;
cursor: "arrow";
}
-.inkingCanvas-paths-ink, .inkingCanvas-paths-markers {
+
+.inkingCanvas-paths-ink,
+.inkingCanvas-paths-markers {
pointer-events: none;
z-index: 10000; // overlays ink on top of everything
cursor: "arrow";
}
+
.inkingCanvas-paths-markers {
mix-blend-mode: multiply;
-}
-
+} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index afe3e3ecb..5d4ea76cd 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -60,7 +60,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
set inkData(value: Map<string, StrokeData>) {
- Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value));
+ Doc.GetProto(this.props.Document).ink = new InkField(value);
}
@action
@@ -74,7 +74,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
e.stopPropagation();
e.preventDefault();
- this.previousState = this.inkData;
+ this.previousState = new Map(this.inkData);
if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
// start the new line, saves a uuid to represent the field of the stroke
@@ -106,10 +106,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
const batch = UndoManager.StartBatch("One ink stroke");
const oldState = this.previousState || new Map;
this.previousState = undefined;
- const newState = this.inkData;
+ const newState = new Map(this.inkData);
UndoManager.AddEvent({
undo: () => this.inkData = oldState,
- redo: () => this.inkData = newState,
+ redo: () => this.inkData = newState
});
batch.end();
}
@@ -134,9 +134,13 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
return { x, y };
}
- @undoBatch
@action
removeLine = (id: string): void => {
+ if (!this.previousState) {
+ this.previousState = new Map(this.inkData);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ }
let data = this.inkData;
data.delete(id);
this.inkData = data;
@@ -146,7 +150,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
get drawnPaths() {
let curPage = NumCast(this.props.Document.curPage, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
- if (strokeData.page === -1 || strokeData.page === curPage) {
+ if (strokeData.page === -1 || Math.round(strokeData.page) === Math.round(curPage)) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
count={strokeData.pathData.length}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 17d4a1e49..d1a6eb7fd 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,5 +1,4 @@
import { observable, action, computed } from "mobx";
-
import { CirclePicker, ColorResult } from 'react-color';
import React = require("react");
import { observer } from "mobx-react";
@@ -35,9 +34,7 @@ export class InkingControl extends React.Component {
@action
switchColor = (color: ColorResult): void => {
this._selectedColor = color.hex;
- SelectionManager.SelectedDocuments().forEach(doc =>
- doc.props.ContainingCollectionView && Doc.SetOnPrototype(doc.props.Document, "backgroundColor", color.hex)
- );
+ SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex);
}
@action
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 37b1f5899..b8d428d31 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -28,6 +28,8 @@ export class InkingStroke extends React.Component<StrokeProps> {
deleteStroke = (e: React.PointerEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
+ e.stopPropagation();
+ e.preventDefault();
}
}
@@ -50,7 +52,7 @@ export class InkingStroke extends React.Component<StrokeProps> {
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
- let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index d63b0147b..57a53c999 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -15,6 +15,9 @@ body {
div {
user-select: none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
}
#dash-title {
@@ -40,7 +43,7 @@ p {
::-webkit-scrollbar {
-webkit-appearance: none;
- height: 5px;
+ height: 10px;
width: 10px;
}
@@ -97,6 +100,21 @@ button:hover {
right: 0px;
}
+.main-notifs-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ color: white;
+ background: #f44b42;
+ font-weight: 300;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px;
+ text-align: center;
+ padding-top: 4px;
+ font-size: 12px;
+}
+
//toolbar stuff
#toolbar {
position: absolute;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 7aef7d3e5..3d9750a85 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,280 +1,11 @@
-import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, runInAction, trace } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
+import { MainView } from "./MainView";
+import { Docs } from "../documents/Documents";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import * as ReactDOM from 'react-dom';
-import Measure from 'react-measure';
-import * as request from 'request';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
-import { Docs } from '../documents/Documents';
-import { SetupDrag, DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { UndoManager } from '../util/UndoManager';
-import { PresentationView } from './PresentationView';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { ContextMenu } from './ContextMenu';
-import { DocumentDecorations } from './DocumentDecorations';
-import { InkingControl } from './InkingControl';
-import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
-import { DocumentView } from './nodes/DocumentView';
-import { PreviewCursor } from './PreviewCursor';
-import { SearchBox } from './SearchBox';
-import { SelectionManager } from '../util/SelectionManager';
-import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { listSpec } from '../../new_fields/Schema';
-import { Id } from '../../new_fields/RefField';
-import { HistoryUtil } from '../util/History';
-
-
-@observer
-export class Main extends React.Component {
- public static Instance: Main;
- @observable private _workspacesShown: boolean = false;
- @observable public pwidth: number = 0;
- @observable public pheight: number = 0;
-
- @computed private get mainContainer(): Opt<Doc> {
- return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
- }
- private set mainContainer(doc: Opt<Doc>) {
- if (doc) {
- if (!("presentationView" in doc)) {
- doc.presentationView = new Doc();
- }
- CurrentUserUtils.UserDocument.activeWorkspace = doc;
- }
- }
-
- constructor(props: Readonly<{}>) {
- super(props);
- Main.Instance = this;
- // causes errors to be generated when modifying an observable outside of an action
- configure({ enforceActions: "observed" });
- if (window.location.pathname !== RouteStore.home) {
- let pathname = window.location.pathname.split("/");
- if (pathname.length > 1 && pathname[pathname.length - 2] === 'doc') {
- CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
- }
- }
-
- library.add(faFont);
- library.add(faImage);
- library.add(faFilePdf);
- library.add(faObjectGroup);
- library.add(faTable);
- library.add(faGlobeAsia);
- library.add(faUndoAlt);
- library.add(faRedoAlt);
- library.add(faPenNib);
- library.add(faFilm);
- library.add(faMusic);
- library.add(faTree);
- this.initEventListeners();
- this.initAuthenticationRouters();
- }
-
- initEventListeners = () => {
- // window.addEventListener("pointermove", (e) => this.reportLocation(e))
- window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
- window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
- window.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- DragManager.AbortDrag();
- SelectionManager.DeselectAll();
- }
- }, false); // drag event handler
- // click interactions for the context menu
- document.addEventListener("pointerdown", action(function (e: PointerEvent) {
- if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
- ContextMenu.Instance.clearItems();
- }
- }), true);
- }
-
- initAuthenticationRouters = async () => {
- // Load the user's active workspace, or create a new one if initial session after signup
- if (!CurrentUserUtils.MainDocId) {
- const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
- if (doc) {
- this.openWorkspace(doc);
- } else {
- this.createNewWorkspace();
- }
- } else {
- DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
- field instanceof Doc ? this.openWorkspace(field) :
- this.createNewWorkspace(CurrentUserUtils.MainDocId));
- }
- }
-
- @action
- createNewWorkspace = async (id?: string) => {
- const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
- if (list) {
- let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
- let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` });
- list.push(mainDoc);
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- let pendingDocument = Docs.SchemaDocument(["title"], [], { title: "New Mobile Uploads" });
- mainDoc.optionalRightCollection = pendingDocument;
- }, 0);
- }
- }
-
- @action
- openWorkspace = async (doc: Doc, fromHistory = false) => {
- CurrentUserUtils.MainDocId = doc[Id];
- this.mainContainer = doc;
- fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
- const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
- // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
- setTimeout(async () => {
- if (col) {
- const l = Cast(col.data, listSpec(Doc));
- if (l && l.length > 0) {
- CollectionDockingView.Instance.AddRightSplit(col);
- }
- }
- }, 100);
- }
- @action
- onResize = (r: any) => {
- this.pwidth = r.offset.width;
- this.pheight = r.offset.height;
- }
- getPWidth = () => {
- return this.pwidth;
- }
- getPHeight = () => {
- return this.pheight;
- }
- @computed
- get mainContent() {
- let mainCont = this.mainContainer;
- let content = !mainCont ? (null) :
- <DocumentView Document={mainCont}
- toggleMinimized={emptyFunction}
- addDocument={undefined}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={this.getPWidth}
- PanelHeight={this.getPHeight}
- isTopMost={true}
- selectOnLoad={false}
- focus={emptyFunction}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined} />;
- const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
- return <Measure offset onResize={this.onResize}>
- {({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div">
- {content}
- {pres ? <PresentationView Document={pres} key="presentation" /> : null}
- </div>
- }
- </Measure>;
- }
-
- /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
- nodesMenu() {
-
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
-
- let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
- let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
- // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
- let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
- let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
- let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
-
- let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
- [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
- [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
- [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
- [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
- [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
- [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
- ];
-
- return < div id="add-nodes-menu" >
- <input type="checkbox" id="add-menu-toggle" />
- <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
-
- <div id="add-options-content">
- <ul id="add-options-list">
- {btns.map(btn =>
- <li key={btn[1]} ><div ref={btn[0]}>
- <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
- <FontAwesomeIcon icon={btn[1]} size="sm" />
- </button>
- </div></li>)}
- </ul>
- </div>
- </div >;
- }
-
- /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
- @computed
- get miscButtons() {
- let logoutRef = React.createRef<HTMLDivElement>();
-
- return [
- <button className="clear-db-button" key="clear-db" onClick={DocServer.DeleteDatabase}>Clear Database</button>,
- <div id="toolbar" key="toolbar">
- <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
- <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
- </div >,
- <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>,
- <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
- ];
-
- }
-
- render() {
- return (
- <div id="main-div">
- <DocumentDecorations />
- {this.mainContent}
- <PreviewCursor />
- <ContextMenu />
- {this.nodesMenu()}
- {this.miscButtons}
- <InkingControl />
- <MainOverlayTextBox />
- </div>
- );
- }
-}
+import * as React from 'react';
(async () => {
await Docs.initProtos();
await CurrentUserUtils.loadCurrentUser();
- ReactDOM.render(<Main />, document.getElementById('root'));
+ ReactDOM.render(<MainView />, document.getElementById('root'));
})();
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 91f626737..a0359419b 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,12 +1,14 @@
import { action, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { emptyFunction, returnTrue, returnZero } from '../../Utils';
+import { emptyFunction, returnTrue, returnZero, Utils } from '../../Utils';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
import "normalize.css";
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { Doc } from '../../new_fields/Doc';
interface MainOverlayTextBoxProps {
}
@@ -15,10 +17,12 @@ interface MainOverlayTextBoxProps {
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
@observable _textXf: () => Transform = () => Transform.Identity();
- private _textFieldKey: string = "data";
+ public TextFieldKey: string = "data";
private _textColor: string | null = null;
+ private _textHideOnLeave?: boolean;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
+ public TextDoc?: Doc;
constructor(props: MainOverlayTextBoxProps) {
super(props);
@@ -26,8 +30,16 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
MainOverlayTextBox.Instance = this;
reaction(() => FormattedTextBox.InputBoxOverlay,
(box?: FormattedTextBox) => {
- if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
- else this.setTextDoc();
+ if (box) {
+ this.TextDoc = box.props.Document;
+ let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
+ let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); };
+ this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf)
+ }
+ else {
+ this.TextDoc = undefined;
+ this.setTextDoc();
+ }
});
}
@@ -36,12 +48,12 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
if (this._textTargetDiv) {
this._textTargetDiv.style.color = this._textColor;
}
- this._textFieldKey = textFieldKey!;
+ this.TextFieldKey = textFieldKey!;
this._textXf = tx ? tx : () => Transform.Identity();
this._textTargetDiv = div;
+ this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
if (div) {
- if (div.parentElement && div.parentElement instanceof HTMLDivElement && div.parentElement.id === "screenSpace") this._textXf = () => Transform.Identity();
- this._textColor = div.style.color;
+ this._textColor = (getComputedStyle(div) as any).color;
div.style.color = "transparent";
}
}
@@ -81,16 +93,22 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.removeEventListener('pointerup', this.textBoxUp);
}
+ addDocTab = (doc: Doc, location: string) => {
+ if (true) { // location === "onRight") { need to figure out stack to add "inTab"
+ CollectionDockingView.Instance.AddRightSplit(doc);
+ }
+ }
render() {
if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
+ let auto = FormattedTextBox.InputBoxOverlay.props.height;
return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
<div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
- <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
+ style={{ width: `${textRect.width * s}px`, height: auto ? "auto" : `${textRect.height * s}px` }}>
+ <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} />
</div>
</ div>;
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
new file mode 100644
index 000000000..42d5929bf
--- /dev/null
+++ b/src/client/views/MainView.tsx
@@ -0,0 +1,322 @@
+import { IconName, library } from '@fortawesome/fontawesome-svg-core';
+import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt, faBell } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, configure, observable, runInAction, trace } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import Measure from 'react-measure';
+import * as request from 'request';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { RouteStore } from '../../server/RouteStore';
+import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { Docs } from '../documents/Documents';
+import { SetupDrag, DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { PresentationView } from './PresentationView';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { ContextMenu } from './ContextMenu';
+import { DocumentDecorations } from './DocumentDecorations';
+import { InkingControl } from './InkingControl';
+import "./Main.scss";
+import { MainOverlayTextBox } from './MainOverlayTextBox';
+import { DocumentView } from './nodes/DocumentView';
+import { PreviewCursor } from './PreviewCursor';
+import { SearchBox } from './SearchBox';
+import { SelectionManager } from '../util/SelectionManager';
+import { FieldResult, Field, Doc, Opt, DocListCast } from '../../new_fields/Doc';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+import { listSpec } from '../../new_fields/Schema';
+import { Id } from '../../new_fields/FieldSymbols';
+import { HistoryUtil } from '../util/History';
+import { CollectionBaseView } from './collections/CollectionBaseView';
+
+
+@observer
+export class MainView extends React.Component {
+ public static Instance: MainView;
+ @observable private _workspacesShown: boolean = false;
+ @observable public pwidth: number = 0;
+ @observable public pheight: number = 0;
+ @computed private get mainContainer(): Opt<Doc> {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
+ }
+ private set mainContainer(doc: Opt<Doc>) {
+ if (doc) {
+ if (!("presentationView" in doc)) {
+ doc.presentationView = Docs.TreeDocument([], { title: "Presentation" });
+ }
+ CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ }
+ }
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ MainView.Instance = this;
+ // causes errors to be generated when modifying an observable outside of an action
+ configure({ enforceActions: "observed" });
+ if (window.location.search.includes("readonly")) {
+ DocServer.makeReadOnly();
+ }
+ if (window.location.search.includes("safe")) {
+ if (!window.location.search.includes("nro")) {
+ DocServer.makeReadOnly();
+ }
+ CollectionBaseView.SetSafeMode(true);
+ }
+ if (window.location.pathname !== RouteStore.home) {
+ let pathname = window.location.pathname.substr(1).split("/");
+ if (pathname.length > 1) {
+ let type = pathname[0];
+ if (type === "doc") {
+ CurrentUserUtils.MainDocId = pathname[1];
+ }
+ }
+ }
+
+ library.add(faFont);
+ library.add(faImage);
+ library.add(faFilePdf);
+ library.add(faObjectGroup);
+ library.add(faTable);
+ library.add(faGlobeAsia);
+ library.add(faUndoAlt);
+ library.add(faRedoAlt);
+ library.add(faPenNib);
+ library.add(faFilm);
+ library.add(faMusic);
+ library.add(faTree);
+ this.initEventListeners();
+ this.initAuthenticationRouters();
+ }
+
+ initEventListeners = () => {
+ // window.addEventListener("pointermove", (e) => this.reportLocation(e))
+ window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
+ window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
+ window.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ DragManager.AbortDrag();
+ SelectionManager.DeselectAll();
+ }
+ }, false); // drag event handler
+ // click interactions for the context menu
+ document.addEventListener("pointerdown", action(function (e: PointerEvent) {
+
+ const targets = document.elementsFromPoint(e.x, e.y);
+ if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
+ ContextMenu.Instance.clearItems();
+ }
+ }), true);
+ }
+
+ initAuthenticationRouters = async () => {
+ // Load the user's active workspace, or create a new one if initial session after signup
+ if (!CurrentUserUtils.MainDocId) {
+ const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ if (doc) {
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
+ } else {
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+ field instanceof Doc ? this.openWorkspace(field) :
+ this.createNewWorkspace(CurrentUserUtils.MainDocId));
+ }
+ }
+
+ @action
+ createNewWorkspace = async (id?: string) => {
+ const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ if (list) {
+ let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
+ let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
+ list.push(mainDoc);
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => {
+ this.openWorkspace(mainDoc);
+ // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
+ // mainDoc.optionalRightCollection = pendingDocument;
+ }, 0);
+ }
+ }
+
+ @observable _notifsCol: Opt<Doc>;
+
+ @action
+ openWorkspace = async (doc: Doc, fromHistory = false) => {
+ CurrentUserUtils.MainDocId = doc[Id];
+ this.mainContainer = doc;
+ fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
+ const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
+ // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
+ setTimeout(async () => {
+ if (col) {
+ const l = Cast(col.data, listSpec(Doc));
+ if (l) {
+ runInAction(() => this._notifsCol = col);
+ }
+ }
+ }, 100);
+ }
+
+ openNotifsCol = () => {
+ if (this._notifsCol && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.AddRightSplit(this._notifsCol);
+ }
+ }
+
+ onDrop = (e: React.DragEvent<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log("Drop");
+ }
+
+ @action
+ onResize = (r: any) => {
+ this.pwidth = r.offset.width;
+ this.pheight = r.offset.height;
+ }
+ getPWidth = () => {
+ return this.pwidth;
+ }
+ getPHeight = () => {
+ return this.pheight;
+ }
+ @computed
+ get mainContent() {
+ let mainCont = this.mainContainer;
+ let content = !mainCont ? (null) :
+ <DocumentView Document={mainCont}
+ addDocument={undefined}
+ addDocTab={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.getPWidth}
+ PanelHeight={this.getPHeight}
+ isTopMost={true}
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined} />;
+ const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
+ return <Measure offset onResize={this.onResize}>
+ {({ measureRef }) =>
+ <div ref={measureRef} id="mainContent-div" onDrop={this.onDrop}>
+ {content}
+ {pres ? <PresentationView Document={pres} key="presentation" /> : null}
+ </div>
+ }
+ </Measure>;
+ }
+
+ /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
+ nodesMenu() {
+
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
+ let weburl = "https://cs.brown.edu/courses/cs166/";
+ let audiourl = "http://techslides.com/demos/samples/sample.mp3";
+ let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
+
+ let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
+ let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+ let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+ let addTreeNode = action(() => CurrentUserUtils.UserDocument);
+ //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
+ // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
+ let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
+ let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+ let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
+ let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
+ [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
+ [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
+ [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
+ [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
+ [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
+ [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
+ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
+ [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
+ [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
+ ];
+
+ return < div id="add-nodes-menu" >
+ <input type="checkbox" id="add-menu-toggle" />
+ <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+
+ <div id="add-options-content">
+ <ul id="add-options-list">
+ {btns.map(btn =>
+ <li key={btn[1]} ><div ref={btn[0]}>
+ <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
+ <FontAwesomeIcon icon={btn[1]} size="sm" />
+ </button>
+ </div></li>)}
+ </ul>
+ </div>
+ </div >;
+ }
+
+ /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
+ @computed
+ get miscButtons() {
+ const length = this._notifsCol ? DocListCast(this._notifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => this._notifsCol!);
+ let logoutRef = React.createRef<HTMLDivElement>();
+
+ return [
+ <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>,
+ <div id="toolbar" key="toolbar">
+ <div ref={notifsRef}>
+ <button className="toolbar-button round-button" title="Notifs"
+ onClick={this.openNotifsCol} onPointerDown={this._notifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ <FontAwesomeIcon icon={faBell} size="sm" />
+ </button>
+ <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
+ {length}
+ </div>
+ </div>
+ <button className="toolbar-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
+ </div >,
+ this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div> : null,
+ <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+ <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ ];
+
+ }
+
+ @observable isSearchVisible = false;
+ @action
+ toggleSearch = () => {
+ this.isSearchVisible = !this.isSearchVisible;
+ }
+
+ render() {
+ return (
+ <div id="main-div">
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.nodesMenu()}
+ {this.miscButtons}
+ <InkingControl />
+ <MainOverlayTextBox />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
index 593191004..d2d41a4ba 100644
--- a/src/client/views/PresentationView.tsx
+++ b/src/client/views/PresentationView.tsx
@@ -1,16 +1,13 @@
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import React = require("react");
-import { observable, action, runInAction, reaction } from "mobx";
-import "./PresentationView.scss";
-import "./Main.tsx";
-import { DocumentManager } from "../util/DocumentManager";
-import { Utils } from "../../Utils";
-import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, FieldValue, PromiseValue, StrCast } from "../../new_fields/Types";
-import { Id } from "../../new_fields/RefField";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
import { List } from "../../new_fields/List";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { listSpec } from "../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
+import { DocumentManager } from "../util/DocumentManager";
+import "./PresentationView.scss";
+import React = require("react");
export interface PresViewProps {
Document: Doc;
@@ -27,6 +24,7 @@ interface PresListProps extends PresViewProps {
*/
class PresentationViewList extends React.Component<PresListProps> {
+
/**
* Renders a single child document. It will just append a list element.
* @param document The document to render.
@@ -42,8 +40,17 @@ class PresentationViewList extends React.Component<PresListProps> {
//this doc is selected
className += " presentationView-selected";
}
+ let onEnter = (e: React.PointerEvent) => { document.libraryBrush = true; }
+ let onLeave = (e: React.PointerEvent) => { document.libraryBrush = false; }
return (
- <div className={className} key={document[Id] + index} onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
+ <div className={className} key={document[Id] + index}
+ onPointerEnter={onEnter} onPointerLeave={onLeave}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(document.libraryBrush, false) || BoolCast(document.protoBrush, false) ? `1px` : "0px",
+ }}
+ onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
<strong className="presentationView-name">
{`${index + 1}. ${title}`}
</strong>
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 4218ea7c9..7c1d00eb0 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -22,9 +22,11 @@ export class PreviewCursor extends React.Component<{}> {
}
paste = (e: ClipboardEvent) => {
console.log(e.clipboardData);
- console.log(e.clipboardData.getData("text/html"));
- console.log(e.clipboardData.getData("text/csv"));
- console.log(e.clipboardData.getData("text/plain"));
+ if (e.clipboardData) {
+ console.log(e.clipboardData.getData("text/html"));
+ console.log(e.clipboardData.getData("text/csv"));
+ console.log(e.clipboardData.getData("text/plain"));
+ }
}
@action
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
index 6e64e1af1..63d2065e2 100644
--- a/src/client/views/SearchBox.tsx
+++ b/src/client/views/SearchBox.tsx
@@ -16,10 +16,12 @@ import { isString } from 'util';
import { constant } from 'async';
import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
-import { Id } from '../../new_fields/RefField';
+import { Id } from '../../new_fields/FieldSymbols';
import { DocumentManager } from '../util/DocumentManager';
import { SetupDrag } from '../util/DragManager';
import { Docs } from '../documents/Documents';
+import { RouteStore } from '../../server/RouteStore';
+import { NumCast } from '../../new_fields/Types';
library.add(faSearch);
library.add(faObjectGroup);
@@ -70,6 +72,22 @@ export class SearchBox extends React.Component {
}
return docs;
}
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ let posting = DocServer.prepend(RouteStore.dataUriToImage);
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
@action
handleClickFilter = (e: Event): void => {
@@ -129,15 +147,26 @@ export class SearchBox extends React.Component {
for (const doc of docs) {
doc.x = x;
doc.y = y;
- doc.width = 200;
- doc.height = 200;
+ const size = 200;
+ const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ if (aspect > 1) {
+ doc.height = size;
+ doc.width = size / aspect;
+ } else if (aspect > 0) {
+ doc.width = size;
+ doc.height = size * aspect;
+ } else {
+ doc.width = size;
+ doc.height = size;
+ }
+ doc.zoomBasis = 1;
x += 250;
if (x > 1000) {
x = 0;
- y += 250;
+ y += 300;
}
}
- return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, title: `Search Docs: "${this.searchString}"` });
+ return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
}
// Useful queries:
@@ -154,8 +183,8 @@ export class SearchBox extends React.Component {
<input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
style={{ width: this._resultsOpen ? "500px" : undefined }} />
- <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button>
- <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" />
+ {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
+ {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
</div>
{this._resultsOpen ? (
<div className="searchBox-results">
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
index 01c7316d6..6901f60da 100644
--- a/src/client/views/SearchItem.tsx
+++ b/src/client/views/SearchItem.tsx
@@ -1,13 +1,9 @@
import React = require("react");
-import { Doc } from "../../new_fields/Doc";
-import { DocumentManager } from "../util/DocumentManager";
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Cast } from "../../new_fields/Types";
-import { FieldView, FieldViewProps } from './nodes/FieldView';
-import { computed } from "mobx";
-import { IconField } from "../../new_fields/IconField";
+import { Doc } from "../../new_fields/Doc";
+import { DocumentManager } from "../util/DocumentManager";
import { SetupDrag } from "../util/DragManager";
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 22c4edc25..3288abd90 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,12 +1,11 @@
-import { observable, computed, action, trace } from "mobx";
-import React = require("react");
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
import './DocumentDecorations.scss';
-import { Template } from "./Templates";
import { DocumentView } from "./nodes/DocumentView";
-import { List } from "../../new_fields/List";
-import { Doc } from "../../new_fields/Doc";
-import { NumCast } from "../../new_fields/Types";
+import { Template } from "./Templates";
+import React = require("react");
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -43,16 +42,16 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- if (template.Name == "Bullet") {
+ if (template.Name === "Bullet") {
let topDocView = this.props.docs[0];
topDocView.addTemplate(template);
- topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
} else {
this.props.docs.map(d => d.addTemplate(template));
}
this.props.templates.set(template, true);
} else {
- if (template.Name == "Bullet") {
+ if (template.Name === "Bullet") {
let topDocView = this.props.docs[0];
topDocView.removeTemplate(template);
topDocView.props.Document.subBulletDocs = undefined;
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index ca0c04d1b..3cd525afa 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -41,45 +41,50 @@ export namespace Templates {
export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div id="screenSpace" style="top: 100%; font-size:14px; background:yellow; width:100%; position:absolute">
- <FormattedTextBox {...props} fieldKey={"caption"} />
+ <div style="height:100%; width:100%;">{layout}</div>
+ <div style="bottom: 0; font-size:14px; width:100%; position:absolute">
+ <FormattedTextBox {...props} height="min-content" fieldKey={"caption"} hideOnLeave={"true"} />
</div>
</div>` );
- export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
- `<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ export const Title = new Template("Title", TemplatePosition.InnerTop,
+ `<div style="height:100%">
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
<span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
</div>
+ <div style="display:flex; flex-direction:column; height:calc(100% - 25px);">
+ <div style="height:min-content; width:100%;overflow:auto">{layout}</div>
+ </div>
</div>` );
- export const Title = new Template("Title", TemplatePosition.InnerTop,
- `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
- <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
- <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
- </div></div>` );
+ export const Header = new Template("Header", TemplatePosition.InnerTop,
+ `< div style = "display:flex; flex-direction:column; height:100%;" >
+ <div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} />
+ </div>
+ <div style="width:100%;height:min-content;overflow:auto;">{layout}</div>
+ </div > ` );
export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
- `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ `< div >
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
<div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
- <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
- width="15px" height="15px"/>
- </div>
- </div>`
+ <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
+ width="15px" height="15px" />
+ </div>
+ </div > `
);
export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
- return (`<div>
- <div style="height:100%; width:100%;position:absolute;">{layout}</div>
- <div style="width:${width}px; height:${height}px; top:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
- <ImageBox id="isExpander" {...props} PanelWidth={${width}} fieldKey={"${field}"} />
+ return (`< div >
+ <div style="height:100%; width:100%; position:absolute;">{layout}</div>
+ <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
+ <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
</div>
- </div>`);
+ </div > `);
}
- export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet];
+ export const TemplateList: Template[] = [Title, Header, Caption, Bullet];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
new file mode 100644
index 000000000..1f5acb96a
--- /dev/null
+++ b/src/client/views/collections/CollectionBaseView.scss
@@ -0,0 +1,11 @@
+@import "../globalCssVariables";
+#collectionBaseView {
+ border-width: 0;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border-color: $light-color-secondary;
+ border-style: solid;
+ border-radius: 0 0 $border-radius $border-radius;
+ box-sizing: border-box;
+ border-radius: inherit;
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 31bdf213e..5238ad114 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,14 +1,16 @@
-import { action, computed } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ContextMenu } from '../ContextMenu';
-import { FieldViewProps } from '../nodes/FieldView';
-import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc';
-import { listSpec } from '../../../new_fields/Schema';
+import { Doc, DocListCast, Opt } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { Id } from '../../../new_fields/RefField';
+import { listSpec } from '../../../new_fields/Schema';
+import { Cast, FieldValue, NumCast, PromiseValue } from '../../../new_fields/Types';
import { SelectionManager } from '../../util/SelectionManager';
+import { ContextMenu } from '../ContextMenu';
+import { FieldViewProps } from '../nodes/FieldView';
+import './CollectionBaseView.scss';
+import { DocumentManager } from '../../util/DocumentManager';
export enum CollectionViewType {
Invalid,
@@ -16,6 +18,7 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
+ Stacking
}
export interface CollectionRenderProps {
@@ -36,9 +39,20 @@ export interface CollectionViewProps extends FieldViewProps {
@observer
export class CollectionBaseView extends React.Component<CollectionViewProps> {
+ @observable private static _safeMode = false;
+ static InSafeMode() { return this._safeMode; }
+ static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
get collectionViewType(): CollectionViewType | undefined {
let Document = this.props.Document;
let viewField = Cast(Document.viewType, "number");
+ if (CollectionBaseView._safeMode) {
+ if (viewField === CollectionViewType.Freeform) {
+ return CollectionViewType.Tree;
+ }
+ if (viewField === CollectionViewType.Invalid) {
+ return CollectionViewType.Freeform;
+ }
+ }
if (viewField !== undefined) {
return viewField;
} else {
@@ -82,15 +96,15 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
return false;
}
- @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; }
@action.bound
addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
let props = this.props;
var curPage = NumCast(props.Document.curPage, -1);
- Doc.SetOnPrototype(doc, "page", curPage);
+ Doc.GetProto(doc).page = curPage;
if (curPage >= 0) {
- Doc.SetOnPrototype(doc, "annotationOn", props.Document);
+ Doc.GetProto(doc).annotationOn = props.Document;
}
if (!this.createsCycle(doc, props.Document)) {
//TODO This won't create the field if it doesn't already exist
@@ -108,7 +122,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
// set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
let zoom = NumCast(this.props.Document.scale, 1);
- Doc.SetOnPrototype(doc, "zoomBasis", zoom);
+ // Doc.GetProto(doc).zoomBasis = zoom;
}
}
return true;
@@ -116,6 +130,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
removeDocument(doc: Doc): boolean {
+ let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView)
+ docView && SelectionManager.DeselectDoc(docView);
const props = this.props;
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
@@ -127,7 +143,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
break;
}
}
- PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => {
+ PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => {
if (annotationOn === props.Document) {
doc.annotationOn = undefined;
}
@@ -145,11 +161,10 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (this.props.Document === targetCollection) {
+ if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
return true;
}
if (this.removeDocument(doc)) {
- SelectionManager.DeselectAll();
return addDocument(doc);
}
return false;
@@ -165,8 +180,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
};
const viewtype = this.collectionViewType;
return (
- <div className={this.props.className || "collectionView-cont"}
- style={{ borderRadius: "inherit", pointerEvents: "all" }}
+ <div id="collectionBaseView" className={this.props.className || "collectionView-cont"}
onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
</div>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 58f1e33a1..51e29cb54 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,25 +1,29 @@
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, Lambda } from "mobx";
+import { action, Lambda, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import Measure from "react-measure";
import * as GoldenLayout from "../../../client/goldenLayout";
-import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
-import { FieldId, Id } from "../../../new_fields/RefField";
+import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
+import { Id } from '../../../new_fields/FieldSymbols';
+import { FieldId } from "../../../new_fields/RefField";
import { listSpec } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
+import { DocumentManager } from '../../util/DocumentManager';
import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
+import { CollectionViewType } from './CollectionBaseView';
import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
-import React = require("react");
import { ParentDocSelector } from './ParentDocumentSelector';
-import { DocumentManager } from '../../util/DocumentManager';
+import React = require("react");
+import { MainView } from '../MainView';
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -74,7 +78,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
@action
- public CloseRightSplit(document: Doc): boolean {
+ public CloseRightSplit = (document: Doc): boolean => {
let retVal = false;
if (this._goldenLayout.root.contentItems[0].isRow) {
retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
@@ -83,7 +87,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
child.contentItems[0].remove();
this.layoutChanged(document);
return true;
- } else
+ } else {
Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
child.contentItems[j].remove();
@@ -94,8 +98,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
return false;
});
+ }
return false;
- })
+ });
}
if (retVal) {
this.stateChanged();
@@ -106,7 +111,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@action
layoutChanged(removed?: Doc) {
this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
- this._goldenLayout.emit('sbcreteChanged');
+ this._goldenLayout.emit('stateChanged');
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
this.stateChanged();
@@ -116,7 +121,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@action
- public AddRightSplit(document: Doc, minimize: boolean = false) {
+ public AddRightSplit = (document: Doc, minimize: boolean = false) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
@@ -152,6 +157,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return newContentItem;
}
+ @action
+ public AddTab = (stack: any, document: Doc) => {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (docs) {
+ docs.push(document);
+ }
+ let docContentConfig = CollectionDockingView.makeDocumentConfig(document);
+ var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
+ stack.addChild(newContentItem.contentItems[0], undefined);
+ this.layoutChanged();
+ }
setupGoldenLayout() {
var config = StrCast(this.props.Document.dockingConfig);
@@ -257,6 +273,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let y = e.clientY;
let docid = (e.target as any).DashDocId;
let tab = (e.target as any).parentElement as HTMLElement;
+ let glTab = (e.target as any).Tab;
+ if (glTab && glTab.contentItem && glTab.contentItem.parent) {
+ glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
+ }
DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
if (f instanceof Doc) {
DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
@@ -314,7 +334,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
tab.element.append(counter);
let upDiv = document.createElement("span");
- ReactDOM.render(<ParentDocSelector Document={doc} />, upDiv);
+ const stack = tab.contentItem.parent;
+ ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
tab.reactComponents = [upDiv];
tab.element.append(upDiv);
counter.DashDocId = tab.contentItem.config.props.documentId;
@@ -327,6 +348,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
});
}
+ tab.titleElement[0].Tab = tab;
tab.closeElement.off('click') //unbind the current click handler
.click(async function () {
if (tab.reactionDisposer) {
@@ -336,6 +358,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (doc instanceof Doc) {
let theDoc = doc;
CollectionDockingView.Instance._removedDocs.push(theDoc);
+ SelectionManager.DeselectAll();
}
tab.contentItem.remove();
});
@@ -394,7 +417,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
-
+ get _stack(): any {
+ let parent = (this.props as any).glContainer.parent.parent;
+ if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
+ return parent.parent.contentItems[1];
+ return parent;
+ }
constructor(props: any) {
super(props);
DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
@@ -413,21 +441,39 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (this._mainCont.current && this._mainCont.current.children) {
let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
scale = Utils.GetScreenTransform(this._mainCont.current).scale;
- return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling());
+ return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
}
return Transform.Identity();
}
+ get scaleToFitMultiplier() {
+ let docWidth = NumCast(this._document!.width);
+ let docHeight = NumCast(this._document!.height);
+ if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1;
+ if (StrCast(this._document!.layout).indexOf("Collection") === -1 ||
+ NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1;
+ let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ?
+ this._panelHeight / docHeight : this._panelWidth / docWidth);
+ return scaling;
+ }
get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
+ addDocTab = (doc: Doc, location: string) => {
+ if (doc.dockingConfig) {
+ MainView.Instance.openWorkspace(doc);
+ } else if (location === "onRight") {
+ CollectionDockingView.Instance.AddRightSplit(doc);
+ } else {
+ CollectionDockingView.Instance.AddTab(this._stack, doc);
+ }
+ }
get content() {
if (!this._document) {
return (null);
}
return (
<div className="collectionDockingView-content" ref={this._mainCont}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
<DocumentView key={this._document[Id]} Document={this._document}
- toggleMinimized={emptyFunction}
bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
@@ -440,6 +486,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
+ addDocTab={this.addDocTab}
ContainingCollectionView={undefined} />
</div >);
}
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index f6fb79582..50201bae8 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -1,49 +1,56 @@
.collectionPdfView-buttonTray {
- top : 15px;
- left : 20px;
- position: relative;
+ top: 15px;
+ left: 20px;
+ position: relative;
transform-origin: left top;
position: absolute;
}
+
.collectionPdfView-thumb {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: darkgray;
}
+
.collectionPdfView-slider {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: lightgray;
}
-.collectionPdfView-cont{
+
+.collectionPdfView-cont {
width: 100%;
height: 100%;
- position: absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
+ z-index: -1;
}
+
.collectionPdfView-cont-dragging {
span {
user-select: none;
}
}
+
.collectionPdfView-backward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 0px;
+ top: 0px;
+ left: 0px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
}
+
.collectionPdfView-forward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 45px;
+ top: 0px;
+ left: 45px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index b3762206a..bf887ce7c 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -8,7 +8,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
import { emptyFunction } from "../../../Utils";
import { NumCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
@observer
@@ -61,7 +61,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
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: "PDFOptions", event: emptyFunction });
+ ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" });
}
}
@@ -70,7 +70,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
return (
<>
<CollectionFreeFormView {...props} CollectionView={this} />
- {this.props.isSelected() ? this.uIButtons : (null)}
+ {renderProps.active() ? this.uIButtons : (null)}
</>
);
}
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 5e9d2ac67..186e006f3 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -4,56 +4,63 @@
.collectionSchemaView-container {
border-width: $COLLECTION_BORDER_WIDTH;
- border-color : $intermediate-color;
+ border-color: $intermediate-color;
border-style: solid;
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
+ overflow: hidden;
.collectionSchemaView-cellContents {
height: $MAX_ROW_HEIGHT;
+
img {
- width:auto;
+ width: auto;
max-height: $MAX_ROW_HEIGHT;
}
}
-
+
.collectionSchemaView-previewRegion {
position: relative;
background: $light-color;
float: left;
height: 100%;
+
.collectionSchemaView-previewDoc {
height: 100%;
- width: 100%;
+ width: 100%;
position: absolute;
}
+
.collectionSchemaView-input {
position: absolute;
max-width: 150px;
width: 100%;
bottom: 0px;
}
+
.documentView-node:first-child {
position: relative;
background: $light-color;
}
}
+
.collectionSchemaView-previewHandle {
position: absolute;
height: 15px;
- width: 15px;
- z-index: 20;
- right: 0;
- top: 20px;
- background: Black ;
+ width: 15px;
+ z-index: 20;
+ right: 0;
+ top: 20px;
+ background: Black;
}
- .collectionSchemaView-dividerDragger{
- position: relative;
- background: black;
- float: left;
+
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ background: black;
+ float: left;
height: 37px;
width: 20px;
z-index: 20;
@@ -61,6 +68,7 @@
top: 0;
background: $main-accent;
}
+
.collectionSchemaView-columnsHandle {
position: absolute;
height: 37px;
@@ -70,6 +78,7 @@
bottom: 0;
background: $main-accent;
}
+
.collectionSchemaView-colDividerDragger {
position: relative;
box-sizing: border-box;
@@ -78,6 +87,7 @@
float: top;
width: 100%;
}
+
.collectionSchemaView-dividerDragger {
position: relative;
box-sizing: border-box;
@@ -86,11 +96,13 @@
float: left;
height: 100%;
}
+
.collectionSchemaView-tableContainer {
position: relative;
float: left;
height: 100%;
}
+
.ReactTable {
// position: absolute; // display: inline-block;
// overflow: auto;
@@ -99,6 +111,7 @@
background: $light-color;
box-sizing: border-box;
border: none !important;
+
.rt-table {
overflow-y: auto;
overflow-x: auto;
@@ -107,26 +120,32 @@
direction: ltr; // direction:rtl;
// display:block;
}
+
.rt-tbody {
//direction: ltr;
direction: rtl;
}
+
.rt-tr-group {
direction: ltr;
max-height: $MAX_ROW_HEIGHT;
}
+
.rt-td {
border-width: 1px;
border-right-color: $intermediate-color;
+
.imageBox-cont {
position: relative;
max-height: 100%;
}
+
.imageBox-cont img {
object-fit: contain;
max-width: 100%;
height: 100%;
}
+
.videoBox-cont {
object-fit: contain;
width: auto;
@@ -134,15 +153,17 @@
}
}
}
+
.ReactTable .rt-thead.-header {
background: $intermediate-color;
color: $light-color;
- text-transform: uppercase;
+ // text-transform: uppercase;
letter-spacing: 2px;
font-size: 12px;
height: 30px;
padding-top: 4px;
}
+
.ReactTable .rt-th,
.ReactTable .rt-td {
max-height: $MAX_ROW_HEIGHT;
@@ -150,32 +171,36 @@
font-size: 13px;
text-align: center;
}
+
.ReactTable .rt-tbody .rt-tr-group:last-child {
border-bottom: $intermediate-color;
border-bottom-style: solid;
border-bottom-width: 1;
}
+
.documentView-node-topmost {
- text-align:left;
+ text-align: left;
transform-origin: center top;
display: inline-block;
}
+
.documentView-node:first-child {
background: $light-color;
}
}
+
//options menu styling
#schemaOptionsMenuBtn {
position: absolute;
height: 20px;
width: 20px;
border-radius: 50%;
- z-index: 21;
+ z-index: 21;
right: 4px;
- top: 4px;
+ top: 4px;
pointer-events: auto;
- background-color:black;
- display:inline-block;
+ background-color: black;
+ display: inline-block;
padding: 0px;
font-size: 100%;
}
@@ -189,10 +214,12 @@ ul {
padding: 0px;
margin: 0px;
}
+
.schema-options-subHeader {
color: $intermediate-color;
margin-bottom: 5px;
}
+
#schemaOptionsMenuBtn:hover {
transform: scale(1.15);
}
@@ -202,15 +229,15 @@ ul {
font-size: 12px;
}
- #options-flyout-div {
+#options-flyout-div {
text-align: left;
- padding:0px;
+ padding: 0px;
z-index: 100;
font-family: $sans-serif;
padding-left: 5px;
- }
+}
- #schema-col-checklist {
+#schema-col-checklist {
overflow: scroll;
text-align: left;
//background-color: $light-color-secondary;
@@ -218,8 +245,8 @@ ul {
max-height: 175px;
font-family: $sans-serif;
font-size: 12px;
- }
-
+}
+
.Resizer {
box-sizing: border-box;
@@ -227,6 +254,7 @@ ul {
opacity: 0.5;
z-index: 1;
background-clip: padding-box;
+
&.horizontal {
height: 11px;
margin: -5px 0;
@@ -234,22 +262,26 @@ ul {
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
+
&:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
}
+
&.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
+
&:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5);
}
}
+
&:hover {
-webkit-transition: all 2s ease;
transition: all 2s ease;
@@ -270,10 +302,12 @@ ul {
-ms-flex-direction: column;
flex-direction: column;
}
+
header {
padding: 1rem;
background: #eee;
}
+
footer {
padding: 1rem;
background: #eee;
@@ -287,10 +321,12 @@ ul {
display: flex;
flex-direction: column;
}
+
header {
padding: 1rem;
background: #eee;
}
+
footer {
padding: 1rem;
background: #eee;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index f4ad5b357..b9e5a5b65 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,7 +2,7 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction } from "mobx";
+import { action, computed, observable, untracked, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
@@ -20,22 +20,22 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { listSpec } from "../../../new_fields/Schema";
import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/RefField";
-import { isUndefined } from "typescript-collections/dist/lib/util";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Id } from "../../../new_fields/FieldSymbols";
import { Gateway } from "../../northstar/manager/Gateway";
-import { DocServer } from "../../DocServer";
-import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel";
-import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
-import { AggregateFunction } from "../../northstar/model/idea/idea";
-import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
import { Docs } from "../../documents/Documents";
import { ContextMenu } from "../ContextMenu";
+import { CollectionView } from "./CollectionView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+library.add(faCog);
+library.add(faPlus);
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@@ -58,7 +58,7 @@ class KeyToggle extends React.Component<{ keyName: string, checked: boolean, tog
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _mainCont?: HTMLDivElement;
- private _startSplitPercent = 0;
+ private _startPreviewWidth = 0;
private DIVIDER_WIDTH = 4;
@observable _columns: Array<string> = ["title", "data", "author"];
@@ -66,15 +66,41 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@observable _columnsPercentage = 0;
@observable _keys: string[] = [];
@observable _newKeyName: string = "";
+ @observable previewScript: string = "";
- @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
+ @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
+ @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
@computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
+ @computed get tableColumns() {
+ return this.columns.map(col => {
+ const ref = React.createRef<HTMLParagraphElement>();
+ return {
+ Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>,
+ accessor: (doc: Doc) => doc ? doc[col] : 0,
+ id: col
+ };
+ });
+ }
+
+ onHeaderDrag = (columnName: string) => {
+ let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
+ if (schemaDoc instanceof Doc) {
+ let columnDocs = DocListCast(schemaDoc.data);
+ if (columnDocs) {
+ let ddoc = columnDocs.find(doc => doc.title === columnName);
+ if (ddoc)
+ return ddoc;
+ }
+ }
+ return this.props.Document;
+ }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
- Document: rowProps.value[0],
- fieldKey: rowProps.value[1],
+ Document: rowProps.original,
+ fieldKey: rowProps.column.id as string,
ContainingCollectionView: this.props.CollectionView,
isSelected: returnFalse,
select: emptyFunction,
@@ -86,12 +112,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
whenActiveChanged: emptyFunction,
PanelHeight: returnZero,
PanelWidth: returnZero,
+ addDocTab: this.props.addDocTab,
};
let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
let onItemDown = (e: React.PointerEvent) =>
- (this.props.CollectionView!.props.isSelected() ?
- SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined);
+ (this.props.CollectionView.props.isSelected() ?
+ SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e) : undefined);
let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
@@ -106,28 +133,26 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
height={Number(MAX_ROW_HEIGHT)}
GetValue={() => {
let field = props.Document[props.fieldKey];
- if (field) {
- //TODO Types
- // return field.ToScriptString();
- return String(field);
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return false;
}
return applyToDoc(props.Document, script.run);
}}
OnFillDown={async (value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return;
}
const run = script.run;
//TODO This should be able to be refactored to compile the script once
- const val = await DocListCastAsync(this.props.Document[this.props.fieldKey])
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
@@ -178,30 +203,31 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
//toggles preview side-panel of schema
@action
- toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ toggleExpander = () => {
+ this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
}
+ onDividerDown = (e: React.PointerEvent) => {
+ this._startPreviewWidth = this.previewWidth();
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ }
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
+ this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
+ this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent === this.splitPercentage) {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ if (this._startPreviewWidth === this.previewWidth()) {
+ this.toggleExpander();
}
}
- onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this.splitPercentage;
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
- }
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
@@ -228,16 +254,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
csv = csv.substr(0, csv.length - 1) + "\n";
let self = this;
DocListCast(this.props.Document.data).map(doc => {
- csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", "");
+ csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", "");
csv = csv.substr(0, csv.length - 1) + "\n";
- })
+ });
csv.substring(0, csv.length - 1);
let dbName = StrCast(this.props.Document.title);
let res = await Gateway.Instance.PostSchema(csv, dbName);
if (self.props.CollectionView.props.addDocument) {
- let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName });
+ let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
if (schemaDoc) {
- self.props.CollectionView.props.addDocument(schemaDoc, false);
+ //self.props.CollectionView.props.addDocument(schemaDoc, false);
+ self.props.Document.schemaDoc = schemaDoc;
}
}
}
@@ -253,62 +280,16 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
this._newKeyName = e.currentTarget.value;
}
- @observable previewScript: string = "";
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.previewScript = e.currentTarget.value;
- }
-
@computed
get previewDocument(): Doc | undefined {
const children = DocListCast(this.props.Document[this.props.fieldKey]);
const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
- return selected ? (this.previewScript && this.previewScript != "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
- }
- get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); }
- get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; }
- get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; }
-
- private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth);
- private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight);
- private previewContentScaling = () => {
- let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth);
- if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
- return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
- }
- return wscale;
+ return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
}
- private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
- private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling();
- get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
+
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
- - this.borderWidth).scale(1 / this.previewContentScaling())
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
- @computed
- get previewPanel() {
- // let doc = CompileScript(this.previewScript, { this: selected }, true)();
- const previewDoc = this.previewDocument;
- return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}>
- {!previewDoc || !this.previewRegionWidth ? (null) : (
- <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
- toggleMinimized={emptyFunction}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
- ScreenToLocalTransform={this.getPreviewTransform}
- ContentScaling={this.previewContentScaling}
- PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight}
- ContainingCollectionView={this.props.CollectionView}
- focus={emptyFunction}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- bringToFront={emptyFunction}
- />
- </div>)}
- <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} />
- </div>);
- }
get documentKeysCheckList() {
const docs = DocListCast(this.props.Document[this.props.fieldKey]);
@@ -334,7 +315,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div id="schema-options-header"><h5><b>Options</b></h5></div>
<div id="options-flyout-div">
<h6 className="schema-options-subHeader">Preview Window</h6>
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div>
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div>
<h6 className="schema-options-subHeader" >Displayed Columns</h6>
<ul id="schema-col-checklist" >
{this.documentKeysCheckList}
@@ -349,33 +330,128 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
@computed
+ get reactTable() {
+ trace();
+ let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
+ return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
+ columns={this.tableColumns}
+ column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
+ getTrProps={this.getTrProps}
+ />
+ }
+
+ @computed
get dividerDragger() {
- return this.splitPercentage === 0 ? (null) :
+ return this.previewWidth() === 0 ? (null) :
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
+ @computed
+ get previewPanel() {
+ trace();
+ return <CollectionSchemaPreview
+ Document={this.previewDocument}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ }
+ @action
+ setPreviewScript = (script: string) => {
+ this.previewScript = script;
+ }
+
render() {
- library.add(faCog);
- library.add(faPlus);
- const children = this.children;
+ trace();
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
- <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}>
- <ReactTable data={children} page={0} pageSize={children.length} showPagination={false}
- columns={this.columns.map(col => ({
- Header: col,
- accessor: (doc: Doc) => [doc, col],
- id: col
- }))}
- column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
- getTrProps={this.getTrProps}
- />
- </div>
+ {this.reactTable}
{this.dividerDragger}
- {this.previewPanel}
+ {!this.previewWidth() ? (null) : this.previewPanel}
{this.tableOptionsPanel}
</div>
);
}
+}
+interface CollectionSchemaPreviewProps {
+ Document?: Doc;
+ width: () => number;
+ height: () => number;
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ getTransform: () => Transform;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ addDocTab: (document: Doc, where: string) => void;
+ setPreviewScript: (script: string) => void;
+ previewScript?: string;
+}
+
+@observer
+export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
+ private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
+ private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
+ private contentScaling = () => {
+ let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width());
+ if (wscale * this.nativeHeight > this.props.height()) {
+ return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height());
+ }
+ return wscale;
+ }
+ private PanelWidth = () => this.nativeWidth * this.contentScaling();
+ private PanelHeight = () => this.nativeHeight * this.contentScaling();
+ private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling())
+ get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.setPreviewScript(e.currentTarget.value);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+ render() {
+ let input = this.props.previewScript === undefined ? (null) :
+ <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}>
+ {!this.props.Document || !this.props.width ? (null) : (
+ <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}>
+ <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
+ addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getTransform}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+ ContainingCollectionView={this.props.CollectionView}
+ focus={emptyFunction}
+ parentActive={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
+ addDocTab={this.props.addDocTab}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </div>)}
+ {input}
+ </div>);
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
new file mode 100644
index 000000000..af194aec9
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -0,0 +1,41 @@
+@import "../globalCssVariables";
+.collectionStackingView {
+ overflow-y: auto;
+
+ .collectionStackingView-docView-container {
+ width: 45%;
+ margin: 5% 2.5%;
+ padding-left: 2.5%;
+ height: auto;
+ }
+
+ .collectionStackingView-flexCont {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ width:100%;
+ height:100%;
+ position: absolute;
+ display:grid;
+ top: 0;
+ left: 0;
+ width: 100%;
+ position: absolute;
+
+ }
+
+ .collectionStackingView-description {
+ font-size: 100%;
+ margin-bottom: 1vw;
+ padding: 10px;
+ height: 2vw;
+ width: 100%;
+ font-family: $sans-serif;
+ background: $dark-color;
+ color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
new file mode 100644
index 000000000..cad7cd50c
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -0,0 +1,185 @@
+import React = require("react");
+import { action, computed, IReactionDisposer, reaction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { BoolCast, NumCast } from "../../../new_fields/Types";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { CollectionSchemaPreview } from "./CollectionSchemaView";
+import "./CollectionStackingView.scss";
+import { CollectionSubView } from "./CollectionSubView";
+import { ContextMenu } from "../ContextMenu";
+
+@observer
+export class CollectionStackingView extends CollectionSubView(doc => doc) {
+ _masonryGridRef: HTMLDivElement | null = null;
+ _heightDisposer?: IReactionDisposer;
+ _gridSize = 1;
+ @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() - 2 * this.xMargin : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }
+
+ singleColDocHeight(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]();
+ }
+ 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])],
+ () => {
+ if (this.singleColumn) {
+ let children = this.childDocs.filter(d => !d.isMinimized);
+ this.props.Document.height = children.reduce((height, d, i) =>
+ height + this.singleColDocHeight(d) + (i === children.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 => {
+ this.props.removeDocument(doc);
+ addDocument(doc);
+ return true;
+ }
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ 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);
+ }
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDropTarget(ele!);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+
+ @computed
+ get singleColumnChildren() {
+ let children = this.childDocs.filter(d => !d.isMinimized);
+ return children.map((d, i) => {
+ let dref = React.createRef<HTMLDivElement>();
+ let script = undefined;
+ let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
+ let rowHeight = () => this.singleColDocHeight(d);
+ let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
+ return <div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{ width: colWidth(), height: rowHeight(), marginLeft: "auto", marginRight: "auto" }} >
+ <CollectionSchemaPreview
+ Document={d}
+ width={colWidth}
+ height={rowHeight}
+ getTransform={dxf}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}
+ previewScript={script}>
+ </CollectionSchemaPreview>
+ </div>;
+ });
+ }
+ @computed
+ get children() {
+ return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(d, dref.current!);
+ let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this._gridSize + this.gridGap));
+ let childFocus = (doc: Doc) => {
+ doc.libraryBrush = true;
+ this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered.
+ }
+ return (<div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{
+ width: NumCast(d.nativeWidth, d[WidthSym]()),
+ height: NumCast(d.nativeHeight, d[HeightSym]()),
+ transformOrigin: "top left",
+ gridRowEnd: `span ${rowSpan}`,
+ gridColumnEnd: `span 1`,
+ transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})`
+ }} >
+ <DocumentView key={d[Id]} Document={d}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.moveDocument}
+ ContainingCollectionView={this.props.CollectionView}
+ isTopMost={false}
+ ScreenToLocalTransform={dxf}
+ focus={childFocus}
+ ContentScaling={returnOne}
+ PanelWidth={d[WidthSym]}
+ PanelHeight={d[HeightSym]}
+ selectOnLoad={false}
+ parentActive={this.props.active}
+ addDocTab={this.props.addDocTab}
+ bringToFront={emptyFunction}
+ whenActiveChanged={this.props.whenActiveChanged}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </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"
+ });
+ }
+ }
+ render() {
+ let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.childDocs.filter(d => !d.isMinimized).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">
+ <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
+ onContextMenu={this.onContextMenu}
+ ref={this.createRef} onWheel={(e: React.WheelEvent) => e.stopPropagation()}
+ 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.singleColumn ? this.singleColumnChildren : this.children}
+ </div></div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ffd3e0659..762955a08 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,24 +1,24 @@
-import { action, runInAction } from "mobx";
-import React = require("react");
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DragManager } from "../../util/DragManager";
-import { Docs, DocumentOptions } from "../../documents/Documents";
-import { RouteStore } from "../../../server/RouteStore";
+import { action } from "mobx";
+import * as rp from 'request-promise';
+import CursorField from "../../../new_fields/CursorField";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, PromiseValue } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import * as rp from 'request-promise';
-import { CollectionView } from "./CollectionView";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { DocServer } from "../../DocServer";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField";
-import { url } from "inspector";
+import { CollectionView } from "./CollectionView";
+import React = require("react");
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { Id } from "../../../new_fields/FieldSymbols";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -47,7 +47,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.createDropTarget(ele);
}
- get children() {
+ get childDocs() {
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
return DocListCast(this.props.Document[this.props.fieldKey]);
@@ -72,7 +72,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
cursors[ind].setPosition(pos);
} else {
- let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos });
+ let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
cursors.push(entry);
}
}
@@ -82,28 +82,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.dropAction || de.data.userDropAction) {
- ["width", "height", "curPage"].map(key =>
- de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
- PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
- }
let added = false;
if (de.data.dropAction || de.data.userDropAction) {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (de.data.moveDocument) {
- const move = de.data.moveDocument;
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = move(d, this.props.Document, this.props.addDocument);
- return moved || added;
- }, false);
+ let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
+ added = movedDocs.reduce((added: boolean, d) =>
+ de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
} else {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
}
e.stopPropagation();
return added;
@@ -131,7 +118,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
options.dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
- if (path.includes('localhost')) {
+ if (path.includes(window.location.hostname)) {
let s = path.split('/');
let id = s[s.length - 1];
DocServer.GetRefField(id).then(field => {
@@ -155,6 +142,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ if (e.ctrlKey) {
+ e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
+ return;
+ }
let html = e.dataTransfer.getData("text/html");
let text = e.dataTransfer.getData("text/plain");
@@ -164,6 +155,25 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
e.stopPropagation();
e.preventDefault();
+ if (html && FormattedTextBox.IsFragment(html)) {
+ let href = FormattedTextBox.GetHref(html);
+ if (href) {
+ let docid = FormattedTextBox.GetDocFromUrl(href);
+ if (docid) { // prosemirror text containing link to dash document
+ 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
+ (f instanceof Doc) && this.props.addDocument(f, false);
+ }
+ });
+ } else {
+ this.props.addDocument && this.props.addDocument(Docs.WebDocument(href, options));
+ }
+ } else if (text) {
+ this.props.addDocument && this.props.addDocument(Docs.TextDocument({ ...options, documentText: "@@@" + text }), false);
+ }
+ return;
+ }
if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
this.props.addDocument(htmlDoc, false);
@@ -210,7 +220,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName });
+ let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
docPromise.then(doc => doc && this.props.addDocument(doc));
}));
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 411d67ff7..2dc4b2e80 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -19,37 +19,20 @@
padding-left: 20px;
}
- li {
- margin: 5px 0;
- }
-
.no-indent {
padding-left: 0;
}
.bullet {
- float:left;
+ float: left;
position: relative;
width: 15px;
display: block;
color: $intermediate-color;
- margin-top: 3px;
- transform: scale(1.3,1.3);
- }
-
- .docContainer {
- margin-left: 10px;
- display: block;
- // width:100%;//width: max-content;
- }
- .docContainer:hover {
- .treeViewItem-openRight {
- display:inline;
- }
+ margin-top: 8px;
+ transform: scale(1.3, 1.3);
}
-
-
.editableView-container {
font-weight: bold;
}
@@ -61,10 +44,8 @@
// margin-top: 3px;
display: inline;
}
- .treeViewItem-openRight {
- margin-left: 5px;
- display:none;
- }
+
+
.docContainer:hover {
.delete-button {
display: inline;
@@ -73,16 +54,56 @@
}
.coll-title {
- width:max-content;
+ width: max-content;
display: block;
font-size: 24px;
}
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
+
.collectionTreeView-keyHeader {
font-style: italic;
font-size: 8pt;
}
+}
+
+.docContainer {
+ margin-left: 10px;
+ display: block;
+ // width:100%;//width: max-content;
+
+ .treeViewItem-openRight {
+ margin-left: 5px;
+ display: none;
+ }
+}
+#docContainer-data {
+ margin-top: 5px;
+}
+
+.docContainer:hover {
+ .treeViewItem-openRight {
+ display: inline-block;
+ height:13px;
+ // display: inline;
+ svg {
+ display:block;
+ padding:0px;
+ margin: 0px;
+ }
+ }
+}
+
+.treeViewItem-header {
+ border: transparent 1px solid;
+}
+.treeViewItem-header-above {
+ border: transparent 1px solid;
+ border-top: black 1px solid;
+}
+.treeViewItem-header-below {
+ border: transparent 1px solid;
+ border-bottom: black 1px solid;
+}
+.treeViewItem-header-inside {
+ border: transparent 1px solid;
+ border: black 1px solid;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 70c09d97c..8a6764c58 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,25 +1,26 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
+import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { Document, listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, NumCast, StrCast, PromiseValue } from '../../../new_fields/Types';
+import { Docs } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
+import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
import { EditableView } from "../EditableView";
+import { MainView } from '../MainView';
+import { CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from './CollectionDockingView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
-import { Document, listSpec } from '../../../new_fields/Schema';
-import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/RefField';
-import { ContextMenu } from '../ContextMenu';
-import { undoBatch } from '../../util/UndoManager';
-import { Main } from '../Main';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { CollectionDockingView } from './CollectionDockingView';
-import { DocumentManager } from '../../util/DocumentManager';
-import { List } from '../../../new_fields/List';
-import { Docs } from '../../documents/Documents';
+import { Transform } from '../../util/Transform';
+import { SelectionManager } from '../../util/SelectionManager';
export interface TreeViewProps {
@@ -27,6 +28,12 @@ export interface TreeViewProps {
deleteDoc: (doc: Doc) => void;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
+ addDocTab: (doc: Doc, where: string) => void;
+ addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+ ScreenToLocalTransform: () => Transform;
+ treeViewId: string;
+ parentKey: string;
+ active: () => boolean;
}
export enum BulletType {
@@ -45,23 +52,49 @@ library.add(faCaretRight);
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
class TreeView extends React.Component<TreeViewProps> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private treedropDisposer?: DragManager.DragDropDisposer;
+ protected createTreeDropTarget = (ele: HTMLDivElement) => {
+ this.treedropDisposer && this.treedropDisposer();
+ if (ele) {
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } });
+ }
+ }
+ @observable _isOver: boolean = false;
@observable _collapsed: boolean = true;
@undoBatch delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight");
- @undoBatch openRight = async () => {
- if (this.props.document.dockingConfig) {
- Main.Instance.openWorkspace(this.props.document);
- } else {
- CollectionDockingView.Instance.AddRightSplit(this.props.document);
- }
- };
+ @action onMouseEnter = () => { this._isOver = true; }
+ @action onMouseLeave = () => { this._isOver = false; }
- get children() {
- return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this.props.active() && (this.props.document.libraryBrush = true);
+ if (e.buttons === 1) {
+ this._header!.current!.className = "treeViewItem-header";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ this._header!.current!.className = "treeViewItem-header";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ this.props.document.libraryBrush = false;
+ let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible);
+ this._header!.current!.className = "treeViewItem-header"
+ if (inside && this._bulletType != BulletType.List) this._header!.current!.className = "treeViewItem-header-inside";
+ else if (before) this._header!.current!.className = "treeViewItem-header-above";
+ else if (!before) this._header!.current!.className = "treeViewItem-header-below";
+ e.stopPropagation();
}
-
onPointerDown = (e: React.PointerEvent) => {
e.stopPropagation();
}
@@ -69,19 +102,17 @@ class TreeView extends React.Component<TreeViewProps> {
@action
remove = (document: Document, key: string) => {
let children = Cast(this.props.document[key], listSpec(Doc), []);
- if (children) {
- children.splice(children.indexOf(document), 1);
- }
+ children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1);
}
@action
- move: DragManager.MoveFunction = (document, target, addDoc) => {
- if (this.props.document === target) {
- return true;
+ move: DragManager.MoveFunction = (document: Doc, target: Doc, addDoc) => {
+ if (this.props.document !== target) {
+ //TODO This should check if it was removed
+ this.props.deleteDoc(document);
+ return addDoc(document);
}
- //TODO This should check if it was removed
- this.remove(document, "data");
- return addDoc(document);
+ return true;
}
renderBullet(type: BulletType) {
@@ -93,26 +124,16 @@ class TreeView extends React.Component<TreeViewProps> {
}
return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
}
-
- @action
- onMouseEnter = () => {
- this._isOver = true;
- }
- @observable _isOver: boolean = false;
- @action
- onMouseLeave = () => {
- this._isOver = false;
- }
/**
* Renders the EditableView title element for placement into the tree.
*/
renderTitle() {
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
+ let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true);
let editableView = (titleString: string) =>
(<EditableView
oneLine={!this._isOver ? true : false}
- display={"block"}
+ display={"inline"}
contents={titleString}
height={36}
GetValue={() => StrCast(this.props.document.title)}
@@ -122,16 +143,19 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}}
/>);
- let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []);
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
<FontAwesomeIcon icon="angle-right" size="lg" />
- <FontAwesomeIcon icon="angle-right" size="lg" />
+ {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
</div>);
return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
- style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
+ style={{
+ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0",
+ pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
+ }}
+ >
{editableView(StrCast(this.props.document.title))}
{openRight}
{/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
@@ -139,29 +163,64 @@ class TreeView extends React.Component<TreeViewProps> {
}
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) });
- ContextMenu.Instance.addItem({
- description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document,
- { title: this.props.document.title + ".kvp", width: 300, height: 300 }))
- });
- if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ if (!e.isPropagationStopped()) { // 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: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+ if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
+ if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ }
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ } else {
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ }
+ ContextMenu.Instance.displayMenu(e.pageX - 156, e.pageY - 15);
+ e.stopPropagation();
+ }
+ }
+ treeDrop = (e: Event, de: DragManager.DropEvent) => {
+ let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible);
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before);
+ if (inside) {
+ let docList = Cast(this.props.document.data, listSpec(Doc));
+ if (docList !== undefined) {
+ addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }
+ }
+ }
+ let added = false;
+ if (de.data.dropAction || de.data.userDropAction) {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false);
+ } else if (de.data.moveDocument) {
+ let movedDocs = de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments;
+ added = movedDocs.reduce((added: boolean, d) =>
+ de.data.moveDocument(d, this.props.document, addDoc) || added, false);
+ } else {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false);
}
- ContextMenu.Instance.addItem({
- description: "Delete", event: undoBatch(() => {
- this.props.deleteDoc(this.props.document);
- })
- });
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
e.stopPropagation();
+ return added;
}
+ return false;
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
+ public static AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) {
+ let list = Cast(target[key], listSpec(Doc));
+ if (list) {
+ let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+ if (ind === -1) list.push(doc);
+ else list.splice(before ? ind : ind + 1, 0, doc);
+ }
+ return true;
+ }
+ _bulletType: BulletType = BulletType.List;
render() {
let bulletType = BulletType.List;
let contentElement: (JSX.Element | null)[] = [];
@@ -171,68 +230,102 @@ class TreeView extends React.Component<TreeViewProps> {
while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
}
keys.map(key => {
- let docList = DocListCast(this.props.document[key]);
+ let docList = Cast(this.props.document[key], listSpec(Doc));
+ let remDoc = (doc: Doc) => this.remove(doc, key);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, key, doc, addBefore, before);
let doc = Cast(this.props.document[key], Doc);
- if (doc instanceof Doc || docList.length) {
+ if (doc instanceof Doc || docList) {
if (!this._collapsed) {
bulletType = BulletType.Collapsible;
- let spacing = (key === "data") ? 0 : -10;
contentElement.push(<ul key={key + "more"}>
{(key === "data") ? (null) :
<span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
- <div style={{ display: "block", marginTop: `${spacing}px` }}>
- {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)}
+ <div style={{ display: "block" }}>
+ {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, key, addDoc, remDoc, this.move,
+ this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active)}
</div>
</ul >);
- } else
+ } else {
bulletType = BulletType.Collapsed;
+ }
}
});
+ this._bulletType = bulletType;
return <div className="treeViewItem-container"
+ ref={this.createTreeDropTarget}
onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
- {this.renderBullet(bulletType)}
- {this.renderTitle()}
+ <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.renderBullet(bulletType)}
+ {this.renderTitle()}
+ </div>
{contentElement}
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
- return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
- <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ public static GetChildElements(
+ docs: Doc[],
+ treeViewId: string,
+ key: string,
+ add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean,
+ remove: ((doc: Doc) => void),
+ move: DragManager.MoveFunction,
+ dropAction: dropActionType,
+ addDocTab: (doc: Doc, where: string) => void,
+ screenToLocalXf: () => Transform,
+ active: () => boolean
+ ) {
+ return docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized)).map(child =>
+ <TreeView document={child} treeViewId={treeViewId} key={child[Id]} deleteDoc={remove} addDocument={add} moveDocument={move}
+ dropAction={dropAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} parentKey={key} active={active} />);
}
}
@observer
export class CollectionTreeView extends CollectionSubView(Document) {
+ private treedropDisposer?: DragManager.DragDropDisposer;
+ protected createTreeDropTarget = (ele: HTMLDivElement) => {
+ if (this.treedropDisposer) {
+ this.treedropDisposer();
+ }
+ if (ele) {
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
@action
remove = (document: Document) => {
- let children = Cast(this.props.Document.data, listSpec(Doc), []);
- if (children) {
- children.splice(children.indexOf(document), 1);
- }
+ let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1);
}
onContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) });
- }
- if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) {
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) });
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document
+ ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
}
}
+
+ onTreeDrop = (e: React.DragEvent) => {
+ this.onDrop(e, {});
+ }
render() {
- let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
- if (!this.children) {
+ let dropAction = Cast(this.props.Document.dropAction, "string") as dropActionType;
+ if (!this.childDocs) {
return (null);
}
- let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction);
+ let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
+ let childElements = TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.fieldKey, addDoc, this.remove,
+ moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active);
return (
<div id="body" className="collectionTreeView-dropTarget"
style={{ borderRadius: "inherit" }}
onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => e.stopPropagation()}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
+ onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()}
+ onDrop={this.onTreeDrop}
+ ref={this.createTreeDropTarget}>
<div className="coll-title">
<EditableView
contents={this.props.Document.title}
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
index db8b84832..9d2c23d3e 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -5,7 +5,7 @@
position: inherit;
top: 0;
left:0;
-
+ z-index: -1;
}
.collectionVideoView-time{
color : white;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 16121bb1b..7853544d5 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,4 +1,5 @@
import { action, observable, trace } from "mobx";
+import * as htmlToImage from "html-to-image";
import { observer } from "mobx-react";
import { ContextMenu } from "../ContextMenu";
import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
@@ -6,10 +7,14 @@ import React = require("react");
import "./CollectionVideoView.scss";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { emptyFunction } from "../../../Utils";
-import { Id } from "../../../new_fields/RefField";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
-import { NumCast } from "../../../new_fields/Types";
+import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import { SearchBox } from "../SearchBox";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
@observer
@@ -66,8 +71,44 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
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: "VideoOptions", event: emptyFunction });
}
+
+ let field = Cast(this.props.Document[this.props.fieldKey], VideoField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt"
+ });
+ }
+ let width = NumCast(this.props.Document.width);
+ let height = NumCast(this.props.Document.height);
+ ContextMenu.Instance.addItem({
+ description: "Take Snapshot", event: async () => {
+ var canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height);
+
+ //convert to desired file format
+ var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+
+ let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.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-"
+ });
+ this.props.addDocument && this.props.addDocument(imageSummary, false);
+ DocUtils.MakeLink(imageSummary, this.props.Document);
+ }
+ });
+ },
+ icon: "expand-arrows-alt"
+ });
}
setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 55fd2a284..68eefab4c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,15 +1,27 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
+import { observer } from "mobx-react";
import * as React from 'react';
-import { FieldViewProps, FieldView } from '../nodes/FieldView';
-import { CollectionBaseView, CollectionViewType, CollectionRenderProps } from './CollectionBaseView';
-import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
-import { CollectionSchemaView } from './CollectionSchemaView';
-import { CollectionDockingView } from './CollectionDockingView';
-import { CollectionTreeView } from './CollectionTreeView';
-import { ContextMenu } from '../ContextMenu';
+import { Id } from '../../../new_fields/FieldSymbols';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { observer } from 'mobx-react';
import { undoBatch } from '../../util/UndoManager';
-import { Id } from '../../../new_fields/RefField';
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from "./CollectionDockingView";
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionTreeView } from "./CollectionTreeView";
+export const COLLECTION_BORDER_WIDTH = 2;
+
+library.add(faTh);
+library.add(faTree);
+library.add(faSquare);
+library.add(faProjectDiagram);
+library.add(faSignature);
+library.add(faThList);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
@@ -21,6 +33,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />);
case CollectionViewType.Freeform:
default:
return (<CollectionFreeFormView {...props} CollectionView={this} />);
@@ -32,9 +45,15 @@ export class CollectionView extends React.Component<FieldViewProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform) });
- ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema) });
- ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree) });
+ let subItems: ContextMenuProps[] = [];
+ subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" });
+ if (CollectionBaseView.InSafeMode()) {
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" });
+ }
+ subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });
+ subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
+ subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
+ ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
}
}
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
index f3c605f3e..2dd3e49f2 100644
--- a/src/client/views/collections/ParentDocumentSelector.scss
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -1,8 +1,22 @@
.PDS-flyout {
position: absolute;
z-index: 9999;
- background-color: #d3d3d3;
+ background-color: #eeeeee;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
min-width: 150px;
color: black;
+ top: 12px;
+
+ padding: 10px;
+ border-radius: 3px;
+
+ hr {
+ height: 1px;
+ margin: 0px;
+ background-color: gray;
+ border-top: 0px;
+ border-bottom: 0px;
+ border-right: 0px;
+ border-left: 0px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 52f7914f3..f11af04a3 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -3,36 +3,64 @@ import './ParentDocumentSelector.scss';
import { Doc } from "../../../new_fields/Doc";
import { observer } from "mobx-react";
import { observable, action, runInAction } from "mobx";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
+import { NumCast } from "../../../new_fields/Types";
+import { CollectionViewType } from "./CollectionBaseView";
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
@observer
-export class SelectorContextMenu extends React.Component<{ Document: Doc }> {
- @observable private _docs: Doc[] = [];
+export class SelectorContextMenu extends React.Component<SelectorProps> {
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
- constructor(props: { Document: Doc }) {
+ constructor(props: SelectorProps) {
super(props);
this.fetchDocuments();
}
async fetchDocuments() {
+ let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
- runInAction(() => this._docs = docs);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(() => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ });
+ }
+
+ getOnClick({ col, target }: { col: Doc, target: Doc }) {
+ return () => {
+ col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+ if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ this.props.addDocTab(col, "inTab");
+ };
}
render() {
return (
<>
- {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)}
+ <p>Contexts:</p>
+ {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._otherDocs.length ? <hr></hr> : null}
+ {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
</>
);
}
}
@observer
-export class ParentDocSelector extends React.Component<{ Document: Doc }> {
+export class ParentDocSelector extends React.Component<SelectorProps> {
@observable hover = false;
@action
@@ -49,13 +77,13 @@ export class ParentDocSelector extends React.Component<{ Document: Doc }> {
let flyout;
if (this.hover) {
flyout = (
- <div className="PDS-flyout">
- <SelectorContextMenu Document={this.props.Document} />
+ <div className="PDS-flyout" title=" ">
+ <SelectorContextMenu {...this.props} />
</div>
);
}
return (
- <span style={{ position: "relative", display: "inline-block" }}
+ <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}>
<p>^</p>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 737ffba7d..7a0fd2b31 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -10,3 +10,9 @@
transform: translate(10000px,10000px);
pointer-events: all;
}
+.collectionfreeformlinkview-linkText {
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
+ transform: translate(10000px,10000px);
+ pointer-events: all;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 63d2f7642..ba7e6cf9e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,11 +1,10 @@
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { InkingControl } from "../../InkingControl";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { InkingControl } from "../../InkingControl";
export interface CollectionFreeFormLinkViewProps {
A: Doc;
@@ -40,18 +39,25 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
+ let text = "";
+ let first = this.props.LinkDocs[0];
+ if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
+ else text = "-multiple-";
return (
<>
- <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${l.length / 2}` }}
+ <line key="linkLine" className="collectionfreeformlinkview-linkLine"
+ style={{ strokeWidth: `${2 * l.length / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
- <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} />
+ {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
+ <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
+ {text}
+ </text>
</>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index d5ce4e1e7..c4dd534ed 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,17 +1,16 @@
-import { computed, IReactionDisposer, reaction, trace } from "mobx";
+import { computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Utils } from "../../../../Utils";
+import { Doc, DocListCast } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { listSpec } from "../../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
-import { List } from "../../../../new_fields/List";
-import { Id } from "../../../../new_fields/RefField";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -32,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let srcTarg = srcDoc;
let x1 = NumCast(srcDoc.x);
let x2 = NumCast(dstDoc.x);
- let x1w = NumCast(srcDoc.width, -1);
- let x2w = NumCast(dstDoc.width, -1);
+ let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
if (x1w < 0 || x2w < 0 || i === j) { }
else {
let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
@@ -60,12 +59,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
};
}
+ if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
- if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
- else brushAction(dstBrushDocs);
- if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
- else brushAction(srcBrushDocs);
+ brushAction(dstBrushDocs);
+ brushAction(srcBrushDocs);
}
});
});
@@ -100,21 +99,26 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let targetViews = this.documentAnchors(connection.b);
let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
- possiblePairs.map(possiblePair =>
- drawnPairs.reduce((found, drawnPair) => {
- let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b);
+ possiblePairs.map(possiblePair => {
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
+ let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
+ let match = match1 || match2;
if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
return match || found;
- }, false)
- ||
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
- );
+ }, false)) {
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+ }
+ });
return drawnPairs;
}, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ return connections.map(c => {
+ let x = c.l.reduce((p, l) => p + l[Id], "");
+ return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+ });
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 40ec8a325..3193f5624 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,12 +1,13 @@
import { observer } from "mobx-react";
+import * as mobxUtils from 'mobx-utils';
+import CursorField from "../../../../new_fields/CursorField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { Cast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import CursorField from "../../../../new_fields/CursorField";
-import { Cast } from "../../../../new_fields/Types";
-import { listSpec } from "../../../../new_fields/Schema";
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
@@ -21,7 +22,9 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
let cursors = Cast(doc.cursors, listSpec(CursorField));
- return (cursors || []).filter(cursor => cursor.data.metadata.id !== id);
+ const now = mobxUtils.now();
+ // const now = Date.now();
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000);
}
private crosshairs?: HTMLCanvasElement;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 063c9e2cf..e10ba9d7e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -25,6 +25,9 @@
height: 100%;
width: 100%;
}
+ >.jsx-parser {
+ z-index:0;
+ }
//nested freeform views
// .collectionfreeformview-container {
@@ -52,6 +55,10 @@
position: inherit;
height: 100%;
}
+
+ >.jsx-parser {
+ z-index:0;
+ }
.formattedTextBox-cont {
background: $light-color-secondary;
@@ -63,6 +70,7 @@
border-radius: $border-radius;
box-sizing: border-box;
position:absolute;
+ z-index: -1;
.marqueeView {
overflow: hidden;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b8bed795d..63f24ff53 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,8 +1,14 @@
-import { action, computed, trace } from "mobx";
+import { action, computed } from "mobx";
import { observer } from "mobx-react";
-import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnOne } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
+import { HistoryUtil } from "../../../util/History";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
@@ -11,6 +17,7 @@ import { InkingCanvas } from "../../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { pageSchema } from "../../nodes/ImageBox";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -18,13 +25,6 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
-import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
-import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
-import { pageSchema } from "../../nodes/ImageBox";
-import { Id } from "../../../../new_fields/RefField";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { HistoryUtil } from "../../../util/History";
export const panZoomSchema = createSchema({
panX: "number",
@@ -37,7 +37,6 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema)
@observer
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
- public static RIGHT_BTN_DRAG = false;
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
private _lastX: number = 0;
private _lastY: number = 0;
@@ -46,8 +45,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
private panX = () => this.Document.panX || 0;
private panY = () => this.Document.panY || 0;
private zoomScaling = () => this.Document.scale || 1;
@@ -66,13 +65,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return true;
}
private selectDocuments = (docs: Doc[]) => {
- SelectionManager.DeselectAll;
+ SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
SelectionManager.SelectDoc(dv!, true));
}
public getActiveDocuments = () => {
const curPage = FieldValue(this.Document.curPage, -1);
- return this.children.filter(doc => {
+ return this.childDocs.filter(doc => {
var page = NumCast(doc.page, -1);
return page === curPage || page === -1;
});
@@ -90,7 +89,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let y = yp - de.data.yOffset / zoom;
let dropX = NumCast(de.data.droppedDocuments[0].x);
let dropY = NumCast(de.data.droppedDocuments[0].y);
- de.data.droppedDocuments.map(d => {
+ de.data.droppedDocuments.forEach(d => {
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
if (!NumCast(d.width)) {
@@ -103,7 +102,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this.bringToFront(d);
});
- SelectionManager.ReselectAll();
}
return true;
}
@@ -112,11 +110,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
- if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
- (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
- (e.button === 0 && e.altKey)) && this.props.active())) ||
- (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
- ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) {
+ if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -134,20 +128,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
- let x = this.props.Document.panX || 0;
- let y = this.props.Document.panY || 0;
- let docs = this.children || [];
+ let x = this.Document.panX || 0;
+ let y = this.Document.panY || 0;
+ let docs = this.childDocs || [];
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
let minx = docs.length ? NumCast(docs[0].x) : 0;
- let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+ let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx;
let miny = docs.length ? NumCast(docs[0].y) : 0;
- let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
let x = NumCast(doc.x);
- let xe = x + NumCast(doc.width);
+ let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1);
let y = NumCast(doc.y);
- let ye = y + NumCast(doc.height);
+ let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
@@ -160,12 +154,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
});
}
- let panelwidth = this._pwidth / this.zoomScaling() / 2;
- let panelheight = this._pheight / this.zoomScaling() / 2;
- if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx;
- if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx;
- if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy;
- if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;
+ let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(),
+ this._pheight / this.zoomScaling());
+ let panelwidth = panelDim[0];
+ let panelheight = panelDim[1];
+ if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -180,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// if (!this.props.active()) {
// return;
// }
- let childSelected = this.children.some(doc => {
+ let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
@@ -211,7 +207,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY);
let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
- let safeScale = Math.abs(localTransform.Scale);
+ let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
this.props.Document.scale = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
e.stopPropagation();
@@ -220,7 +216,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
- this.panDisposer && clearTimeout(this.panDisposer);
+
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
@@ -239,7 +235,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
bringToFront = (doc: Doc) => {
- const docs = this.children;
+ const docs = this.childDocs;
docs.slice().sort((doc1, doc2) => {
if (doc1 === doc) return 1;
if (doc2 === doc) return -1;
@@ -248,7 +244,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
doc.zIndex = docs.length + 1;
}
- panDisposer?: NodeJS.Timeout;
focusDocument = (doc: Doc) => {
const panX = this.Document.panX;
const panY = this.Document.panY;
@@ -270,22 +265,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
SelectionManager.DeselectAll();
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2;
const newState = HistoryUtil.getState();
newState.initializers[id] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
this.setPan(newPanX, newPanY);
this.props.Document.panTransformType = "Ease";
this.props.focus(this.props.Document);
- this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false
}
getDocumentViewProps(document: Doc): DocumentViewProps {
return {
Document: document,
- toggleMinimized: emptyFunction,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
@@ -300,16 +293,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
parentActive: this.props.active,
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
};
}
@computed.struct
get views() {
let curPage = FieldValue(this.Document.curPage, -1);
- let docviews = this.children.reduce((prev, doc) => {
+ let docviews = this.childDocs.reduce((prev, doc) => {
if (!(doc instanceof Doc)) return prev;
var page = NumCast(doc.page, -1);
- if (page === curPage || page === -1) {
+ if (Math.round(page) === Math.round(curPage) || page === -1) {
let minim = BoolCast(doc.isMinimized, false);
if (minim === undefined || !minim) {
prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
@@ -328,7 +322,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
+ private childViews = () => [
+ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
+ ...this.views
+ ];
render() {
const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
@@ -350,7 +347,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+ <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 865bae729..c699b3437 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,25 +1,25 @@
import * as htmlToImage from "html-to-image";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
+import { SearchBox } from "../../SearchBox";
+import { Templates } from "../../Templates";
+import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
-import { Utils } from "../../../../Utils";
-import { Doc } from "../../../../new_fields/Doc";
-import { NumCast, Cast } from "../../../../new_fields/Types";
-import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { List } from "../../../../new_fields/List";
-import { ImageField } from "../../../../new_fields/URLField";
-import { Template, Templates } from "../../Templates";
-import { Gateway } from "../../../northstar/manager/Gateway";
-import { DocServer } from "../../../DocServer";
-import { Id } from "../../../../new_fields/RefField";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -63,7 +63,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.preventDefault();
(async () => {
let text: string = await navigator.clipboard.readText();
- let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != "");
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
for (let i = 0; i < ns.length - 1; i++) {
while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
@@ -80,68 +80,83 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
this.props.addDocument(newBox, false);
y += 40 * this.props.getTransform().Scale;
- })
+ });
})();
} else if (e.key === "b" && e.ctrlKey) {
- //heuristically converts pasted text into a table.
- // assumes each entry is separated by a tab
- // skips all rows until it gets to a row with more than one entry
- // assumes that 1st row has header entry for each column
- // assumes subsequent rows have entries for each column header OR
- // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
- // assumes each cell is a string or a number
e.preventDefault();
- (async () => {
- let text: string = await navigator.clipboard.readText();
- let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != "");
- while (ns.length > 0 && ns[0].split("\t").length < 2)
- ns.splice(0, 1);
- if (ns.length > 0) {
- let columns = ns[0].split("\t");
- let docList: Doc[] = [];
- let groupAttr: string | number = "";
- for (let i = 1; i < ns.length - 1; i++) {
- let values = ns[i].split("\t");
- if (values.length === 1 && columns.length > 1) {
- groupAttr = values[0];
- continue;
- }
- let doc = new Doc();
- columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
- if (groupAttr) {
- doc["_group"] = groupAttr;
- }
- doc.title = i.toString();
- docList.push(doc);
- }
- let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
-
- this.props.addDocument(newCol, false);
+ navigator.clipboard.readText().then(text => {
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ if (ns.length === 1 && text.startsWith("http")) {
+ this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ } else {
+ this.pasteTable(ns, x, y);
}
- })();
- } else {
+ });
+ } else if (!e.ctrlKey) {
let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
this.props.addLiveTextDocument(newBox);
}
e.stopPropagation();
}
+ //heuristically converts pasted text into a table.
+ // assumes each entry is separated by a tab
+ // skips all rows until it gets to a row with more than one entry
+ // assumes that 1st row has header entry for each column
+ // assumes subsequent rows have entries for each column header OR
+ // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
+ // assumes each cell is a string or a number
+ pasteTable(ns: string[], x: number, y: number) {
+ while (ns.length > 0 && ns[0].split("\t").length < 2) {
+ ns.splice(0, 1);
+ }
+ if (ns.length > 0) {
+ let columns = ns[0].split("\t");
+ let docList: Doc[] = [];
+ let groupAttr: string | number = "";
+ let rowProto = new Doc();
+ rowProto.title = rowProto.Id;
+ rowProto.width = 200;
+ rowProto.isPrototype = true;
+ for (let i = 1; i < ns.length - 1; i++) {
+ let values = ns[i].split("\t");
+ if (values.length === 1 && columns.length > 1) {
+ groupAttr = values[0];
+ continue;
+ }
+ let docDataProto = Doc.MakeDelegate(rowProto);
+ docDataProto.isPrototype = true;
+ columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
+ if (groupAttr) {
+ docDataProto._group = groupAttr;
+ }
+ docDataProto.title = i.toString();
+ let doc = Doc.MakeDelegate(docDataProto);
+ doc.width = 200;
+ docList.push(doc);
+ }
+ let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+
+ this.props.addDocument(newCol, false);
+ }
+ }
@action
onPointerDown = (e: React.PointerEvent): void => {
this._downX = this._lastX = e.pageX;
this._downY = this._lastY = e.pageY;
this._commandExecuted = false;
PreviewCursor.Visible = false;
- if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) ||
- (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) {
+ if (e.button === 2 || (e.button === 0 && e.altKey)) {
+ if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
document.addEventListener("pointermove", this.onPointerMove, true);
document.addEventListener("pointerup", this.onPointerUp, true);
document.addEventListener("keydown", this.marqueeCommand, true);
- // bcz: do we need this? it kills the context menu on the main collection
+ if (e.altKey) {
+ //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
+ e.preventDefault();
+ }
+ // bcz: do we need this? it kills the context menu on the main collection if !altKey
// e.stopPropagation();
}
- if (e.altKey) {
- e.preventDefault();
- }
}
@action
@@ -223,27 +238,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "r" || e.key === "s" || e.key === "e" || e.key === "p") {
+ if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") {
this._commandExecuted = true;
e.stopPropagation();
+ e.preventDefault();
(e as any).propagationIsStopped = true;
let bounds = this.Bounds;
- let selected = this.marqueeSelect().map(d => {
- if (e.key === "s") {
- let dCopy = Doc.MakeCopy(d);
- dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2;
- dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- dCopy.page = -1;
- return dCopy;
- }
- else if (e.key !== "r") {
+ let selected = this.marqueeSelect();
+ if (e.key === "c") {
+ selected.map(d => {
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
d.page = -1;
- }
- return d;
- });
+ return d;
+ });
+ }
let ink = Cast(this.props.container.props.Document.ink, InkField);
let inkData = ink ? ink.inkData : undefined;
let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
@@ -253,42 +263,68 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
panX: 0,
panY: 0,
borderRounding: e.key === "e" ? -1 : undefined,
+ backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
scale: zoomBasis,
width: bounds.width * zoomBasis,
height: bounds.height * zoomBasis,
ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
- title: e.key === "s" ? "-summary-" : e.key === "r" ? "-replacement-" : e.key === "p" ? "-summary-" : "a nested collection",
+ title: e.key === "s" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
});
-
+ newCollection.zoomBasis = zoomBasis;
this.marqueeInkDelete(inkData);
- // SelectionManager.DeselectAll();
- if (e.key === "s" || e.key === "r" || e.key === "p") {
- e.preventDefault();
- let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
-
- let dataUrl = await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width, height: bounds.height, quality: 1 });
- summary.proto!.thumbnail = new ImageField(new URL(dataUrl));
- summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]);
- if (e.key === "s" || e.key === "p") {
- summary.proto!.maximizeOnRight = true;
+ if (e.key === "s") {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ summary.proto!.subBulletDocs = new List<Doc>(selected);
+ //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ summary.templates = new List<string>([Templates.Bullet.Layout]);
+ let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+ container.viewType = CollectionViewType.Stacking;
+ this.props.addLiveTextDocument(container);
+ // });
+ } else if (e.key === "S") {
+ await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.ImageDocument(url, {
+ x: bounds.left, y: bounds.top + 100 / zoomBasis,
+ width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
+ });
+ summary.imageSummary = imageSummary;
+ this.props.addDocument(imageSummary, false);
+ }
+ })
newCollection.proto!.summaryDoc = summary;
selected = [newCollection];
- }
- summary.proto!.summarizedDocs = new List<Doc>(selected);
- //summary.proto!.isButton = true;
- selected.map(summarizedDoc => {
- let maxx = NumCast(summarizedDoc.x, undefined);
- let maxy = NumCast(summarizedDoc.y, undefined);
- let maxw = NumCast(summarizedDoc.width, undefined);
- let maxh = NumCast(summarizedDoc.height, undefined);
+ newCollection.x = bounds.left + bounds.width;
+ //this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
+ summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+
+ this.props.addLiveTextDocument(summary);
});
- this.props.addLiveTextDocument(summary);
}
else {
this.props.addDocument(newCollection, false);
- SelectionManager.DeselectAll();
this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
@@ -353,10 +389,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
render() {
- let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+ let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
<div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
- {!this._visible ? null : this.marqueeDiv}
+ {this._visible ? this.marqueeDiv : null}
<div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
{this.props.children}
</div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 631bf1ba8..499b83c0f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,26 +1,24 @@
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, action } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { OmitKeys, Utils } from "../../../Utils";
-import { DocumentManager } from "../../util/DocumentManager";
-import { SelectionManager } from "../../util/SelectionManager";
+import { OmitKeys } from "../../../Utils";
import { Transform } from "../../util/Transform";
-import { UndoManager } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
+import { UndoManager } from "../../util/UndoManager";
+import { SelectionManager } from "../../util/SelectionManager";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
const schema = createSchema({
zoomBasis: "number",
- zIndex: "number"
+ zIndex: "number",
});
//TODO Types: The import order is wrong, so positionSchema is undefined
@@ -30,8 +28,6 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
private _mainCont = React.createRef<HTMLDivElement>();
- private _downX: number = 0;
- private _downY: number = 0;
_bringToFrontDisposer?: IReactionDisposer;
@computed get transform() {
@@ -43,9 +39,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get width(): number { return FieldValue(this.Document.width, 0); }
- @computed get height(): number { return FieldValue(this.Document.height, 0); }
- @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
this.Document.width = w;
@@ -59,14 +54,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
- set zIndex(h: number) {
- this.Document.zIndex = h;
- }
-
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
- toggleMinimized = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs));
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling()).scale(1 / this.zoom)
@@ -74,11 +64,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@computed
get docView() {
return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
- toggleMinimized={this.toggleMinimized}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ collapseToPoint={this.collapseToPoint}
/>;
}
@@ -87,7 +77,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.props.bringToFront(this.props.Document);
if (values instanceof List) {
let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
- this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false);
+ this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
+ this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
}
}, { fireImmediately: true });
}
@@ -96,68 +87,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
if (this._bringToFrontDisposer) this._bringToFrontDisposer();
}
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
- if (first) {
- if (maximizing) target.width = target.height = 1;
- }
- setTimeout(() => {
- let now = Date.now();
- let progress = Math.min(1, (now - stime) / 200);
- let pval = maximizing ?
- [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
- [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
- target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- target.x = pval[0];
- target.y = pval[1];
- if (now < stime + 200) {
- this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing);
- }
- else {
- if (!maximizing) {
- target.isMinimized = true;
- target.x = targ[0];
- target.y = targ[1];
- target.width = width;
- target.height = height;
- }
- target.isIconAnimating = undefined;
- }
- },
- 2);
- }
+ static _undoBatch?: UndoManager.Batch = undefined;
@action
- public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => {
+ public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
SelectionManager.DeselectAll();
- let isMinimized: boolean | undefined;
- let minimizedDoc: Doc | undefined = this.props.Document;
- if (!maximizedDocs) {
- minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
- if (minimizedDoc) maximizedDocs = await DocListCastAsync(minimizedDoc.maximizedDocs);
- }
- if (minimizedDoc && maximizedDocs) {
- let minimizedTarget = minimizedDoc;
+ if (expandedDocs) {
if (!CollectionFreeFormDocumentView._undoBatch) {
CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
}
- maximizedDocs.map(maximizedDoc => {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
- if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
+ if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
if (isMinimized === undefined) {
isMinimized = BoolCast(maximizedDoc.isMinimized, false);
}
- let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
- let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
- maxw !== undefined && maxh !== undefined) {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
- if (isMinimized) maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
- }
+ maximizedDoc.willMaximize = isMinimized;
+ maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
}
});
setTimeout(() => {
@@ -166,66 +113,39 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}, 500);
}
}
- static _undoBatch?: UndoManager.Batch = undefined;
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- if (e.button === 0 && e.altKey) {
- e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started
- } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document
- }
- onClick = async (e: React.MouseEvent) => {
- e.stopPropagation();
- let altKey = e.altKey;
- if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- let isExpander = (e.target as any).id === "isExpander";
- if (BoolCast(this.props.Document.isButton, false) || isExpander) {
- let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
- let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
- let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
- let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
- let expandedDocs: Doc[] = [];
- expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
- expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
- expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
- // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []),
- // ...(maximizedDocs ? maximizedDocs : []),
- // ...(summarizedDocs ? summarizedDocs : []),];
- if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
- let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0].proto!);
- if (!hasView && ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight))) {
- let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
- if (dataDocs) {
- SelectionManager.DeselectAll();
- expandedDocs.forEach(maxDoc => {
- maxDoc.isMinimized = false;
- if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) {
- CollectionDockingView.Instance.AddRightSplit(maxDoc.proto ? Doc.MakeDelegate(maxDoc.proto) : maxDoc);
- }
- });
- }
- } else {
- this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false));
- this.toggleIcon(expandedDocs);
- }
- }
- else if (linkedToDocs.length || linkedFromDocs.length) {
- let linkedFwdDocs = [
- linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
- linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
- if (linkedFwdDocs) {
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0]);
- }
+
+ animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
+
+ setTimeout(() => {
+ let now = Date.now();
+ let progress = Math.min(1, (now - stime) / 200);
+ let pval = maximizing ?
+ [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
+ [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
+ this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ this.props.Document.x = pval[0];
+ this.props.Document.y = pval[1];
+ if (first) {
+ this.props.Document.proto!.willMaximize = false;
+ }
+ if (now < stime + 200) {
+ this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
+ }
+ else {
+ if (!maximizing) {
+ this.props.Document.proto!.isMinimized = true;
+ this.props.Document.x = targ[0];
+ this.props.Document.y = targ[1];
+ this.props.Document.width = width;
+ this.props.Document.height = height;
}
+ this.props.Document.proto!.isIconAnimating = undefined;
}
- }
+ },
+ 2);
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
- onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
-
borderRounding = () => {
let br = NumCast(this.props.Document.borderRounding);
return br >= 0 ? br :
@@ -238,27 +158,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
let zoomFade = 1;
//var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1);
- let transform = this.getTransform().scale(this.contentScaling()).inverse();
- var [sptX, sptY] = transform.transformPoint(0, 0);
- let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
- let w = bptX - sptX;
+ // let transform = this.getTransform().scale(this.contentScaling()).inverse();
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
+ // let w = bptX - sptX;
//zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1;
const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
let fadeUp = .75 * screenWidth;
let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
- zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
+ // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
return (
<div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
- onPointerDown={this.onPointerDown}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter}
- onClick={this.onClick}
style={{
- outlineColor: "black",
- outlineStyle: "dashed",
- outlineWidth:
- BoolCast(this.props.Document.protoBrush, false) ? `${1 / this.contentScaling()}px` :
- BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px",
opacity: zoomFade,
borderRadius: `${this.borderRounding()}px`,
transformOrigin: "left top",
@@ -267,7 +179,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
width: this.width,
height: this.height,
position: "absolute",
- zIndex: this.zIndex,
+ zIndex: this.Document.zIndex || 0,
backgroundColor: "transparent"
}} >
{this.docView}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f3d76c49b..02396c3af 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -21,7 +21,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -44,12 +44,19 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
layoutKey: string,
+ hideOnLeave?: boolean
}> {
@computed get layout(): string {
- return StrCast(this.props.Document[this.props.layoutKey],
- this.props.Document.data ?
+ const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ if (layout === undefined) {
+ return this.props.Document.data ?
"<FieldView {...props} fieldKey='data' />" :
- KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""));
+ KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ } else if (typeof layout === "string") {
+ return layout;
+ } else {
+ return "<p>Loading layout</p>";
+ }
}
CreateBindings(): JsxBindings {
@@ -76,12 +83,17 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
(this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
this.templates.forEach(template => {
let self = this;
+ // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller
+ // than the width/height of the containing document
function convertConstantsToNative(match: string, offset: number, x: string) {
let px = Number(match.replace("px", ""));
- return `${px * self.props.ScreenToLocalTransform().Scale}px`;
+ return `${Math.min(NumCast(self.props.Document.height, 0),
+ Math.min(NumCast(self.props.Document.width, 0),
+ px * self.props.ScreenToLocalTransform().Scale))}px`;
}
- let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
- layout = nativizedTemplate.replace("{layout}", base);
+ // let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
+ // layout = nativizedTemplate.replace("{layout}", base);
+ layout = template.replace("{layout}", base);
base = layout;
});
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 260630216..19f5c7d36 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,9 +1,19 @@
-import { action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction, trace, observable } from "mobx";
import { observer } from "mobx-react";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, Utils } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { SearchUtil } from "../../util/SearchUtil";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -12,21 +22,30 @@ import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
+import { DocComponent } from "../DocComponent";
+import { PresentationView } from "../PresentationView";
import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { Opt, Doc, WidthSym, HeightSym, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { DocServer } from "../../DocServer";
-import { Id } from "../../../new_fields/RefField";
-import { PresentationView } from "../PresentationView";
-import { SearchUtil } from "../../util/SearchUtil";
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
+import { ContextMenuProps } from '../ContextMenuItem';
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+
+library.add(faTrash);
+library.add(faExpandArrowsAlt);
+library.add(faCompressArrowsAlt);
+library.add(faLayerGroup);
+library.add(faAlignCenter);
+library.add(faCaretSquareRight);
+library.add(faSquare);
+library.add(faConciergeBell);
+library.add(faFolder);
+library.add(faMapPin);
+library.add(faLink);
+library.add(faFingerprint);
+library.add(faCrosshairs);
+library.add(faDesktop);
const linkSchema = createSchema({
title: "string",
@@ -42,27 +61,28 @@ const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
- addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
- removeDocument?: (doc: Document) => boolean;
- moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (doc: Doc) => boolean;
+ moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Document) => void;
+ focus: (doc: Doc) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- toggleMinimized: () => void;
bringToFront: (doc: Doc) => void;
+ addDocTab: (doc: Doc, where: string) => void;
+ collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
}
const schema = createSchema({
layout: "string",
nativeWidth: "number",
nativeHeight: "number",
- backgroundColor: "string"
+ backgroundColor: "string",
});
export const positionSchema = createSchema({
@@ -84,6 +104,9 @@ const Document = makeInterface(schema);
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
+ private _lastTap: number = 0;
+ private _doubleTap = false;
+ private _hitExpander = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -100,6 +123,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ constructor(props: DocumentViewProps) {
+ super(props);
+ this.selectOnLoad = props.selectOnLoad;
+ }
+
+
_reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
@@ -109,7 +138,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
// bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
- this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+ this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title),
+ this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
() => {
let maxDoc = DocListCast(this.props.Document.maximizedDocs);
if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
@@ -162,31 +192,100 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
}
+ toggleMinimized = async () => {
+ let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) {
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(
+ NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ }
+ }
- onClick = (e: React.MouseEvent): void => {
- if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+ onClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ let altKey = e.altKey;
+ let ctrlKey = e.ctrlKey;
+ if (this._doubleTap && !this.props.isTopMost) {
+ this.props.addDocTab(this.props.Document, "inTab");
+ SelectionManager.DeselectAll();
+ this.props.Document.libraryBrush = false;
+ }
+ else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
SelectionManager.SelectDoc(this, e.ctrlKey);
+ let isExpander = (e.target as any).id === "isExpander";
+ if (BoolCast(this.props.Document.isButton, false) || isExpander) {
+ SelectionManager.DeselectAll();
+ let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
+ let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+ let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
+ let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let expandedDocs: Doc[] = [];
+ expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+ // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
+ if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));
+ let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
+ let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
+ if (altKey) {
+ maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace");
+ if (!maxLocation || maxLocation === "inPlace") {
+ let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);
+ expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
+ let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ if (!hasView) {
+ this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
+ }
+ expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
+ }
+ }
+ if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
+ let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ expandedDocs.forEach(maxDoc =>
+ (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
+ this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+ }
+ } else {
+ let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
+ }
+ }
+ else if (linkedToDocs.length || linkedFromDocs.length) {
+ let linkedFwdDocs = [
+ linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
+ linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+
+ let linkedFwdContextDocs = [
+ linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined,
+ linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined];
+
+ let linkedFwdPage = [
+ linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
+ linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+
+ if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+ let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], linkedFwdContextDocs[altKey ? 1 : 0]);
+ }
+ }
+ }
}
}
- _hitExpander = false;
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
- if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) {
- return;
- }
this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
- if (e.shiftKey && e.buttons === 1) {
- if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander);
- } else if (this.props.Document) {
- CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
- }
+ if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
e.stopPropagation();
- } else if (this.active) {
- //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc
+ } else {
+ if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -194,11 +293,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble) {
+ if (!e.cancelBubble && this.active) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) {
+ if (!e.altKey && !this.topMost && e.buttons === 1) {
this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
}
}
@@ -209,29 +308,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ this._lastTap = Date.now();
}
- deleteClicked = (): void => {
- this.props.removeDocument && this.props.removeDocument(this.props.Document);
- }
- fieldsClicked = (e: React.MouseEvent): void => {
- let kvp = Docs.KVPDocument(this.props.Document, { title: this.props.Document.title + ".kvp", width: 300, height: 300 });
- CollectionDockingView.Instance.AddRightSplit(kvp);
- }
- makeButton = (e: React.MouseEvent): void => {
- let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+ fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+ makeBtnClicked = (): void => {
+ let doc = Doc.GetProto(this.props.Document);
doc.isButton = !BoolCast(doc.isButton, false);
- if (doc.isButton && !doc.nativeWidth) {
- doc.nativeWidth = this.props.Document[WidthSym]();
- doc.nativeHeight = this.props.Document[HeightSym]();
+ if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally
+ if (doc.isButton && !doc.nativeWidth) {
+ doc.nativeWidth = this.props.Document[WidthSym]();
+ doc.nativeHeight = this.props.Document[HeightSym]();
+ } else {
+ doc.nativeWidth = doc.nativeHeight = undefined;
+ }
}
}
- fullScreenClicked = (e: React.MouseEvent): void => {
- const doc = Doc.MakeDelegate(FieldValue(this.Document.proto));
- if (doc) {
- CollectionDockingView.Instance.OpenFullScreen(doc);
- }
- ContextMenu.Instance.clearItems();
+ fullScreenClicked = (): void => {
+ CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
SelectionManager.DeselectAll();
}
@@ -242,20 +338,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
- const protoDest = destDoc.proto;
- const protoSrc = sourceDoc.proto;
- if (de.mods == "Control") {
+ e.stopPropagation();
+ if (de.mods === "AltKey") {
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
let src = protoSrc ? protoSrc : sourceDoc;
let dst = protoDest ? protoDest : destDoc;
- dst.data = src;
+ dst.data = (src.data! as ObjectField)[Copy]();
dst.nativeWidth = src.nativeWidth;
dst.nativeHeight = src.nativeHeight;
}
else {
- Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
+ // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
+ DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
de.data.droppedDocuments.push(destDoc);
}
- e.stopPropagation();
}
}
@@ -288,6 +386,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.templates = this.templates;
}
+ freezeNativeDimensions = (e: React.MouseEvent): void => {
+ let proto = Doc.GetProto(this.props.Document);
+ if (proto.ignoreAspect === undefined && !proto.nativeWidth) {
+ proto.nativeWidth = this.props.PanelWidth();
+ proto.nativeHeight = this.props.PanelHeight();
+ proto.ignoreAspect = true;
+ }
+ proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false);
+ }
+
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
@@ -299,21 +407,27 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.preventDefault();
const cm = ContextMenu.Instance;
- cm.addItem({ description: "Full Screen", event: this.fullScreenClicked });
- cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton });
- cm.addItem({ description: "Fields", event: this.fieldsClicked });
- cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) });
- cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) });
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
+ subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
+ cm.addItem({ description: "Open...", subitems: subitems });
+ cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
+ cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
cm.addItem({
description: "Find aliases", event: async () => {
const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
- CollectionDockingView.Instance.AddRightSplit(Docs.SchemaDocument(["title"], aliases, {}));
- }
+ this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
+ }, icon: "search"
});
- cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])) });
- cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) });
- cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) });
- cm.addItem({ description: "Delete", event: this.deleteClicked });
+ cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
+ 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" });
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
@@ -324,22 +438,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
+
isSelected = () => SelectionManager.IsSelected(this);
- select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
+ @action select = (ctrlPressed: boolean) => { this.selectOnLoad = false; SelectionManager.SelectDoc(this, ctrlPressed); }
+ @observable selectOnLoad: boolean = false;
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
- @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); }
+ @computed get contents() {
+ return (
+ <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.selectOnLoad} layoutKey={"layout"} />);
+ }
render() {
var scaling = this.props.ContentScaling();
- var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto");
+ var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
-
return (
<div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
+ BoolCast(this.props.Document.protoBrush, false) ?
+ `${1 * this.props.ScreenToLocalTransform().Scale}px`
+ : "0px",
borderRadius: "inherit",
background: this.Document.backgroundColor || "",
width: nativeWidth,
@@ -347,6 +473,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
transform: `scale(${scaling}, ${scaling})`
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
{this.contents}
</div>
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 34b6c5e70..5a83de8e3 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,24 +1,23 @@
import React = require("react");
+import { computed } from "mobx";
import { observer } from "mobx-react";
-import { computed, observable } from "mobx";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
-import { VideoBox } from "./VideoBox";
-import { AudioBox } from "./AudioBox";
-import { DocumentContentsView } from "./DocumentContentsView";
+import { DateField } from "../../../new_fields/DateField";
+import { Doc, FieldResult, Opt } from "../../../new_fields/Doc";
+import { IconField } from "../../../new_fields/IconField";
+import { List } from "../../../new_fields/List";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
+import { emptyFunction, returnFalse, returnOne } from "../../../Utils";
import { Transform } from "../../util/Transform";
-import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
-import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import { AudioBox } from "./AudioBox";
+import { DocumentContentsView } from "./DocumentContentsView";
+import { FormattedTextBox } from "./FormattedTextBox";
import { IconBox } from "./IconBox";
-import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
-import { IconField } from "../../../new_fields/IconField";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { DateField } from "../../../new_fields/DateField";
-import { NumCast } from "../../../new_fields/Types";
+import { ImageBox } from "./ImageBox";
+import { VideoBox } from "./VideoBox";
//
@@ -35,6 +34,7 @@ export interface FieldViewProps {
isTopMost: boolean;
selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocTab: (document: Doc, where: string) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -87,6 +87,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return (
<DocumentContentsView Document={field}
addDocument={undefined}
+ addDocTab={this.props.addDocTab}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
@@ -100,7 +101,6 @@ export class FieldView extends React.Component<FieldViewProps> {
layoutKey={"layout"}
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
- toggleMinimized={emptyFunction}
whenActiveChanged={this.props.whenActiveChanged}
bringToFront={emptyFunction} />
);
@@ -115,7 +115,7 @@ export class FieldView extends React.Component<FieldViewProps> {
// return <WebBox {...this.props} />
// }
else if (!(field instanceof Promise)) {
- return <p>{JSON.stringify(field)}</p>;
+ return <p>{field.toString()}</p>;
}
else {
return <p> {"Waiting for server..."} </p>;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c2aed652d..7dcacd19b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,3 +1,5 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
@@ -5,11 +7,12 @@ import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { Doc, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
import { RichTextField } from "../../../new_fields/RichTextField";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { DocServer } from "../../DocServer";
+import { DocUtils, Docs } from '../../documents/Documents';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
@@ -19,15 +22,17 @@ import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { ContextMenu } from "../../views/ContextMenu";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { View } from "@react-pdf/renderer";
-import { NodeType } from "prosemirror-model";
+import { Id } from '../../../new_fields/FieldSymbols';
+import { MainOverlayTextBox } from '../MainOverlayTextBox';
+import { NodeType } from 'prosemirror-model';
+
+library.add(faEdit);
+library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
@@ -46,8 +51,11 @@ import { NodeType } from "prosemirror-model";
// this will edit the document and assign the new value to that field.
//]
-export interface FormattedTextBoxOverlay {
+export interface FormattedTextBoxProps {
isOverlay?: boolean;
+ hideOnLeave?: boolean;
+ height?: string;
+ color?: string;
}
const richTextSchema = createSchema({
@@ -58,36 +66,58 @@ type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
@observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
- private _proseRef: React.RefObject<HTMLDivElement>;
+ private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private _gotDown: boolean = false;
- private _dropDisposer?: DragManager.DragDropDisposer;
+ private _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _lastState: any = undefined;
+ private _applyingChange: boolean = false;
+ private _linkClicked = "";
private _reactionDisposer: Opt<IReactionDisposer>;
private _inputReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ private dropDisposer?: DragManager.DragDropDisposer;
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+ @observable _entered = false;
@observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static InputBoxOverlayScroll: number = 0;
+ public static IsFragment(html: string) {
+ return html.indexOf("data-pm-slice") !== -1;
+ }
+ public static GetHref(html: string): string {
+ let parser = new DOMParser();
+ let parsedHtml = parser.parseFromString(html, 'text/html');
+ if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
+ (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+ return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
+ }
+ return "";
+ }
+ public static GetDocFromUrl(url: string) {
+ if (url.startsWith(document.location.origin)) {
+ let start = url.indexOf(window.location.origin);
+ let path = url.substr(start, url.length - start);
+ let docid = path.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ return docid;
+ }
+ return "";
+ }
constructor(props: FieldViewProps) {
super(props);
this._ref = React.createRef();
- this._proseRef = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
}
- _applyingChange: boolean = false;
- _lastState: any = undefined;
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._lastState = this._editorView.state.apply(tx);
@@ -106,33 +136,30 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._proseRef = ele;
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
// We're dealing with a link to a document
- if (de.data instanceof DragManager.LinkDragData) {
- let sourceDoc = de.data.linkSourceDocument;
- let destDoc = this.props.Document;
-
- const protoDest = destDoc.proto;
- const protoSrc = sourceDoc.proto;
- Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
- de.data.droppedDocuments.push(destDoc);
- e.stopPropagation();
- } else if (de.data instanceof DragManager.EmbedDragData) {
+ if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
// We're dealing with an internal document drop
let url = de.data.urlField.url.href;
let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
+ e.stopPropagation();
}
}
componentDidMount() {
- if (this._ref.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
- handlers: { drop: this.drop.bind(this) }
- });
- }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -160,7 +187,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._editorView) {
this._editorView.destroy();
}
- this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
+ this.setupEditor(config, // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
+ MainOverlayTextBox.Instance.TextDoc ? MainOverlayTextBox.Instance.TextDoc : this.props.Document,
+ MainOverlayTextBox.Instance.TextFieldKey ? MainOverlayTextBox.Instance.TextFieldKey : this.props.fieldKey);
}
);
} else {
@@ -182,27 +211,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
field => field && this._editorView && !this._applyingChange &&
this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
);
- this.setupEditor(config, this.props.Document);
+ this.setupEditor(config, this.props.Document, this.props.fieldKey);
}
- private setupEditor(config: any, doc?: Doc) {
- let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
- if (this._proseRef.current) {
- this._editorView = new EditorView(this._proseRef.current, {
+ private setupEditor(config: any, doc: Doc, fieldKey: string) {
+ let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
+ let startup = StrCast(doc.documentText);
+ startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : "";
+ if (!startup && !field && doc) {
+ startup = StrCast(doc[fieldKey]);
+ }
+ if (this._proseRef) {
+ this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }
}
});
- let text = StrCast(this.props.Document.documentText);
- if (text.startsWith("@@@")) {
- this.props.Document.proto!.documentText = undefined;
- this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ if (startup) {
+ Doc.GetProto(doc).documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
}
}
if (this.props.selectOnLoad) {
+ console.log("Sel on load " + this.props.Document.title + " " + doc!.title);
this.props.select(false);
this._editorView!.focus();
}
@@ -221,9 +255,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
- if (this._dropDisposer) {
- this._dropDisposer();
- }
}
onPointerDown = (e: React.PointerEvent): void => {
@@ -233,27 +264,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._toolTipTextMenu.tooltip.style.opacity = "0";
}
}
- if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) {
- if (e.target && (e.target as any).href) {
- let href = (e.target as any).href;
+ this._linkClicked = "";
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
+ let href = (e.target as any).href;
+ for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
+ href = parent.childNodes[0].href;
+ }
+ if (href) {
if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
- let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
- DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
- if (f instanceof Doc) {
- if (DocumentManager.Instance.getDocumentView(f)) {
- DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
- }
- else CollectionDockingView.Instance.AddRightSplit(f);
- }
- }));
+ this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ } else {
+ let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+ this.props.addDocument && this.props.addDocument(webDoc);
+ this._linkClicked = webDoc[Id];
}
e.stopPropagation();
e.preventDefault();
}
-
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
- this._gotDown = true;
e.preventDefault();
}
}
@@ -261,6 +290,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "1";
}
+ let ctrlKey = e.ctrlKey;
+ if (this._linkClicked) {
+ DocServer.GetRefField(this._linkClicked).then(f => {
+ (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"));
+ });
+ e.stopPropagation();
+ e.preventDefault();
+ }
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
@@ -276,29 +313,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
}
-
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => {
- if (NumCast(this.props.Document.nativeWidth)) {
- this.props.Document.nativeWidth = undefined;
- this.props.Document.nativeHeight = undefined;
-
- } else {
- this.props.Document.nativeWidth = this.props.Document[WidthSym]();
- this.props.Document.nativeHeight = this.props.Document[HeightSym]();
- }
- }
- specificContextMenu = (e: React.MouseEvent): void => {
- if (!this._gotDown) {
- e.preventDefault();
- return;
- }
- ContextMenu.Instance.addItem({
- description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
- event: this.textCapability
- });
- }
-
onPointerWheel = (e: React.WheelEvent): void => {
if (this.props.isSelected()) {
e.stopPropagation();
@@ -306,7 +320,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
onClick = (e: React.MouseEvent): void => {
- this._proseRef.current!.focus();
+ this._proseRef!.focus();
+ if (this._linkClicked) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
}
onMouseDown = (e: React.MouseEvent): void => {
if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
@@ -324,7 +342,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
- _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
tooltipLinkingMenuPlugin() {
let myprops = this.props;
return new Plugin({
@@ -359,6 +376,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
}
+
+ @action
+ onPointerEnter = (e: React.PointerEvent) => {
+ this._entered = true;
+ }
+ @action
+ onPointerLeave = (e: React.PointerEvent) => {
+ this._entered = false;
+ }
render() {
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
@@ -366,9 +392,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
+ height: this.props.height ? this.props.height : undefined,
+ background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
+ opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
+ color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial",
pointerEvents: interactive ? "all" : "none",
}}
- onKeyDown={this.onKeyPress}
+ // onKeyDown={this.onKeyPress}
onKeyPress={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
@@ -376,11 +406,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
- onContextMenu={this.specificContextMenu}
// tfs: do we need this event handler
onWheel={this.onPointerWheel}
+ onPointerEnter={this.onPointerEnter}
+ onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} />
</div>
);
}
diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss
index 893dc2d36..488681027 100644
--- a/src/client/views/nodes/IconBox.scss
+++ b/src/client/views/nodes/IconBox.scss
@@ -10,6 +10,7 @@
pointer-events: all;
svg {
width: $MINIMIZED_ICON_SIZE !important;
+ height: $MINIMIZED_ICON_SIZE !important;
height: auto;
background: white;
}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index b42eb44a5..00021bc78 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -12,7 +12,6 @@ import { IconField } from "../../../new_fields/IconField";
import { ContextMenu } from "../ContextMenu";
import Measure from "react-measure";
import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
-import { listSpec } from "../../../new_fields/Schema";
library.add(faCaretUp);
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 2316a050e..f1b73a676 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -25,7 +25,11 @@
}
.imageBox-cont img {
- height: 100%;
+ height: auto;
+ width:100%;
+}
+.imageBox-cont-interactive img {
+ height: auto;
width:100%;
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 6472ae711..0d19508fa 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,23 +1,30 @@
-
-import { action, observable, trace } from 'mobx';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
+import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
+import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
+import { Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
import { Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocComponent } from '../DocComponent';
+import { InkingControl } from '../InkingControl';
+import { positionSchema } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
-import { DocComponent } from '../DocComponent';
-import { positionSchema } from './DocumentView';
-import { FieldValue, Cast, StrCast } from '../../../new_fields/Types';
-import { ImageField } from '../../../new_fields/URLField';
-import { List } from '../../../new_fields/List';
-import { InkingControl } from '../InkingControl';
-import { Doc } from '../../../new_fields/Doc';
+var path = require('path');
+
+
+library.add(faImage);
+
export const pageSchema = createSchema({
curPage: "number"
@@ -38,16 +45,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
- @action
- onLoad = (target: any) => {
- var h = this._imgRef.current!.naturalHeight;
- var w = this._imgRef.current!.naturalWidth;
- console.log("title: " + this.Document.title);
- if (this._photoIndex === 0) {
- Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w);
- this.Document.height = FieldValue(this.Document.width, 0) * h / w;
- }
- }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -89,16 +86,19 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
onPointerDown = (e: React.PointerEvent): void => {
- if (Date.now() - this._lastTap < 300) {
- if (e.buttons === 1) {
- this._downX = e.clientX;
- this._downY = e.clientY;
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
- } else {
- this._lastTap = Date.now();
- }
+ if (e.shiftKey && e.ctrlKey)
+
+ e.stopPropagation();
+ // if (Date.now() - this._lastTap < 300) {
+ // if (e.buttons === 1) {
+ // this._downX = e.clientX;
+ // this._downY = e.clientY;
+ // document.removeEventListener("pointerup", this.onPointerUp);
+ // document.addEventListener("pointerup", this.onPointerUp);
+ // }
+ // } else {
+ // this._lastTap = Date.now();
+ // }
}
@action
onPointerUp = (e: PointerEvent): void => {
@@ -132,11 +132,23 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
let url = field.url.href;
- ContextMenu.Instance.addItem({
- description: "Copy path", event: () => {
- Utils.CopyText(url);
- }
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: "Rotate", event: action(() => {
+ let proto = Doc.GetProto(this.props.Document);
+ let nw = this.props.Document.nativeWidth;
+ let nh = this.props.Document.nativeHeight;
+ let w = this.props.Document.width;
+ let h = this.props.Document.height;
+ proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ proto.nativeWidth = nh;
+ proto.nativeHeight = nw;
+ this.props.Document.width = h;
+ this.props.Document.height = w;
+ }), icon: "expand-arrows-alt"
});
+ ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems });
}
}
@@ -157,24 +169,67 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
);
}
+ choosePath(url: URL) {
+ const lower = url.href.toLowerCase();
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1 || !(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ return url.href;
+ }
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
render() {
- trace();
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ 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[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
+ // this._curSuffix = "";
+ // if (w > 20) {
let field = this.Document[this.props.fieldKey];
- let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"];
- if (field instanceof ImageField) paths = [field.url.href];
- else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href);
- let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50);
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) paths = [this.choosePath(field.url)];
+ else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
+ // }
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
- let id = this.props.id; // bcz: used to set id = "isExpander" in templates.tsx
+ let rotation = NumCast(this.props.Document.rotation, 0);
+ let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
+ let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
return (
- <div id={id} className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <img id={id} src={paths[Math.min(paths.length, this._photoIndex)]}
- style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
+ <div id={id} className={`imageBox-cont${interactive}`}
+ onPointerDown={this.onPointerDown}
+ onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <img id={id}
+ key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
+ src={paths[Math.min(paths.length, this._photoIndex)]}
+ style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
+ // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
width={nativeWidth}
ref={this._imgRef}
- onLoad={this.onLoad} />
+ onError={this.onError} />
{paths.length > 1 ? this.dots(paths) : (null)}
- {this.lightbox(paths)}
+ {/* {this.lightbox(paths)} */}
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index c9d665ceb..917be734d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,13 +2,14 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { CompileScript } from "../../util/Scripting";
+import { CompileScript, ScriptOptions } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react");
import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, IsField } from "../../../new_fields/Doc";
+import { Doc, Field } from "../../../new_fields/Doc";
+import { ComputedField } from "../../../fields/ScriptField";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
@@ -18,7 +19,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
@computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
- get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document }
+ get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; }
constructor(props: FieldViewProps) {
super(props);
@@ -27,28 +28,38 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
- if (this._keyInput && this._valueInput) {
- let doc = this.fieldDocToLayout;
- if (!doc) {
- return;
+ if (this._keyInput && this._valueInput && this.fieldDocToLayout) {
+ if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) {
+ this._keyInput = "";
+ this._valueInput = "";
}
- let realDoc = doc;
-
- let script = CompileScript(this._valueInput, { addReturn: true });
- if (!script.compiled) {
- return;
- }
- let res = script.run();
- if (!res.success) return;
- const field = res.result;
- if (IsField(field)) {
- realDoc[this._keyInput] = field;
- }
- this._keyInput = "";
- this._valueInput = "";
}
}
}
+ public static SetField(doc: Doc, key: string, value: string) {
+ let eq = value.startsWith("=");
+ value = eq ? value.substr(1) : value;
+ let dubEq = value.startsWith(":=");
+ value = dubEq ? value.substr(2) : value;
+ let options: ScriptOptions = { addReturn: true };
+ if (dubEq) options.typecheck = false;
+ let script = CompileScript(value, options);
+ if (!script.compiled) {
+ return false;
+ }
+ let field = new ComputedField(script);
+ if (!dubEq) {
+ let res = script.run();
+ if (!res.success) return false;
+ field = res.result;
+ }
+ if (Field.IsField(field, true)) {
+ let target = !eq ? doc : Doc.GetProto(doc);
+ target[key] = field;
+ return true;
+ }
+ return false;
+ }
onPointerDown = (e: React.PointerEvent): void => {
if (e.buttons === 1 && this.props.isSelected()) {
@@ -78,7 +89,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
- for (let key in ids) {
+ for (let key of Object.keys(ids).sort()) {
rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 5de660d57..e8bc17532 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,16 +1,18 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
-import { CompileScript } from "../../util/Scripting";
+import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils';
+import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import "./KeyValuePair.scss";
import React = require("react");
-import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
+import { ComputedField } from '../../../fields/ScriptField';
+import { KeyValueBox } from './KeyValueBox';
// Represents one row in a key value plane
@@ -38,6 +40,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
+ addDocTab: emptyFunction
};
let contents = <FieldView {...props} />;
let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
@@ -46,8 +49,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
<button className="keyValuePair-td-key-delete" onClick={() => {
- if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1)
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
props.Document[props.fieldKey] = undefined;
+ }
else props.Document.proto![props.fieldKey] = undefined;
}}>
X
@@ -59,27 +63,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
<EditableView contents={contents} height={36} GetValue={() => {
let field = FieldValue(props.Document[props.fieldKey]);
- if (field) {
- //TODO Types
- return String(field);
- // return field.ToScriptString();
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
- SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true });
- if (!script.compiled) {
- return false;
- }
- let res = script.run();
- if (!res.success) return false;
- const field = res.result;
- if (IsField(field)) {
- props.Document[props.fieldKey] = field;
- return true;
- }
- return false;
- }}>
+ SetValue={(value: string) =>
+ KeyValueBox.SetField(props.Document, props.fieldKey, value)}>
</EditableView></td>
</tr>
);
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 611cb66b6..68b692aad 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -31,7 +31,7 @@ export class LinkBox extends React.Component<Props> {
@undoBatch
onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- DocumentManager.Instance.jumpToDocument(this.props.pairedDoc);
+ DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
}
onEditButtonPressed = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 11117122d..3f09d6214 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -6,9 +6,8 @@ import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Cast, FieldValue } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { Id } from "../../../new_fields/RefField";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
interface Props {
docView: DocumentView;
@@ -24,7 +23,7 @@ export class LinkMenu extends React.Component<Props> {
return links.map(link => {
let doc = FieldValue(Cast(link[key], Doc));
if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
}
});
}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 3760e378a..449408a61 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -17,6 +17,10 @@
z-index: 25;
pointer-events: all;
}
+.pdfBox-thumbnail {
+ position: absolute;
+ width: 100%;
+}
.pdfButton {
pointer-events: all;
width: 100px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 14fe2df80..855c744e6 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,26 +1,29 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
import Measure from "react-measure";
//@ts-ignore
import { Document, Page } from "react-pdf";
import 'react-pdf/dist/Page/AnnotationLayer.css';
+import { Id } from "../../../new_fields/FieldSymbols";
+import { makeInterface } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { ImageField, PdfField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
+import { DocServer } from "../../DocServer";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { SearchBox } from "../SearchBox";
import { Annotation } from './Annotation';
+import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
+import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
+var path = require('path');
import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { Opt } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { makeInterface } from "../../../new_fields/Schema";
-import { positionSchema } from "./DocumentView";
-import { pageSchema } from "./ImageBox";
-import { ImageField, PdfField } from "../../../new_fields/URLField";
-import { InkingControl } from "../InkingControl";
+import { ContextMenu } from "../ContextMenu";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -65,19 +68,21 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@observable private _currAnno: any = [];
@observable private _interactive: boolean = false;
- @observable private _loaded: boolean = false;
@computed private get curPage() { return NumCast(this.Document.curPage, 1); }
@computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
componentDidMount() {
+ let wasSelected = this.props.active();
this._reactionDisposer = reaction(
- () => [SelectionManager.SelectedDocuments().slice()],
+ () => [this.props.active(), this.curPage],
() => {
- if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage !== this.thumbnailPage && !this.props.isSelected()) {
- this.saveThumbnail();
- this._interactive = true;
- }
+ setTimeout(action(() => { // this forces the active() check to happen after all changes in a transaction have occurred.
+ if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.active()) {
+ this.saveThumbnail();
+ }
+ wasSelected = this._interactive = this.props.active();
+ }), 0);
},
{ fireImmediately: true });
@@ -243,20 +248,26 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@action
saveThumbnail = () => {
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
this._renderAsSvg = false;
setTimeout(() => {
+ runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0);
let nwidth = FieldValue(this.Document.nativeWidth, 0);
let nheight = FieldValue(this.Document.nativeHeight, 0);
- htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
+ htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 })
.then(action((dataUrl: string) => {
- this.props.Document.thumbnail = new ImageField(new URL(dataUrl));
- this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
- this._renderAsSvg = true;
+ SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ this.props.Document.thumbnail = new ImageField(new URL(url));
+ }
+ runInAction(() => this._renderAsSvg = true);
+ })
}))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
- }, 250);
+ }, 1250);
}
@action
@@ -266,7 +277,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
if (this._perPageInfo.length === 0) { //Makes sure it only runs once
this._perPageInfo = [...Array(page._transport.numPages)];
}
- this._loaded = true;
}
@action
@@ -283,7 +293,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
@computed
get pdfPage() {
- return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
+ return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
}
@computed
get pdfContent() {
@@ -291,18 +301,19 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
if (!pdfUrl) {
return <p>No pdf url to render</p>;
}
- let body = NumCast(this.props.Document.nativeHeight) ?
- this.pdfPage :
+ let pdfpage = this.pdfPage;
+ let body = this.Document.nativeHeight ?
+ pdfpage :
<Measure offset onResize={this.setScaling}>
{({ measureRef }) =>
<div className="pdfBox-page" ref={measureRef}>
- {this.pdfPage}
+ {pdfpage}
</div>
}
</Measure>;
- let xf = NumCast(this.Document.nativeHeight) / this.renderHeight;
+ let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}>
{body}
</Document>
</div >;
@@ -310,35 +321,81 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@computed
get pdfRenderer() {
- let proxy = this._loaded ? (null) : this.imageProxyRenderer;
let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- if ((!this._interactive && proxy) || !pdfUrl) {
+ let proxy = this.imageProxyRenderer;
+ if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) {
return proxy;
}
return [
+ proxy,
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
- this.pdfContent,
- proxy
+ this.pdfContent
];
}
+ choosePath(url: URL) {
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1)
+ return url.href;
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
+
@computed
get imageProxyRenderer() {
let thumbField = this.props.Document.thumbnail;
- if (thumbField) {
- let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
- return <img src={path} width="100%" />;
+ if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) {
+
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ // this._curSuffix = "";
+ // if (w > 20) {
+ let field = thumbField;
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) path = this.choosePath(field.url);
+ // }
+ return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />;
}
return (null);
}
@action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
@action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
+ onContextMenu = (e: React.MouseEvent): void => {
+ let field = Cast(this.Document[this.props.fieldKey], PdfField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => {
+ Utils.CopyText(url);
+ }, icon: "expand-arrows-alt"
+ });
+ }
+ }
render() {
- let classname = "pdfBox-cont"; // + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
+ let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} >
+ <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} >
{this.pdfRenderer}
</div >
);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 96dc884c8..1239b498f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,19 +1,17 @@
import React = require("react");
+import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
import * as rp from "request-promise";
-import "./VideoBox.scss";
-import { action, IReactionDisposer, reaction, observable } from "mobx";
-import { DocComponent } from "../DocComponent";
-import { positionSchema } from "./DocumentView";
import { makeInterface } from "../../../new_fields/Schema";
-import { pageSchema } from "./ImageBox";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Cast, FieldValue } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import Measure from "react-measure";
-import "./VideoBox.scss";
import { RouteStore } from "../../../server/RouteStore";
import { DocServer } from "../../DocServer";
+import { DocComponent } from "../DocComponent";
+import { positionSchema } from "./DocumentView";
+import { FieldView, FieldViewProps } from './FieldView';
+import { pageSchema } from "./ImageBox";
+import "./VideoBox.scss";
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
@@ -22,37 +20,30 @@ const VideoDocument = makeInterface(positionSchema, pageSchema);
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
private _reactionDisposer?: IReactionDisposer;
private _videoRef: HTMLVideoElement | null = null;
- private _loaded: boolean = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable public Playing: boolean = false;
public static LayoutString() { return FieldView.LayoutString(VideoBox); }
- public get player(): HTMLVideoElement | undefined {
- if (this._videoRef) {
- return this._videoRef;
- }
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
}
- @action
- setScaling = (r: any) => {
- if (this._loaded) {
- // bcz: the nativeHeight should really be set when the document is imported.
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- var newNativeHeight = nativeWidth * r.offset.height / r.offset.width;
- if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) {
- this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
- this.Document.nativeHeight = newNativeHeight;
- }
- } else {
- this._loaded = true;
+
+ videoLoad = () => {
+ let aspect = this.player!.videoWidth / this.player!.videoHeight;
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ if (!nativeWidth || !nativeHeight) {
+ if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
+ this.Document.nativeHeight = this.Document.nativeWidth / aspect;
+ this.Document.height = FieldValue(this.Document.width, 0) / aspect;
}
}
@action public Play() {
this.Playing = true;
if (this.player) this.player.play();
- if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000);
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500);
}
@action public Pause() {
@@ -70,7 +61,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@action
- updateTimecode = () => this.player && (this.props.Document.curPage = this.player.currentTime);
+ updateTimecode = () => {
+ this.player && (this.props.Document.curPage = this.player.currentTime);
+ }
componentDidMount() {
if (this.props.setVideoBox) this.props.setVideoBox(this);
@@ -87,16 +80,10 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
if (this._reactionDisposer) this._reactionDisposer();
this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
- vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true });
+ !this.Playing && (vref.currentTime = this.Document.curPage || 0)
+ , { fireImmediately: true });
}
}
- videoContent(path: string) {
- let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
- return <video className={`${style}`} ref={this.setVideoRef} onPointerDown={this.onPointerDown}>
- <source src={path} type="video/mp4" />
- Not supported.
- </video>;
- }
getMp4ForVideo(videoId: string = "JN5beCVArMs") {
return new Promise(async (resolve, reject) => {
@@ -105,32 +92,31 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
connection: 'keep-alive',
"user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0',
},
-
- }
+ };
try {
let responseSchema: any = {};
- const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig)
+ const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
const dataHtml = videoInfoResponse;
const start = dataHtml.indexOf('ytplayer.config = ') + 18;
const end = dataHtml.indexOf(';ytplayer.load');
- const subString = dataHtml.substring(start, end)
+ const subString = dataHtml.substring(start, end);
const subJson = JSON.parse(subString);
const stringSub = subJson.args.player_response;
const stringSubJson = JSON.parse(stringSub);
const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats;
- const videoDetails = stringSubJson.videoDetails
- responseSchema["adaptiveFormats"] = adaptiveFormats;
- responseSchema["videoDetails"] = videoDetails;
- resolve(responseSchema)
+ const videoDetails = stringSubJson.videoDetails;
+ responseSchema.adaptiveFormats = adaptiveFormats;
+ responseSchema.videoDetails = videoDetails;
+ resolve(responseSchema);
}
catch (err) {
console.log(`
--- Youtube ---
Function: getMp4ForVideo
- Error: `, err)
- reject(err)
+ Error: `, err);
+ reject(err);
}
- })
+ });
}
onPointerDown = (e: React.PointerEvent) => {
e.preventDefault();
@@ -139,9 +125,6 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
render() {
let field = Cast(this.Document[this.props.fieldKey], VideoField);
- if (!field) {
- return <div>Loading</div>;
- }
// this.getMp4ForVideo().then((mp4) => {
// console.log(mp4);
@@ -149,15 +132,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
// console.log("")
// });
// //
- let content = this.videoContent(field.url.href);
- return NumCast(this.props.Document.nativeHeight) ?
- content :
- <Measure offset onResize={this.setScaling}>
- {({ measureRef }) =>
- <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
- {content}
- </div>
- }
- </Measure>;
+
+ let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
+ return !field ? <div>Loading</div> :
+ <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown}>
+ <source src={field.url.href} type="video/mp4" />
+ Not supported.
+ </video>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2239a8e38..98c57fc75 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,12 +1,11 @@
-import "./WebBox.scss";
-import React = require("react");
-import { FieldViewProps, FieldView } from './FieldView';
+import { observer } from "mobx-react";
import { HtmlField } from "../../../new_fields/HtmlField";
import { WebField } from "../../../new_fields/URLField";
-import { observer } from "mobx-react";
-import { computed, reaction, IReactionDisposer } from 'mobx';
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./WebBox.scss";
+import React = require("react");
@observer
export class WebBox extends React.Component<FieldViewProps> {
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
new file mode 100644
index 000000000..91b711c79
--- /dev/null
+++ b/src/debug/Repl.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { observer } from 'mobx-react';
+import { observable, computed } from 'mobx';
+import { CompileScript } from '../client/util/Scripting';
+import { makeInterface } from '../new_fields/Schema';
+import { ObjectField } from '../new_fields/ObjectField';
+import { RefField } from '../new_fields/RefField';
+
+@observer
+class Repl extends React.Component {
+ @observable text: string = "";
+
+ @observable executedCommands: { command: string, result: any }[] = [];
+
+ onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+ this.text = e.target.value;
+ }
+
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (!e.ctrlKey && e.key === "Enter") {
+ e.preventDefault();
+ const script = CompileScript(this.text, {
+ addReturn: true, typecheck: false,
+ params: { makeInterface: "any" }
+ });
+ if (!script.compiled) {
+ this.executedCommands.push({ command: this.text, result: "Compile Error" });
+ } else {
+ const result = script.run({ makeInterface });
+ if (result.success) {
+ this.executedCommands.push({ command: this.text, result: result.result });
+ } else {
+ this.executedCommands.push({ command: this.text, result: result.error.message || result.error });
+ }
+ }
+ this.text = "";
+ }
+ }
+
+ @computed
+ get commands() {
+ return this.executedCommands.map(command => {
+ return (
+ <div style={{ marginTop: "5px" }}>
+ <p>{command.command}</p>
+ {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
+ <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre>
+ </div>
+ );
+ });
+ }
+
+ render() {
+ return (
+ <div>
+ <div style={{ verticalAlign: "bottom" }}>
+ {this.commands}
+ </div>
+ <textarea style={{ width: "100%", position: "absolute", bottom: "0px" }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
+ </div>
+ );
+ }
+}
+
+ReactDOM.render(<Repl />, document.getElementById("root")); \ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 4cac09dee..b22300d0b 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -3,184 +3,175 @@ import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
-
-// configure({
-// enforceActions: "observed"
-// });
-
-// @observer
-// class FieldViewer extends React.Component<{ field: BasicField<any> }> {
-// render() {
-// return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>;
-// }
-// }
-
-// @observer
-// class KeyViewer extends React.Component<{ field: Key }> {
-// render() {
-// return this.props.field.Name;
-// }
-// }
-
-// @observer
-// class ListViewer extends React.Component<{ field: ListField<Field> }>{
-// @observable
-// expanded = false;
-
-// render() {
-// let content;
-// if (this.expanded) {
-// content = (
-// <div>
-// {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)}
-// </div>
-// );
-// } else {
-// content = <>[...] ({this.props.field.Id})</>;
-// }
-// return (
-// <div>
-// <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button>
-// {content}
-// </div >
-// );
-// }
-// }
-
-// @observer
-// class DocumentViewer extends React.Component<{ field: Document }> {
-// private keyMap: ObservableMap<string, Key> = new ObservableMap;
-
-// private disposer?: Lambda;
-
-// componentDidMount() {
-// let f = () => {
-// Array.from(this.props.field._proxies.keys()).forEach(id => {
-// if (!this.keyMap.has(id)) {
-// Server.GetField(id, (field) => {
-// if (field && field instanceof Key) {
-// this.keyMap.set(id, field);
-// }
-// });
-// }
-// });
-// };
-// this.disposer = this.props.field._proxies.observe(f);
-// f();
-// }
-
-// componentWillUnmount() {
-// if (this.disposer) {
-// this.disposer();
-// }
-// }
-
-// render() {
-// let fields = Array.from(this.props.field._proxies.entries()).map(kv => {
-// let key = this.keyMap.get(kv[0]);
-// return (
-// <div key={kv[0]}>
-// <b>({key ? key.Name : kv[0]}): </b>
-// <DebugViewer fieldId={kv[1]}></DebugViewer>
-// </div>
-// );
-// });
-// return (
-// <div>
-// Document ({this.props.field.Id})
-// <div style={{ paddingLeft: "25px" }}>
-// {fields}
-// </div>
-// </div>
-// );
-// }
-// }
-
-// @observer
-// class DebugViewer extends React.Component<{ fieldId: string }> {
-// @observable
-// private field?: Field;
-
-// @observable
-// private error?: string;
-
-// constructor(props: { fieldId: string }) {
-// super(props);
-// this.update();
-// }
-
-// update() {
-// Server.GetField(this.props.fieldId, action((field: Opt<Field>) => {
-// this.field = field;
-// if (!field) {
-// this.error = `Field with id ${this.props.fieldId} not found`;
-// }
-// }));
-
-// }
-
-// render() {
-// let content;
-// if (this.field) {
-// // content = this.field.ToJson();
-// if (this.field instanceof ListField) {
-// content = (<ListViewer field={this.field} />);
-// } else if (this.field instanceof Document) {
-// content = (<DocumentViewer field={this.field} />);
-// } else if (this.field instanceof BasicField) {
-// content = (<FieldViewer field={this.field} />);
-// } else if (this.field instanceof Key) {
-// content = (<KeyViewer field={this.field} />);
-// } else {
-// content = (<span>Unrecognized field type</span>);
-// }
-// } else if (this.error) {
-// content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>;
-// } else {
-// content = <span>Field loading: {this.props.fieldId}</span>;
-// }
-// return content;
-// }
-// }
-
-// @observer
-// class Viewer extends React.Component {
-// @observable
-// private idToAdd: string = '';
-
-// @observable
-// private ids: string[] = [];
-
-// @action
-// inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-// this.idToAdd = e.target.value;
-// }
-
-// @action
-// onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
-// if (e.key === "Enter") {
-// this.ids.push(this.idToAdd);
-// this.idToAdd = "";
-// }
-// }
-
-// render() {
-// return (
-// <>
-// <input value={this.idToAdd}
-// onChange={this.inputOnChange}
-// onKeyDown={this.onKeyPress} />
-// <div>
-// {this.ids.map(id => <DebugViewer fieldId={id} key={id}></DebugViewer>)}
-// </div>
-// </>
-// );
-// }
-// }
-
-// ReactDOM.render((
-// <div style={{ position: "absolute", width: "100%", height: "100%" }}>
-// <Viewer />
-// </div>),
-// document.getElementById('root')
-// ); \ No newline at end of file
+import { Doc, Field, FieldResult, Opt } from '../new_fields/Doc';
+import { DocServer } from '../client/DocServer';
+import { Id } from '../new_fields/FieldSymbols';
+import { List } from '../new_fields/List';
+import { URLField } from '../new_fields/URLField';
+import { EditableView } from '../client/views/EditableView';
+import { CompileScript } from '../client/util/Scripting';
+
+function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
+function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
+function applyToDoc(doc: any, key: string | number, scriptString: string): boolean {
+ let script = CompileScript(scriptString, { addReturn: true, params: { this: doc instanceof Doc ? Doc.name : List.name } });
+ if (!script.compiled) {
+ return false;
+ }
+ const res = script.run({ this: doc });
+ if (!res.success) return false;
+ if (!Field.IsField(res.result, true)) return false;
+ doc[key] = res.result;
+ return true;
+}
+
+configure({
+ enforceActions: "observed"
+});
+
+@observer
+class ListViewer extends React.Component<{ field: List<Field> }>{
+ @observable
+ expanded = false;
+
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.expanded = !this.expanded;
+ e.stopPropagation();
+ }
+
+ render() {
+ let content;
+ if (this.expanded) {
+ content = (
+ <div>
+ {this.props.field.map((field, index) => <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} />)}
+ </div>
+ );
+ } else {
+ content = <>[...]</>;
+ }
+ return (
+ <div>
+ <button onClick={this.onClick}>Toggle</button>
+ {content}
+ </div >
+ );
+ }
+}
+
+@observer
+class DocumentViewer extends React.Component<{ field: Doc }> {
+ @observable
+ expanded = false;
+
+ @action
+ onClick = (e: React.MouseEvent) => {
+ this.expanded = !this.expanded;
+ e.stopPropagation();
+ }
+
+ render() {
+ let content;
+ if (this.expanded) {
+ const keys = Object.keys(this.props.field);
+ let fields = keys.map(key => {
+ return (
+ <div key={key}>
+ <b>({key}): </b>
+ <DebugViewer field={this.props.field[key]} setValue={value => applyToDoc(this.props.field, key, value)}></DebugViewer>
+ </div>
+ );
+ });
+ content = (
+ <div>
+ Document ({this.props.field[Id]})
+ <div style={{ paddingLeft: "25px" }}>
+ {fields}
+ </div>
+ </div>
+ );
+ } else {
+ content = <>[...] ({this.props.field[Id]})</>;
+ }
+ return (
+ <div>
+ <button onClick={this.onClick}>Toggle</button>
+ {content}
+ </div >
+ );
+ }
+}
+
+@observer
+class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: string): boolean }> {
+
+ render() {
+ let content;
+ const field = this.props.field;
+ if (field instanceof List) {
+ content = (<ListViewer field={field} />);
+ } else if (field instanceof Doc) {
+ content = (<DocumentViewer field={field} />);
+ } else if (typeof field === "string") {
+ content = <p>"{field}"</p>;
+ } else if (typeof field === "number" || typeof field === "boolean") {
+ content = <p>{field}</p>;
+ } else if (field instanceof URLField) {
+ content = <p>{field.url.href}</p>;
+ } else if (field instanceof Promise) {
+ return <p>Field loading</p>;
+ } else {
+ return <p>Unrecognized field type</p>;
+ }
+
+ return <EditableView GetValue={() => Field.toScriptString(field)} SetValue={this.props.setValue}
+ contents={content}></EditableView>;
+ }
+}
+
+@observer
+class Viewer extends React.Component {
+ @observable
+ private idToAdd: string = '';
+
+ @observable
+ private fields: Field[] = [];
+
+ @action
+ inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.idToAdd = e.target.value;
+ }
+
+ @action
+ onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+ if (e.key === "Enter") {
+ DocServer.GetRefField(this.idToAdd).then(action((field: any) => {
+ if (field !== undefined) {
+ this.fields.push(field);
+ }
+ }));
+ this.idToAdd = "";
+ }
+ }
+
+ render() {
+ return (
+ <>
+ <input value={this.idToAdd}
+ onChange={this.inputOnChange}
+ onKeyDown={this.onKeyPress} />
+ <div>
+ {this.fields.map((field, index) => <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>)}
+ </div>
+ </>
+ );
+ }
+}
+
+ReactDOM.render((
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Viewer />
+ </div>),
+ document.getElementById('root')
+); \ No newline at end of file
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index ae532c9e2..ac46ccf90 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,101 +1,97 @@
-// import { Field, FieldId } from "./Field";
-// import { Types } from "../server/Message";
-// import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
-// import { Server } from "../client/Server";
-// import { Without } from "../Utils";
+import { ObjectField } from "../new_fields/ObjectField";
+import { CompiledScript, CompileScript } from "../client/util/Scripting";
+import { Copy, ToScriptString, Parent, SelfProxy } from "../new_fields/FieldSymbols";
+import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { computed } from "mobx";
-// export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
-// capturedIds: { [id: string]: string };
-// }
+function optional(propSchema: PropSchema) {
+ return custom(value => {
+ if (value !== undefined) {
+ return propSchema.serializer(value);
+ }
+ return SKIP;
+ }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => {
+ if (jsonValue !== undefined) {
+ return propSchema.deserializer(jsonValue, callback, context, oldValue);
+ }
+ return SKIP;
+ });
+}
-// export interface ScriptData {
-// script: string;
-// options: SerializableOptions;
-// }
+const optionsSchema = createSimpleSchema({
+ requiredType: true,
+ addReturn: true,
+ typecheck: true,
+ params: optional(map(primitive()))
+});
-// export class ScriptField extends Field {
-// private _script?: CompiledScript;
-// get script(): CompiledScript {
-// return this._script!;
-// }
-// private options?: ScriptData;
+function deserializeScript(script: ScriptField) {
+ const comp = CompileScript(script.scriptString, script.options);
+ if (!comp.compiled) {
+ throw new Error("Couldn't compile loaded script");
+ }
+ (script as any)._script = comp;
+}
-// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
-// super(id);
+@Deserializable("script", deserializeScript)
+export class ScriptField extends ObjectField {
+ protected readonly _script: CompiledScript;
-// this._script = script;
+ constructor(script: CompiledScript) {
+ super();
-// if (save) {
-// Server.UpdateField(this);
-// }
-// }
+ this._script = script;
+ }
-// ToScriptString() {
-// return "new ScriptField(...)";
-// }
+ @serializable(custom(object(optionsSchema).serializer, () => SKIP))
+ get options() {
+ return this._script && this._script.options;
+ }
-// GetValue() {
-// return this.script;
-// }
+ @serializable(custom(primitive().serializer, () => SKIP))
+ get scriptString(): string {
+ return this._script && this._script.originalScript;
+ }
-// TrySetValue(): boolean {
-// throw new Error("Script fields currently can't be modified");
-// }
+ // init(callback: (res: Field) => any) {
+ // const options = this.options!;
+ // const keys = Object.keys(options.options.capturedIds);
+ // Server.GetFields(keys).then(fields => {
+ // let captured: { [name: string]: Field } = {};
+ // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+ // const opts: ScriptOptions = {
+ // addReturn: options.options.addReturn,
+ // params: options.options.params,
+ // requiredType: options.options.requiredType,
+ // capturedVariables: captured
+ // };
+ // const script = CompileScript(options.script, opts);
+ // if (!script.compiled) {
+ // throw new Error("Can't compile script");
+ // }
+ // this._script = script;
+ // callback(this);
+ // });
+ // }
-// UpdateFromServer() {
-// throw new Error("Script fields currently can't be updated");
-// }
+ [Copy](): ObjectField {
+ return new ScriptField(this._script);
+ }
-// static FromJson(id: string, data: ScriptData): ScriptField {
-// let field = new ScriptField(undefined, id, false);
-// field.options = data;
-// return field;
-// }
+ [ToScriptString]() {
+ return "script field";
+ }
+}
-// init(callback: (res: Field) => any) {
-// const options = this.options!;
-// const keys = Object.keys(options.options.capturedIds);
-// Server.GetFields(keys).then(fields => {
-// let captured: { [name: string]: Field } = {};
-// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
-// const opts: ScriptOptions = {
-// addReturn: options.options.addReturn,
-// params: options.options.params,
-// requiredType: options.options.requiredType,
-// capturedVariables: captured
-// };
-// const script = CompileScript(options.script, opts);
-// if (!script.compiled) {
-// throw new Error("Can't compile script");
-// }
-// this._script = script;
-// callback(this);
-// });
-// }
-
-// ToJson() {
-// const { options, originalScript } = this.script;
-// let capturedIds: { [id: string]: string } = {};
-// for (const capt in options.capturedVariables) {
-// capturedIds[options.capturedVariables[capt].Id] = capt;
-// }
-// const opts: SerializableOptions = {
-// ...options,
-// capturedIds
-// };
-// delete (opts as any).capturedVariables;
-// return {
-// id: this.Id,
-// type: Types.Script,
-// data: {
-// script: originalScript,
-// options: opts,
-// },
-// };
-// }
-
-// Copy(): Field {
-// //Script fields are currently immutable, so we can fake copy them
-// return this;
-// }
-// } \ No newline at end of file
+@Deserializable("computed", deserializeScript)
+export class ComputedField extends ScriptField {
+ @computed
+ get value() {
+ const val = this._script.run({ this: (this[Parent] as any)[SelfProxy] });
+ if (val.success) {
+ return val.result;
+ }
+ return undefined;
+ }
+} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss
index d0b7d4e41..eea69b81c 100644
--- a/src/mobile/ImageUpload.scss
+++ b/src/mobile/ImageUpload.scss
@@ -1,7 +1,13 @@
+@import "../client/views/globalCssVariables.scss";
+
.imgupload_cont {
- height: 100vh;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
width: 100vw;
- align-content: center;
+ height: 100vh;
+
.button_file {
text-align: center;
height: 50%;
@@ -10,4 +16,19 @@
color: grey;
font-size: 3em;
}
+
+ .input_file {
+ display: none;
+ }
+
+ .upload_label,
+ .upload_button {
+ background: $dark-color;
+ font-size: 500%;
+ font-family: $sans-serif;
+ text-align: center;
+ padding: 5vh;
+ margin-bottom: 20px;
+ color: white;
+ }
} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 1f9e160ce..bfc1738fc 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -9,6 +9,8 @@ import { Opt, Doc } from '../new_fields/Doc';
import { Cast } from '../new_fields/Types';
import { listSpec } from '../new_fields/Schema';
import { List } from '../new_fields/List';
+import { observer } from 'mobx-react';
+import { observable } from 'mobx';
@@ -19,58 +21,91 @@ import { List } from '../new_fields/List';
// imgInput.click();
// }
// }
+const inputRef = React.createRef<HTMLInputElement>();
-const onFileLoad = async (file: any) => {
- let imgPrev = document.getElementById("img_preview");
- if (imgPrev) {
- let files: File[] = file.target.files;
- if (files.length !== 0) {
- console.log(files[0]);
- let formData = new FormData();
- formData.append("file", files[0]);
+@observer
+class Uploader extends React.Component {
+ @observable
+ error: string = "";
+ @observable
+ status: string = "";
- const upload = window.location.origin + "/upload";
- const res = await fetch(upload, {
- method: 'POST',
- body: formData
- });
- const json = await res.json();
- json.map(async (file: any) => {
- let path = window.location.origin + file;
- var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200 });
+ onClick = async () => {
+ try {
+ this.status = "initializing protos";
+ await Docs.initProtos();
+ let imgPrev = document.getElementById("img_preview");
+ if (imgPrev) {
+ let files: FileList | null = inputRef.current!.files;
+ if (files && files.length !== 0) {
+ console.log(files[0]);
+ const name = files[0].name;
+ let formData = new FormData();
+ formData.append("file", files[0]);
- const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
- if (!res) {
- throw new Error("No user id returned");
- }
- const field = await DocServer.GetRefField(res);
- let pending: Opt<Doc>;
- if (field instanceof Doc) {
- pending = await Cast(field.optionalRightCollection, Doc);
- }
- if (pending) {
- const data = await Cast(pending.data, listSpec(Doc));
- if (data) {
- data.push(doc);
- } else {
- pending.data = new List([doc]);
- }
- }
- });
+ const upload = window.location.origin + "/upload";
+ this.status = "uploading image";
+ const res = await fetch(upload, {
+ method: 'POST',
+ body: formData
+ });
+ this.status = "upload image, getting json";
+ const json = await res.json();
+ json.map(async (file: any) => {
+ let path = window.location.origin + file;
+ var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+
+ this.status = "getting user document";
+
+ const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
+ if (!res) {
+ throw new Error("No user id returned");
+ }
+ const field = await DocServer.GetRefField(res);
+ let pending: Opt<Doc>;
+ if (field instanceof Doc) {
+ pending = await Cast(field.optionalRightCollection, Doc);
+ }
+ if (pending) {
+ this.status = "has pending docs";
+ const data = await Cast(pending.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ pending.data = new List([doc]);
+ }
+ this.status = "finished";
+ }
+ });
- // console.log(window.location.origin + file[0])
+ // console.log(window.location.origin + file[0])
- //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ }
+ }
+ } catch (error) {
+ this.error = JSON.stringify(error);
}
}
-};
+
+ render() {
+ return (
+ <div className="imgupload_cont">
+ <label htmlFor="input_image_file" className="upload_label">Choose an Image</label>
+ <input type="file" accept="image/*" className="input_file" id="input_image_file" ref={inputRef}></input>
+ <button onClick={this.onClick} className="upload_button">Upload</button>
+ <img id="img_preview" src=""></img>
+ <p>{this.status}</p>
+ <p>{this.error}</p>
+ </div>
+ );
+ }
+
+}
+
ReactDOM.render((
- <div className="imgupload_cont">
- {/* <button className = "button_file" = {onPointerDown}> Open Image </button> */}
- <input type="file" accept="image/*" onChange={onFileLoad} className="input_file" id="input_image_file"></input>
- <img id="img_preview" src=""></img>
- <div id="message" />
- </div>),
+ <Uploader />
+),
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts
index fc144222c..fd86031a8 100644
--- a/src/new_fields/CursorField.ts
+++ b/src/new_fields/CursorField.ts
@@ -1,7 +1,8 @@
-import { ObjectField, Copy, OnUpdate } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
import { observable } from "mobx";
import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, createSimpleSchema, object } from "serializr";
+import { serializable, createSimpleSchema, object, date } from "serializr";
+import { OnUpdate, ToScriptString, Copy } from "./FieldSymbols";
export type CursorPosition = {
x: number,
@@ -10,7 +11,8 @@ export type CursorPosition = {
export type CursorMetadata = {
id: string,
- identifier: string
+ identifier: string,
+ timestamp: number
};
export type CursorData = {
@@ -25,7 +27,8 @@ const PositionSchema = createSimpleSchema({
const MetadataSchema = createSimpleSchema({
id: true,
- identifier: true
+ identifier: true,
+ timestamp: true
});
const CursorSchema = createSimpleSchema({
@@ -46,10 +49,15 @@ export default class CursorField extends ObjectField {
setPosition(position: CursorPosition) {
this.data.position = position;
+ this.data.metadata.timestamp = Date.now();
this[OnUpdate]();
}
[Copy]() {
return new CursorField(this.data);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
} \ No newline at end of file
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
index c0a79f267..fc8abb9d9 100644
--- a/src/new_fields/DateField.ts
+++ b/src/new_fields/DateField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, date } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("date")
export class DateField extends ObjectField {
@@ -15,4 +16,8 @@ export class DateField extends ObjectField {
[Copy]() {
return new DateField(this.date);
}
+
+ [ToScriptString]() {
+ return `new DateField(new Date(${this.date.toISOString()}))`;
+ }
}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 7b86cd8b1..7e02a5bc5 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -4,31 +4,37 @@ import { autoObject, SerializationHelper, Deserializable } from "../client/util/
import { DocServer } from "../client/DocServer";
import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
-import { UndoManager, undoBatch } from "../client/util/UndoManager";
import { listSpec } from "./Schema";
-import { List } from "./List";
-import { ObjectField, Parent, OnUpdate } from "./ObjectField";
-import { RefField, FieldId, Id, HandleUpdate } from "./RefField";
-import { Docs } from "../client/documents/Documents";
-
-export function IsField(field: any): field is Field {
- return (typeof field === "string")
- || (typeof field === "number")
- || (typeof field === "boolean")
- || (field instanceof ObjectField)
- || (field instanceof RefField);
+import { ObjectField } from "./ObjectField";
+import { RefField, FieldId } from "./RefField";
+import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+
+export namespace Field {
+ export function toScriptString(field: Field): string {
+ if (typeof field === "string") {
+ return `"${field}"`;
+ } else if (typeof field === "number" || typeof field === "boolean") {
+ return String(field);
+ } else {
+ return field[ToScriptString]();
+ }
+ }
+ export function IsField(field: any): field is Field;
+ export function IsField(field: any, includeUndefined: true): field is Field | undefined;
+ export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
+ return (typeof field === "string")
+ || (typeof field === "number")
+ || (typeof field === "boolean")
+ || (field instanceof ObjectField)
+ || (field instanceof RefField)
+ || (includeUndefined && field === undefined);
+ }
}
export type Field = number | string | boolean | ObjectField | RefField;
export type Opt<T> = T | undefined;
export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
-export const Update = Symbol("Update");
-export const Self = Symbol("Self");
-export const SelfProxy = Symbol("SelfProxy");
-export const WidthSym = Symbol("Width");
-export const HeightSym = Symbol("Height");
-
/**
* Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
* If a default value is given, that will be returned instead of undefined.
@@ -42,10 +48,13 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
}
-export function DocListCast(field: FieldResult) {
- return Cast(field, listSpec(Doc), []).filter(d => d && d instanceof Doc).map(d => d as Doc);
+export function DocListCast(field: FieldResult): Doc[] {
+ return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
}
+export const WidthSym = Symbol("Width");
+export const HeightSym = Symbol("Height");
+
@Deserializable("doc").withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
@@ -53,6 +62,7 @@ export class Doc extends RefField {
const doc = new Proxy<this>(this, {
set: setter,
get: getter,
+ // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
has: (target, key) => key in target.__fields,
ownKeys: target => Object.keys(target.__fields),
getOwnPropertyDescriptor: (target, prop) => {
@@ -60,6 +70,7 @@ export class Doc extends RefField {
return {
configurable: true,//TODO Should configurable be true?
enumerable: true,
+ value: target.__fields[prop]
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
@@ -105,6 +116,10 @@ export class Doc extends RefField {
public [WidthSym] = () => NumCast(this[SelfProxy].width); // bcz: is this the right way to access width/height? it didn't work with : this.width
public [HeightSym] = () => NumCast(this[SelfProxy].height);
+ [ToScriptString]() {
+ return "invalid";
+ }
+
public [HandleUpdate](diff: any) {
const set = diff.$set;
if (set) {
@@ -138,8 +153,11 @@ export namespace Doc {
export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
}
+ export function IsPrototype(doc: Doc) {
+ return GetT(doc, "isPrototype", "boolean", true);
+ }
export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
- const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") == -1 ? doc.proto : doc;
+ const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
if (proto) {
proto[key] = value;
@@ -169,27 +187,35 @@ export namespace Doc {
// compare whether documents or their protos match
export function AreProtosEqual(doc: Doc, other: Doc) {
- let r = (doc[Id] === other[Id]);
- let r2 = (doc.proto && doc.proto.Id === other[Id]);
- let r3 = (other.proto && other.proto.Id === doc[Id]);
- let r4 = (doc.proto && other.proto && doc.proto[Id] === other.proto[Id]);
- return r || r2 || r3 || r4 ? true : false;
+ let r = (doc === other);
+ let r2 = (doc.proto === other);
+ let r3 = (other.proto === doc);
+ let r4 = (doc.proto === other.proto);
+ return r || r2 || r3 || r4;
}
- export function MakeAlias(doc: Doc) {
- const alias = new Doc;
+ // gets the document's prototype or returns the document if it is a prototype
+ export function GetProto(doc: Doc) {
+ return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!;
+ }
- if (!doc.proto) {
- alias.proto = doc;
- } else {
- PromiseValue(Cast(doc.proto, Doc)).then(proto => {
- if (proto) {
- alias.proto = proto;
- }
- });
+ export function allKeys(doc: Doc): string[] {
+ const results: Set<string> = new Set;
+
+ let proto: Doc | undefined = doc;
+ while (proto) {
+ Object.keys(proto).forEach(key => results.add(key));
+ proto = proto.proto;
}
- return alias;
+ return Array.from(results);
+ }
+
+ export function MakeAlias(doc: Doc) {
+ if (!GetT(doc, "isPrototype", "boolean", true)) {
+ return Doc.MakeCopy(doc);
+ }
+ return new Doc;
}
export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
@@ -213,41 +239,14 @@ export namespace Doc {
return copy;
}
- export function MakeLink(source: Doc, target: Doc) {
- UndoManager.RunInBatch(() => {
- let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
- //let linkDoc = new Doc;
- linkDoc.proto!.title = "-link name-";
- linkDoc.proto!.linkDescription = "";
- linkDoc.proto!.linkTags = "Default";
-
- linkDoc.proto!.linkedTo = target;
- linkDoc.proto!.linkedFrom = source;
-
- let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc));
- if (!linkedFrom) {
- target.linkedFromDocs = linkedFrom = new List<Doc>();
- }
- linkedFrom.push(linkDoc);
-
- let linkedTo = Cast(source.linkedToDocs, listSpec(Doc));
- if (!linkedTo) {
- source.linkedToDocs = linkedTo = new List<Doc>();
- }
- linkedTo.push(linkDoc);
- return linkDoc;
- }, "make link");
- }
-
- export function MakeDelegate(doc: Doc): Doc;
- export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>;
- export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> {
+ export function MakeDelegate(doc: Doc, id?: string): Doc;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>;
+ export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> {
if (!doc) {
return undefined;
}
- const delegate = new Doc();
+ const delegate = new Doc(id, true);
delegate.proto = doc;
return delegate;
}
- export const Prototype = Symbol("Prototype");
} \ No newline at end of file
diff --git a/src/new_fields/FieldSymbols.ts b/src/new_fields/FieldSymbols.ts
new file mode 100644
index 000000000..a436dcf2b
--- /dev/null
+++ b/src/new_fields/FieldSymbols.ts
@@ -0,0 +1,10 @@
+
+export const Update = Symbol("Update");
+export const Self = Symbol("Self");
+export const SelfProxy = Symbol("SelfProxy");
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export const Copy = Symbol("Copy");
+export const ToScriptString = Symbol("Copy"); \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
index d998746bb..f952acff9 100644
--- a/src/new_fields/HtmlField.ts
+++ b/src/new_fields/HtmlField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("html")
export class HtmlField extends ObjectField {
@@ -15,4 +16,8 @@ export class HtmlField extends ObjectField {
[Copy]() {
return new HtmlField(this.html);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
index 1a928389d..62b2cd254 100644
--- a/src/new_fields/IconField.ts
+++ b/src/new_fields/IconField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("icon")
export class IconField extends ObjectField {
@@ -15,4 +16,8 @@ export class IconField extends ObjectField {
[Copy]() {
return new IconField(this.icon);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 2d75f8a19..4e3b7abe0 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
import { deepCopy } from "../Utils";
export enum InkTool {
@@ -40,4 +41,8 @@ export class InkField extends ObjectField {
[Copy]() {
return new InkField(deepCopy(this.inkData));
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index 70e36f911..f1e4c4721 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -1,11 +1,12 @@
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Field, Update, Self, FieldResult, SelfProxy } from "./Doc";
+import { Field } from "./Doc";
import { setter, getter, deleteProperty, updateFunction } from "./util";
import { serializable, alias, list } from "serializr";
import { observable, action } from "mobx";
-import { ObjectField, OnUpdate, Copy, Parent } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
+import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
const listHandlers: any = {
/// Mutator methods
@@ -225,7 +226,7 @@ type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
@Deserializable("list")
class ListImpl<T extends Field> extends ObjectField {
- constructor(fields: T[] = []) {
+ constructor(fields?: T[]) {
super();
const list = new Proxy<this>(this, {
set: setter,
@@ -244,7 +245,9 @@ class ListImpl<T extends Field> extends ObjectField {
defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
});
this[SelfProxy] = list;
- (list as any).push(...fields);
+ if (fields) {
+ (list as any).push(...fields);
+ }
return list;
}
@@ -284,6 +287,11 @@ class ListImpl<T extends Field> extends ObjectField {
private [Self] = this;
private [SelfProxy]: any;
+
+ [ToScriptString]() {
+ return "invalid";
+ // return `new List([${(this as any).map((field => Field.toScriptString(field))}])`;
+ }
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index f276bfa67..5f4a6f8fb 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,14 +1,13 @@
import { Doc } from "./Doc";
import { RefField } from "./RefField";
-
-export const OnUpdate = Symbol("OnUpdate");
-export const Parent = Symbol("Parent");
-export const Copy = Symbol("Copy");
+import { OnUpdate, Parent, Copy, ToScriptString } from "./FieldSymbols";
export abstract class ObjectField {
- protected [OnUpdate](diff?: any) { };
+ protected [OnUpdate](diff?: any) { }
private [Parent]?: RefField | ObjectField;
abstract [Copy](): ObjectField;
+
+ abstract [ToScriptString](): string;
}
export namespace ObjectField {
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index fd99ae1c0..130ec066e 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -3,8 +3,9 @@ import { FieldWaiting } from "./Doc";
import { primitive, serializable } from "serializr";
import { observable, action } from "mobx";
import { DocServer } from "../client/DocServer";
-import { RefField, Id } from "./RefField";
-import { ObjectField, Copy } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ObjectField } from "./ObjectField";
+import { Id, Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("proxy")
export class ProxyField<T extends RefField> extends ObjectField {
@@ -26,6 +27,10 @@ export class ProxyField<T extends RefField> extends ObjectField {
return new ProxyField<T>(this.fieldId);
}
+ [ToScriptString]() {
+ return "invalid";
+ }
+
@serializable(primitive())
readonly fieldId: string = "";
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
index 202c65f21..75ce4287f 100644
--- a/src/new_fields/RefField.ts
+++ b/src/new_fields/RefField.ts
@@ -1,9 +1,8 @@
import { serializable, primitive, alias } from "serializr";
import { Utils } from "../Utils";
+import { Id, HandleUpdate, ToScriptString } from "./FieldSymbols";
export type FieldId = string;
-export const HandleUpdate = Symbol("HandleUpdate");
-export const Id = Symbol("Id");
export abstract class RefField {
@serializable(alias("id", primitive()))
private __id: FieldId;
@@ -15,4 +14,6 @@ export abstract class RefField {
}
protected [HandleUpdate]?(diff: any): void;
+
+ abstract [ToScriptString](): string;
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index eb30e76de..89d077a47 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -1,6 +1,7 @@
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
+import { Copy, ToScriptString } from "./FieldSymbols";
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@@ -15,4 +16,8 @@ export class RichTextField extends ObjectField {
[Copy]() {
return new RichTextField(this.Data);
}
+
+ [ToScriptString]() {
+ return "invalid";
+ }
} \ No newline at end of file
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index b821baec9..40ffaecd5 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -1,5 +1,7 @@
-import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType } from "./Types";
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types";
import { Doc, Field } from "./Doc";
+import { ObjectField } from "./ObjectField";
+import { RefField } from "./RefField";
type AllToInterface<T extends Interface[]> = {
1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
@@ -10,10 +12,16 @@ export const emptySchema = createSchema({});
export const Document = makeInterface(emptySchema);
export type Document = makeInterface<[typeof emptySchema]>;
-export type makeInterface<T extends Interface[]> = Partial<AllToInterface<T>> & Doc & { proto: Doc | undefined };
+export interface InterfaceFunc<T extends Interface[]> {
+ (docs: Doc[]): makeInterface<T>[];
+ (): makeInterface<T>;
+ (doc: Doc): makeInterface<T>;
+}
+
+export type makeInterface<T extends Interface[]> = AllToInterface<T> & Doc & { proto: Doc | undefined };
// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
-export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
+export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFunc<T> {
let schema: Interface = {};
for (const s of schemas) {
for (const key in s) {
@@ -24,7 +32,21 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
get(target: any, prop, receiver) {
const field = receiver.doc[prop];
if (prop in schema) {
- return Cast(field, (schema as any)[prop]);
+ const desc = (schema as any)[prop];
+ if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
+ return Cast(field, desc.type, desc.defaultVal);
+ } else if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
+ const doc = Cast(field, Doc);
+ if (doc === undefined) {
+ return undefined;
+ } else if (doc instanceof Doc) {
+ return desc(doc);
+ } else {
+ return doc.then(doc => doc && desc(doc));
+ }
+ } else {
+ return Cast(field, desc);
+ }
}
return field;
},
@@ -33,14 +55,21 @@ export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc)
return true;
}
});
- return function (doc?: Doc) {
- doc = doc || new Doc;
+ const fn = (doc: Doc) => {
if (!(doc instanceof Doc)) {
throw new Error("Currently wrapping a schema in another schema isn't supported");
}
const obj = Object.create(proto, { doc: { value: doc, writable: false } });
return obj;
};
+ return function (doc?: Doc | Doc[]) {
+ doc = doc || new Doc;
+ if (doc instanceof Doc) {
+ return fn(doc);
+ } else {
+ return doc.map(fn);
+ }
+ };
}
export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
@@ -79,4 +108,11 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon
export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
return { List: type as any };//TODO Types
+}
+
+export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
+ return {
+ type: type as any,
+ defaultVal
+ };
} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index 4b4c58eb8..8dd893aa4 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -2,14 +2,16 @@ import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
-export type ToType<T extends ToConstructor<Field> | ListSpec<Field>> =
+export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
T extends "number" ? number :
T extends "boolean" ? boolean :
T extends ListSpec<infer U> ? List<U> :
// T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+ T extends DefaultFieldConstructor<infer _U> ? never :
T extends { new(...args: any[]): List<Field> } ? never :
- T extends { new(...args: any[]): infer R } ? R : never;
+ T extends { new(...args: any[]): infer R } ? R :
+ T extends (doc?: Doc) => infer R ? R : never;
export type ToConstructor<T extends Field> =
T extends string ? "string" :
@@ -19,12 +21,17 @@ export type ToConstructor<T extends Field> =
new (...args: any[]) => T;
export type ToInterface<T extends Interface> = {
- [P in Exclude<keyof T, "proto">]: FieldResult<ToType<T[P]>>;
+ [P in Exclude<keyof T, "proto">]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
};
// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
export type ListSpec<T extends Field> = { List: ToConstructor<T> };
+export type DefaultFieldConstructor<T extends Field> = {
+ type: ToConstructor<T>,
+ defaultVal: T
+};
+
// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
@@ -32,9 +39,10 @@ export type Tail<T extends any[]> =
((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any);
//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
export interface Interface {
- [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ [key: string]: InterfaceValue;
// [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
}
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index d00a95a16..4a2841fb6 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -1,6 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
-import { ObjectField, Copy } from "./ObjectField";
+import { ObjectField } from "./ObjectField";
+import { ToScriptString, Copy } from "./FieldSymbols";
function url() {
return custom(
@@ -13,15 +14,24 @@ function url() {
);
}
-export class URLField extends ObjectField {
+export abstract class URLField extends ObjectField {
@serializable(url())
readonly url: URL;
- constructor(url: URL) {
+ constructor(url: string);
+ constructor(url: URL);
+ constructor(url: URL | string) {
super();
+ if (typeof url === "string") {
+ url = new URL(url);
+ }
this.url = url;
}
+ [ToScriptString]() {
+ return `new ${this.constructor.name}("${this.url.href}")`;
+ }
+
[Copy](): this {
return new (this.constructor as any)(this.url);
}
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index a5f5e368b..8cb1db953 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,11 +1,13 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Update, Doc, Field } from "./Doc";
+import { Doc, Field } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField } from "./Proxy";
import { FieldValue } from "./Types";
-import { RefField, Id } from "./RefField";
-import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ObjectField } from "./ObjectField";
import { action } from "mobx";
+import { Parent, OnUpdate, Update, Id, SelfProxy } from "./FieldSymbols";
+import { ComputedField } from "../fields/ScriptField";
export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
if (SerializationHelper.IsSerializing()) {
@@ -37,8 +39,13 @@ export const setter = action(function (target: any, prop: string | symbol | numb
delete curValue[Parent];
delete curValue[OnUpdate];
}
- target.__fields[prop] = value;
- target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+ if (value === undefined) {
+ delete target.__fields[prop];
+ } else {
+ target.__fields[prop] = value;
+ }
+ if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } });
+ else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
UndoManager.AddEvent({
redo: () => receiver[prop] = value,
undo: () => receiver[prop] = curValue
@@ -56,26 +63,21 @@ export function getter(target: any, prop: string | symbol | number, receiver: an
return getField(target, prop);
}
-//TODO The callback parameter is never being passed in currently, so we should be able to get rid of it.
-export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
const field = target.__fields[prop];
if (field instanceof ProxyField) {
- return field.value(callback);
+ return field.value();
+ }
+ if (field instanceof ComputedField) {
+ return field.value;
}
- if (field === undefined && !ignoreProto) {
+ if (field === undefined && !ignoreProto && prop !== "proto") {
const proto = getField(target, "proto", true);
if (proto instanceof Doc) {
- let field = proto[prop];
- if (field instanceof Promise) {
- callback && field.then(callback);
- return undefined;
- } else {
- callback && callback(field);
- return field;
- }
+ return proto[prop];
}
+ return undefined;
}
- callback && callback(field);
return field;
}
@@ -84,7 +86,8 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
delete target[prop];
return true;
}
- throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+ target[SelfProxy][prop] = undefined;
+ return true;
}
export function updateFunction(target: any, prop: any, value: any, receiver: any) {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index fdf5b6a5c..c4af5cdaa 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -11,6 +11,7 @@ export enum RouteStore {
// UPLOAD AND STATIC FILE SERVING
public = "/public",
upload = "/upload",
+ dataUriToImage = "/uploadURI",
images = "/images",
// USER AND WORKSPACES
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5b63ac356..816c5f269 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,21 +1,17 @@
-import { computed, observable, action, runInAction } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import * as rp from 'request-promise';
+import { DocServer } from "../../../client/DocServer";
import { Docs } from "../../../client/documents/Documents";
-import { Attribute, AttributeGroup, Catalog, Schema, AggregateFunction } from "../../../client/northstar/model/idea/idea";
+import { Gateway, NorthstarSettings } from "../../../client/northstar/manager/Gateway";
+import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { RouteStore } from "../../RouteStore";
-import { DocServer } from "../../../client/DocServer";
-import { Doc, Opt, Field } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
-import { CollectionTreeView } from "../../../client/views/collections/CollectionTreeView";
import { CollectionView } from "../../../client/views/collections/CollectionView";
-import { NorthstarSettings, Gateway } from "../../../client/northstar/manager/Gateway";
-import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel";
-import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel";
-import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation";
-import { Cast, PromiseValue } from "../../../new_fields/Types";
+import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
+import { Cast } from "../../../new_fields/Types";
+import { RouteStore } from "../../RouteStore";
export class CurrentUserUtils {
private static curr_email: string;
@@ -33,11 +29,12 @@ export class CurrentUserUtils {
private static createUserDocument(id: string): Doc {
let doc = new Doc(id, true);
doc.viewType = CollectionViewType.Tree;
+ doc.dropAction = "alias";
doc.layout = CollectionView.LayoutString();
doc.title = this.email;
doc.data = new List<Doc>();
doc.excludeFromLibrary = true;
- doc.optionalRightCollection = Docs.SchemaDocument(["title"], [], { title: "Pending documents" });
+ doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" });
// doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
// (doc.library as Doc).excludeFromLibrary = true;
return doc;
@@ -66,9 +63,9 @@ export class CurrentUserUtils {
NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
await Gateway.Instance.ClearCatalog();
const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
- let extras = await Promise.all(extraSchemas.map(async sc => await Gateway.Instance.GetSchema("", sc)));
+ let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
- if (catprom) await Promise.all(catprom);
+ // if (catprom) await Promise.all(catprom);
} catch (e) {
}
@@ -83,29 +80,29 @@ export class CurrentUserUtils {
@action static SetNorthstarCatalog(ctlog: Catalog, extras: Catalog[]) {
CurrentUserUtils.NorthstarDBCatalog = ctlog;
- if (ctlog && ctlog.schemas) {
- extras.map(ex => ctlog.schemas!.push(ex));
- return ctlog.schemas.map(async schema => {
- let schemaDocuments: Doc[] = [];
- let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema);
- await Promise.all(attributesToBecomeDocs.reduce((promises, attr) => {
- promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
- if (field instanceof Doc) {
- schemaDocuments.push(field);
- } else {
- var atmod = new ColumnAttributeModel(attr);
- let histoOp = new HistogramOperation(schema.displayName!,
- new AttributeTransformationModel(atmod, AggregateFunction.None),
- new AttributeTransformationModel(atmod, AggregateFunction.Count),
- new AttributeTransformationModel(atmod, AggregateFunction.Count));
- schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
- }
- })));
- return promises;
- }, [] as Promise<void>[]));
- return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
- });
- }
+ // if (ctlog && ctlog.schemas) {
+ // extras.map(ex => ctlog.schemas!.push(ex));
+ // return ctlog.schemas.map(async schema => {
+ // let schemaDocuments: Doc[] = [];
+ // let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema);
+ // await Promise.all(attributesToBecomeDocs.reduce((promises, attr) => {
+ // promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ // if (field instanceof Doc) {
+ // schemaDocuments.push(field);
+ // } else {
+ // var atmod = new ColumnAttributeModel(attr);
+ // let histoOp = new HistogramOperation(schema.displayName!,
+ // new AttributeTransformationModel(atmod, AggregateFunction.None),
+ // new AttributeTransformationModel(atmod, AggregateFunction.Count),
+ // new AttributeTransformationModel(atmod, AggregateFunction.Count));
+ // schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
+ // }
+ // })));
+ // return promises;
+ // }, [] as Promise<void>[]));
+ // return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }));
+ // });
+ // }
}
public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { this._northstarCatalog = ctlog; }
diff --git a/src/server/database.ts b/src/server/database.ts
index 69005d2d3..70b3efced 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -8,9 +8,13 @@ export class Database {
private url = 'mongodb://localhost:27017/Dash';
private currentWrites: { [id: string]: Promise<void> } = {};
private db?: mongodb.Db;
+ private onConnect: (() => void)[] = [];
constructor() {
- this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
+ this.MongoClient.connect(this.url, (err, client) => {
+ this.db = client.db();
+ this.onConnect.forEach(fn => fn());
+ });
}
public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
@@ -32,66 +36,98 @@ export class Database {
};
newProm = prom ? prom.then(run) : run();
this.currentWrites[id] = newProm;
+ } else {
+ this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
}
}
public delete(id: string, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).remove({ id: id });
+ if (this.db) {
+ this.db.collection(collectionName).remove({ id: id });
+ } else {
+ this.onConnect.push(() => this.delete(id, collectionName));
+ }
}
public deleteAll(collectionName = Database.DocumentsCollection): Promise<any> {
- return new Promise(res =>
- this.db && this.db.collection(collectionName).deleteMany({}, res));
+ return new Promise(res => {
+ if (this.db) {
+ this.db.collection(collectionName).deleteMany({}, res);
+ } else {
+ this.onConnect.push(() => this.db && this.db.collection(collectionName).deleteMany({}, res));
+ }
+ });
}
public insert(value: any, collectionName = Database.DocumentsCollection) {
- if (!this.db) { return; }
- if ("id" in value) {
- value._id = value.id;
- delete value.id;
- }
- const id = value._id;
- const collection = this.db.collection(collectionName);
- const prom = this.currentWrites[id];
- let newProm: Promise<void>;
- const run = (): Promise<void> => {
- return new Promise<void>(resolve => {
- collection.insertOne(value, (err, res) => {
- if (this.currentWrites[id] === newProm) {
- delete this.currentWrites[id];
- }
- resolve();
+ if (this.db) {
+ if ("id" in value) {
+ value._id = value.id;
+ delete value.id;
+ }
+ const id = value._id;
+ const collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.insertOne(value, (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ });
});
- });
- };
- newProm = prom ? prom.then(run) : run();
- this.currentWrites[id] = newProm;
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
+ } else {
+ this.onConnect.push(() => this.insert(value, collectionName));
+ }
}
public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
- if (result) {
- result.id = result._id;
- delete result._id;
- fn(result);
- } else {
- fn(undefined);
- }
- });
+ if (this.db) {
+ this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
+ if (result) {
+ result.id = result._id;
+ delete result._id;
+ fn(result);
+ } else {
+ fn(undefined);
+ }
+ });
+ } else {
+ this.onConnect.push(() => this.getDocument(id, fn, collectionName));
+ }
}
public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
- if (err) {
- console.log(err.message);
- console.log(err.errmsg);
- }
- fn(docs.map(doc => {
- doc.id = doc._id;
- delete doc._id;
- return doc;
- }));
- });
+ if (this.db) {
+ this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
+ if (err) {
+ console.log(err.message);
+ console.log(err.errmsg);
+ }
+ fn(docs.map(doc => {
+ doc.id = doc._id;
+ delete doc._id;
+ return doc;
+ }));
+ });
+ } else {
+ this.onConnect.push(() => this.getDocuments(ids, fn, collectionName));
+ }
+ }
+
+ public query(query: any): Promise<mongodb.Cursor> {
+ if (this.db) {
+ return Promise.resolve<mongodb.Cursor>(this.db.collection('newDocuments').find(query));
+ } else {
+ return new Promise<mongodb.Cursor>(res => {
+ this.onConnect.push(() => res(this.query(query)));
+ });
+ }
}
public print() {
diff --git a/src/server/downsize.ts b/src/server/downsize.ts
new file mode 100644
index 000000000..ed68fbecc
--- /dev/null
+++ b/src/server/downsize.ts
@@ -0,0 +1,40 @@
+import * as sharp from 'sharp';
+import * as fs from 'fs';
+
+const folder = "./src/server/public/files/";
+const pngTypes = ["png", "PNG"];
+const jpgTypes = ["jpg", "JPG", "jpeg", "JPEG"];
+const smallResizer = sharp().resize(100);
+fs.readdir(folder, async (err, files) => {
+ if (err) {
+ console.log(err);
+ return;
+ }
+ // files.forEach(file => {
+ // if (file.includes("_s") || file.includes("_m") || file.includes("_l")) {
+ // fs.unlink(folder + file, () => { });
+ // }
+ // });
+ for (const file of files) {
+ const filesplit = file.split(".");
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ if (pngTypes.some(type => file.endsWith(type))) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ } else if (jpgTypes.some(type => file.endsWith(type))) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ } else {
+ continue;
+ }
+ resizers.forEach(resizer => {
+ fs.createReadStream(folder + file).pipe(resizer.resizer).pipe(fs.createWriteStream(folder + filesplit[0] + resizer.suffix + "." + filesplit[1]));
+ });
+ }
+}); \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 47bf0da6a..eda1ab422 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -6,6 +6,8 @@ import * as session from 'express-session';
import * as expressValidator from 'express-validator';
import * as formidable from 'formidable';
import * as fs from 'fs';
+import * as sharp from 'sharp';
+const imageDataUri = require('image-data-uri');
import * as mobileDetect from 'mobile-detect';
import * as passport from 'passport';
import * as path from 'path';
@@ -57,7 +59,7 @@ app.use(session({
app.use(flash());
app.use(expressFlash());
-app.use(bodyParser.json());
+app.use(bodyParser.json({ limit: "10mb" }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(expressValidator());
app.use(passport.initialize());
@@ -165,13 +167,15 @@ addSecureRoute(
RouteStore.getCurrUser
);
+const pngTypes = [".png", ".PNG"];
+const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
+const uploadDir = __dirname + "/public/files/";
// SETTERS
-
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
+app.post(
+ RouteStore.upload,
+ (req, res) => {
let form = new formidable.IncomingForm();
- form.uploadDir = __dirname + "/public/files/";
+ form.uploadDir = uploadDir;
form.keepExtensions = true;
// let path = req.body.path;
console.log("upload");
@@ -179,15 +183,76 @@ addSecureRoute(
console.log("parsing");
let names: string[] = [];
for (const name in files) {
- names.push(`/files/` + path.basename(files[name].path));
+ const file = path.basename(files[name].path);
+ const ext = path.extname(file);
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
+ });
+ }
+ names.push(`/files/` + file);
}
res.send(names);
});
+ }
+);
+
+addSecureRoute(
+ Method.POST,
+ (user, res, req) => {
+ const uri = req.body.uri;
+ const filename = req.body.name;
+ if (!uri || !filename) {
+ res.status(401).send("incorrect parameters specified");
+ return;
+ }
+ imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => {
+ const ext = path.extname(savedName);
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgTypes.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext));
+ });
+ }
+ res.send("/files/" + filename + ext);
+ });
},
undefined,
- RouteStore.upload
+ RouteStore.dataUriToImage
);
-
// AUTHENTICATION
// Sign Up
@@ -282,6 +347,7 @@ function setField(socket: Socket, newValue: Transferable) {
if (newValue.type === Types.Text) {
Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data });
console.log("set field");
+ console.log("checking in");
}
}
@@ -298,7 +364,7 @@ const suffixMap: { [type: string]: (string | [string, string | ((json: any) => a
"number": "_n",
"string": "_t",
// "boolean": "_b",
- "image": ["_t", "url"],
+ // "image": ["_t", "url"],
"video": ["_t", "url"],
"pdf": ["_t", "url"],
"audio": ["_t", "url"],
diff --git a/src/server/public/files/.gitignore b/src/server/public/files/.gitignore
index f59ec20aa..c96a04f00 100644
--- a/src/server/public/files/.gitignore
+++ b/src/server/public/files/.gitignore
@@ -1 +1,2 @@
-* \ No newline at end of file
+*
+!.gitignore \ No newline at end of file
diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts
new file mode 100644
index 000000000..6f4d6642f
--- /dev/null
+++ b/src/server/remapUrl.ts
@@ -0,0 +1,59 @@
+import { Database } from "./database";
+import { Search } from "./Search";
+import * as path from 'path';
+
+const suffixMap: { [type: string]: true } = {
+ "video": true,
+ "pdf": true,
+ "audio": true,
+ "web": true
+};
+
+async function update() {
+ await new Promise(res => setTimeout(res, 10));
+ console.log("update");
+ const cursor = await Database.Instance.query({});
+ console.log("Cleared");
+ const updates: [string, any][] = [];
+ function updateDoc(doc: any) {
+ if (doc.__type !== "Doc") {
+ return;
+ }
+ const fields = doc.fields;
+ if (!fields) {
+ return;
+ }
+ const update: any = {
+ };
+ let dynfield = false;
+ for (const key in fields) {
+ const value = fields[key];
+ if (value && value.__type && suffixMap[value.__type]) {
+ const url = new URL(value.url);
+ if (url.href.includes("azure")) {
+ dynfield = true;
+
+ update.$set = { ["fields." + key + ".url"]: `${url.protocol}//localhost:1050${url.pathname}` };
+ }
+ }
+ }
+ if (dynfield) {
+ updates.push([doc._id, update]);
+ }
+ }
+ await cursor.forEach(updateDoc);
+ await Promise.all(updates.map(doc => {
+ console.log(doc[0], doc[1]);
+ return new Promise(res => Database.Instance.update(doc[0], doc[1], () => {
+ console.log("wrote " + JSON.stringify(doc[1]));
+ res();
+ }, false, "newDocuments"));
+ }));
+ console.log("Done");
+ // await Promise.all(updates.map(update => {
+ // return limit(() => Search.Instance.updateDocument(update));
+ // }));
+ cursor.close();
+}
+
+update();
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
new file mode 100644
index 000000000..de1fd25e1
--- /dev/null
+++ b/src/server/updateSearch.ts
@@ -0,0 +1,101 @@
+import { Database } from "./database";
+import { Cursor } from "mongodb";
+import { Search } from "./Search";
+import pLimit from 'p-limit';
+
+const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ // "boolean": "_b",
+ // "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+};
+
+function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ if (val === null || val === undefined) {
+ return;
+ }
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val };
+}
+
+function getSuffix(value: string | [string, any]): string {
+ return typeof value === "string" ? value : value[0];
+}
+
+const limit = pLimit(5);
+async function update() {
+ // await new Promise(res => setTimeout(res, 5));
+ console.log("update");
+ await Search.Instance.clear();
+ const cursor = await Database.Instance.query({});
+ console.log("Cleared");
+ const updates: any[] = [];
+ let numDocs = 0;
+ function updateDoc(doc: any) {
+ numDocs++;
+ if ((numDocs % 50) === 0) {
+ console.log("updateDoc " + numDocs);
+ }
+ console.log("doc " + numDocs);
+ if (doc.__type !== "Doc") {
+ return;
+ }
+ const fields = doc.fields;
+ if (!fields) {
+ return;
+ }
+ const update: any = { id: doc._id };
+ let dynfield = false;
+ for (const key in fields) {
+ const value = fields[key];
+ const term = ToSearchTerm(value);
+ if (term !== undefined) {
+ let { suffix, value } = term;
+ update[key + suffix] = value;
+ dynfield = true;
+ }
+ }
+ if (dynfield) {
+ updates.push(update);
+ console.log(updates.length);
+ }
+ }
+ await cursor.forEach(updateDoc);
+ await Promise.all(updates.map(update => {
+ return limit(() => Search.Instance.updateDocument(update));
+ }));
+ cursor.close();
+}
+
+update(); \ No newline at end of file
diff --git a/test/test.ts b/test/test.ts
index 91dc43379..f1cf75bd4 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -1,9 +1,17 @@
import { expect } from 'chai';
import 'mocha';
+const { JSDOM } = require('jsdom');
+const dom = new JSDOM("", {
+ url: "http://localhost:1050"
+});
+(global as any).window = dom.window;
+
+
import { autorun, reaction } from "mobx";
import { Doc } from '../src/new_fields/Doc';
import { Cast } from '../src/new_fields/Types';
-
+import { createSchema, makeInterface, defaultSpec } from '../src/new_fields/Schema';
+import { ImageField } from '../src/new_fields/URLField';
describe("Document", () => {
it('should hold fields', () => {
let key = "Test";
@@ -33,3 +41,120 @@ describe("Document", () => {
expect(ran).to.equal(true);
});
});
+
+const testSchema1 = createSchema({
+ a: "number",
+ b: "string",
+ c: "boolean",
+ d: ImageField,
+ e: Doc
+});
+
+type TestDoc = makeInterface<[typeof testSchema1]>;
+const TestDoc = makeInterface(testSchema1);
+
+const testSchema2 = createSchema({
+ a: defaultSpec("boolean", true),
+ b: defaultSpec("number", 5),
+ c: defaultSpec("string", "hello world")
+});
+
+type TestDoc2 = makeInterface<[typeof testSchema2]>;
+const TestDoc2 = makeInterface(testSchema2);
+
+const testSchema3 = createSchema({
+ a: TestDoc2
+});
+
+type TestDoc3 = makeInterface<[typeof testSchema3]>;
+const TestDoc3 = makeInterface(testSchema3);
+
+describe("Schema", () => {
+ it("should do the right thing 1", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = 5;
+ test1.b = "hello";
+ test1.c = true;
+ test1.d = ifield;
+ test1.e = test2;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(5);
+ expect(doc.b).to.equal("hello");
+ expect(doc.c).to.equal(true);
+ expect(doc.d).to.equal(ifield);
+ expect(doc.e).to.equal(test2);
+ });
+
+ it("should do the right thing 2", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = "hello";
+ test1.b = 5;
+ test1.c = test2;
+ test1.d = true;
+ test1.e = ifield;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(undefined);
+ expect(doc.b).to.equal(undefined);
+ expect(doc.c).to.equal(undefined);
+ expect(doc.d).to.equal(undefined);
+ expect(doc.e).to.equal(undefined);
+ });
+
+ it("should do the right thing 3", () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const ifield = new ImageField(new URL("http://google.com"));
+ test1.a = "hello";
+ test1.b = 5;
+ test1.c = test2;
+ test1.d = true;
+ test1.e = ifield;
+ const doc = TestDoc(test1);
+ expect(doc.a).to.equal(undefined);
+ expect(doc.b).to.equal(undefined);
+ expect(doc.c).to.equal(undefined);
+ expect(doc.d).to.equal(undefined);
+ expect(doc.e).to.equal(undefined);
+ });
+
+ it("should do the right thing 4", () => {
+ const doc = TestDoc2();
+ expect(doc.a).to.equal(true);
+ expect(doc.b).to.equal(5);
+ expect(doc.c).to.equal("hello world");
+
+ const d2 = new Doc;
+ d2.a = false;
+ d2.b = 4;
+ d2.c = "goodbye";
+ const doc2 = TestDoc2(d2);
+ expect(doc2.a).to.equal(false);
+ expect(doc2.b).to.equal(4);
+ expect(doc2.c).to.equal("goodbye");
+
+ const d3 = new Doc;
+ d3.a = "hello";
+ d3.b = false;
+ d3.c = 5;
+ const doc3 = TestDoc2(d3);
+ expect(doc3.a).to.equal(true);
+ expect(doc3.b).to.equal(5);
+ expect(doc3.c).to.equal("hello world");
+ });
+
+ it("should do the right thing 5", async () => {
+ const test1 = new Doc;
+ const test2 = new Doc;
+ const doc = TestDoc3(test1);
+ expect(doc.a).to.equal(undefined);
+ test1.a = test2;
+ const doc2 = (await doc.a)!;
+ expect(doc2.a).to.equal(true);
+ expect(doc2.b).to.equal(5);
+ expect(doc2.c).to.equal("hello world");
+ });
+});
diff --git a/webpack.config.js b/webpack.config.js
index c08742272..5e0a6a883 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,6 +8,7 @@ module.exports = {
entry: {
bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'],
viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'],
+ repl: ["./src/debug/Repl.tsx", 'webpack-hot-middleware/client?reload=true'],
test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'],
inkControls: ["./src/mobile/InkControls.tsx", 'webpack-hot-middleware/client?reload=true'],
imageUpload: ["./src/mobile/ImageUpload.tsx", 'webpack-hot-middleware/client?reload=true'],