aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vscode/settings.json3
-rw-r--r--package.json9
-rw-r--r--src/Utils.ts42
-rw-r--r--src/client/Server.ts157
-rw-r--r--src/client/SocketStub.ts22
-rw-r--r--src/client/documents/Documents.ts39
-rw-r--r--src/client/northstar/core/brusher/BrushLinkModel.ts40
-rw-r--r--src/client/northstar/core/brusher/IBaseBrushable.ts4
-rw-r--r--src/client/northstar/core/filter/FilterModel.ts91
-rw-r--r--src/client/northstar/core/filter/IBaseFilterConsumer.ts2
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts73
-rw-r--r--src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts238
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.scss39
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx174
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss30
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx124
-rw-r--r--src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss13
-rw-r--r--src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx79
-rw-r--r--src/client/northstar/model/ModelHelpers.ts24
-rw-r--r--src/client/northstar/model/binRanges/VisualBinRangeHelper.ts6
-rw-r--r--src/client/northstar/operations/BaseOperation.ts10
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts119
-rw-r--r--src/client/northstar/utils/Extensions.ts9
-rw-r--r--src/client/northstar/utils/LABColor.ts90
-rw-r--r--src/client/northstar/utils/MathUtil.ts13
-rw-r--r--src/client/northstar/utils/SizeConverter.ts99
-rw-r--r--src/client/northstar/utils/StyleContants.ts95
-rw-r--r--src/client/util/DocumentManager.ts60
-rw-r--r--src/client/util/DragManager.ts295
-rw-r--r--src/client/util/Scripting.ts10
-rw-r--r--src/client/util/SelectionManager.ts21
-rw-r--r--src/client/util/type_decls.d10
-rw-r--r--src/client/views/ContextMenu.tsx9
-rw-r--r--src/client/views/DocumentDecorations.scss89
-rw-r--r--src/client/views/DocumentDecorations.tsx273
-rw-r--r--src/client/views/EditableView.tsx11
-rw-r--r--src/client/views/InkingCanvas.scss173
-rw-r--r--src/client/views/InkingCanvas.tsx205
-rw-r--r--src/client/views/InkingControl.scss135
-rw-r--r--src/client/views/InkingControl.tsx3
-rw-r--r--src/client/views/InkingStroke.tsx20
-rw-r--r--src/client/views/Main.scss7
-rw-r--r--src/client/views/Main.tsx210
-rw-r--r--src/client/views/collections/CollectionDockingView.scss2
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx40
-rw-r--r--src/client/views/collections/CollectionPDFView.scss2
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx89
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx2
-rw-r--r--src/client/views/collections/CollectionVideoView.scss2
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx108
-rw-r--r--src/client/views/collections/CollectionView.tsx57
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx86
-rw-r--r--src/client/views/collections/MarqueeView.tsx175
-rw-r--r--src/client/views/collections/PreviewCursor.scss18
-rw-r--r--src/client/views/collections/PreviewCursor.tsx73
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx37
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx106
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx115
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss (renamed from src/client/views/collections/CollectionFreeFormView.scss)41
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx (renamed from src/client/views/collections/CollectionFreeFormView.tsx)280
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss (renamed from src/client/views/collections/MarqueeView.scss)8
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx202
-rw-r--r--src/client/views/collections/collectionFreeForm/PreviewCursor.scss27
-rw-r--r--src/client/views/collections/collectionFreeForm/PreviewCursor.tsx119
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx8
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx12
-rw-r--r--src/client/views/nodes/DocumentView.scss20
-rw-r--r--src/client/views/nodes/DocumentView.tsx379
-rw-r--r--src/client/views/nodes/FieldView.tsx18
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx285
-rw-r--r--src/client/views/nodes/HistogramBox.scss8
-rw-r--r--src/client/views/nodes/HistogramBox.tsx67
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/client/views/nodes/PDFBox.scss4
-rw-r--r--src/client/views/nodes/PDFBox.tsx53
-rw-r--r--src/client/views/nodes/Sticky.tsx138
-rw-r--r--src/client/views/nodes/VideoBox.tsx29
-rw-r--r--src/client/views/nodes/WebBox.scss2
-rw-r--r--src/fields/BooleanField.ts25
-rw-r--r--src/fields/Document.ts221
-rw-r--r--src/fields/KeyStore.ts87
-rw-r--r--src/fields/ListField.ts5
-rw-r--r--src/mobile/ImageUpload.tsx49
-rw-r--r--src/server/Message.ts168
-rw-r--r--src/server/RouteStore.ts5
-rw-r--r--src/server/ServerUtil.ts96
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.tsx19
-rw-r--r--src/server/authentication/controllers/user_controller.ts3
-rw-r--r--src/server/authentication/models/current_user_utils.ts91
-rw-r--r--src/server/authentication/models/user_model.ts11
-rw-r--r--src/server/database.ts27
-rw-r--r--src/server/index.ts49
-rw-r--r--src/server/public/files/upload_a6a70d84ebb65febf7900e29f52cc86d.pdfbin0 -> 1043556 bytes
-rw-r--r--tsconfig.json2
97 files changed, 4535 insertions, 2153 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 081b05b38..fc315ffaf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,6 +7,7 @@
"**/.DS_Store": true,
},
"editor.formatOnSave": true,
+ "editor.detectIndentation": false,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true
-}
+} \ No newline at end of file
diff --git a/package.json b/package.json
index 0ca2ab1ef..6e894fd20 100644
--- a/package.json
+++ b/package.json
@@ -38,10 +38,10 @@
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"@hig/flyout": "^1.0.3",
- "@trendmicro/react-dropdown": "^1.3.0",
- "@types/async": "^2.4.1",
"@hig/theme-context": "^2.1.3",
"@hig/theme-data": "^2.3.3",
+ "@trendmicro/react-dropdown": "^1.3.0",
+ "@types/async": "^2.4.1",
"@types/bcrypt-nodejs": "0.0.30",
"@types/bluebird": "^3.5.25",
"@types/body-parser": "^1.17.0",
@@ -80,6 +80,7 @@
"@types/react-measure": "^2.0.4",
"@types/react-table": "^6.7.22",
"@types/request": "^2.48.1",
+ "@types/request-promise": "^4.1.42",
"@types/socket.io": "^2.1.2",
"@types/socket.io-client": "^1.4.32",
"@types/typescript": "^2.0.0",
@@ -91,6 +92,7 @@
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
"bootstrap": "^4.3.1",
+ "class-transformer": "^0.2.0",
"connect-flash": "^0.1.1",
"connect-mongo": "^2.0.3",
"cookie-parser": "^1.4.4",
@@ -103,6 +105,7 @@
"express-validator": "^5.3.1",
"expressjs": "^1.0.1",
"flexlayout-react": "^0.3.3",
+ "font-awesome": "^4.7.0",
"formidable": "^1.2.1",
"golden-layout": "^1.5.9",
"html-to-image": "^0.1.0",
@@ -153,9 +156,9 @@
"react-split-pane": "^0.1.85",
"react-table": "^6.9.2",
"request": "^2.88.0",
+ "request-promise": "^4.2.4",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
- "socketio": "^1.0.0",
"typescript-collections": "^1.3.2",
"url-loader": "^1.1.2",
"uuid": "^3.3.2",
diff --git a/src/Utils.ts b/src/Utils.ts
index a4db94809..a5d9bd0ca 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -37,20 +37,52 @@ export class Utils {
document.body.removeChild(textArea);
}
+ public static loggingEnabled: Boolean = false;
+ public static logFilter: number | undefined = undefined;
+ private static log(prefix: string, messageName: string, message: any, receiving: boolean) {
+ if (!this.loggingEnabled) {
+ return;
+ }
+ message = message || {};
+ if (this.logFilter !== undefined && this.logFilter !== message.type) {
+ return;
+ }
+ let idString = (message._id || message.id || "").padStart(36, ' ');
+ prefix = prefix.padEnd(16, ' ');
+ console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)}`);
+ }
+ private static loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
+ return (args: any) => {
+ this.log(prefix, messageName, args, true);
+ func(args);
+ }
+ };
+
public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
+ this.log("Emit", message.Name, args, false);
socket.emit(message.Message, args);
}
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any) {
- socket.emit(message.Message, args, fn);
+ public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
+ public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
+ public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
+ this.log("Emit", message.Name, args, false);
+ if (fn) {
+ socket.emit(message.Message, args, this.loggingCallback('Receiving', fn, message.Name));
+ } else {
+ return new Promise<any>(res => socket.emit(message.Message, args, this.loggingCallback('Receiving', res, message.Name)));
+ }
}
- public static AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
- socket.on(message.Message, handler);
+ public static AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
+ socket.on(message.Message, this.loggingCallback('Incoming', handler, message.Name));
}
public static AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
- socket.on(message.Message, (arg: T, fn: (res: any) => any) => handler([arg, fn]));
+ socket.on(message.Message, (arg: T, fn: (res: any) => any) => {
+ this.log('S receiving', message.Name, arg, true);
+ handler([arg, this.loggingCallback('S sending', fn, message.Name)])
+ });
}
}
diff --git a/src/client/Server.ts b/src/client/Server.ts
index 7d882c76d..37e3c2c0d 100644
--- a/src/client/Server.ts
+++ b/src/client/Server.ts
@@ -2,7 +2,7 @@ import { Key } from "../fields/Key"
import { ObservableMap, action, reaction } from "mobx";
import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field"
import { Document } from "../fields/Document"
-import { SocketStub } from "./SocketStub";
+import { SocketStub, FieldMap } from "./SocketStub";
import * as OpenSocket from 'socket.io-client';
import { Utils } from "./../Utils";
import { MessageStore, Types } from "./../server/Message";
@@ -15,74 +15,102 @@ export class Server {
// Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached).
// Call this is from within a reaction and test whether the return value is FieldWaiting.
- // 'hackTimeout' is here temporarily for simplicity when debugging things.
- public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): Opt<Field> | FIELD_WAITING {
- let cached = this.ClientFieldsCached.get(fieldid);
- if (!cached) {
- this.ClientFieldsCached.set(fieldid, FieldWaiting);
- SocketStub.SEND_FIELD_REQUEST(fieldid, action((field: Field | undefined) => {
- let cached = this.ClientFieldsCached.get(fieldid);
- if (cached != FieldWaiting)
- callback(cached);
- else {
- if (field) {
- this.ClientFieldsCached.set(fieldid, field);
- } else {
- this.ClientFieldsCached.delete(fieldid)
+ public static GetField(fieldid: FieldId): Promise<Opt<Field>>;
+ public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): void;
+ public static GetField(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void {
+ let fn = (cb: (field: Opt<Field>) => void) => {
+
+ let cached = this.ClientFieldsCached.get(fieldid);
+ if (!cached) {
+ this.ClientFieldsCached.set(fieldid, FieldWaiting);
+ SocketStub.SEND_FIELD_REQUEST(fieldid, action((field: Field | undefined) => {
+ let cached = this.ClientFieldsCached.get(fieldid);
+ if (cached != FieldWaiting)
+ cb(cached);
+ else {
+ if (field) {
+ this.ClientFieldsCached.set(fieldid, field);
+ } else {
+ this.ClientFieldsCached.delete(fieldid)
+ }
+ cb(field)
}
- callback(field)
- }
- }));
- } else if (cached != FieldWaiting) {
- setTimeout(() => callback(cached as Field), 0);
+ }));
+ } else if (cached != FieldWaiting) {
+ setTimeout(() => cb(cached as Field), 0);
+ } else {
+ reaction(() => {
+ return this.ClientFieldsCached.get(fieldid);
+ }, (field, reaction) => {
+ if (field !== "<Waiting>") {
+ reaction.dispose()
+ cb(field)
+ }
+ })
+ }
+ }
+ if (callback) {
+ fn(callback);
} else {
- reaction(() => {
- return this.ClientFieldsCached.get(fieldid);
- }, (field, reaction) => {
- if (field !== FieldWaiting) {
- reaction.dispose()
- callback(field)
- }
- })
+ return new Promise(res => fn(res));
}
- return cached;
}
- public static GetFields(fieldIds: FieldId[], callback: (fields: { [id: string]: Field }) => any) {
- let neededFieldIds: FieldId[] = [];
- let waitingFieldIds: FieldId[] = [];
- let existingFields: { [id: string]: Field } = {};
- for (let id of fieldIds) {
- let field = this.ClientFieldsCached.get(id);
- if (!field) {
- neededFieldIds.push(id);
- this.ClientFieldsCached.set(id, FieldWaiting);
- } else if (field === FieldWaiting) {
- waitingFieldIds.push(id);
- } else {
- existingFields[id] = field;
- }
- }
- SocketStub.SEND_FIELDS_REQUEST(neededFieldIds, (fields) => {
- for (let key in fields) {
- let field = fields[key];
- if (!(this.ClientFieldsCached.get(field.Id) instanceof Field)) {
- this.ClientFieldsCached.set(field.Id, field)
+ public static GetFields(fieldIds: FieldId[]): Promise<{ [id: string]: Field }>;
+ public static GetFields(fieldIds: FieldId[], callback: (fields: FieldMap) => any): void;
+ public static GetFields(fieldIds: FieldId[], callback?: (fields: FieldMap) => any): Promise<FieldMap> | void {
+ let fn = (cb: (fields: FieldMap) => void) => {
+
+ let neededFieldIds: FieldId[] = [];
+ let waitingFieldIds: FieldId[] = [];
+ let existingFields: { [id: string]: Field } = {};
+ for (let id of fieldIds) {
+ let field = this.ClientFieldsCached.get(id);
+ if (!field) {
+ neededFieldIds.push(id);
+ this.ClientFieldsCached.set(id, FieldWaiting);
+ } else if (field === FieldWaiting) {
+ waitingFieldIds.push(id);
+ } else {
+ existingFields[id] = field;
}
}
- reaction(() => {
- return waitingFieldIds.map(this.ClientFieldsCached.get);
- }, (cachedFields, reaction) => {
- if (!cachedFields.some(field => !field || field === FieldWaiting)) {
- reaction.dispose();
- for (let field of cachedFields) {
- let realField = field as Field;
- existingFields[realField.Id] = realField;
+ SocketStub.SEND_FIELDS_REQUEST(neededFieldIds, action((fields: FieldMap) => {
+ for (let id of neededFieldIds) {
+ let field = fields[id];
+ if (field) {
+ if (!(this.ClientFieldsCached.get(field.Id) instanceof Field)) {
+ this.ClientFieldsCached.set(field.Id, field)
+ } else {
+ throw new Error("we shouldn't be trying to replace things that are already in the cache")
+ }
+ } else {
+ if (this.ClientFieldsCached.get(id) === FieldWaiting) {
+ this.ClientFieldsCached.delete(id);
+ } else {
+ throw new Error("we shouldn't be trying to replace things that are already in the cache")
+ }
}
- callback({ ...fields, ...existingFields })
}
- }, { fireImmediately: true })
- });
+ reaction(() => {
+ return waitingFieldIds.map(id => this.ClientFieldsCached.get(id));
+ }, (cachedFields, reaction) => {
+ if (!cachedFields.some(field => !field || field === FieldWaiting)) {
+ reaction.dispose();
+ for (let field of cachedFields) {
+ let realField = field as Field;
+ existingFields[realField.Id] = realField;
+ }
+ cb({ ...fields, ...existingFields })
+ }
+ }, { fireImmediately: true })
+ }));
+ };
+ if (callback) {
+ fn(callback);
+ } else {
+ return new Promise(res => fn(res));
+ }
}
public static GetDocumentField(doc: Document, key: Key, callback?: (field: Field) => void) {
@@ -138,12 +166,17 @@ export class Server {
if (Server.ClientFieldsCached.has(field._id)) {
var f = Server.ClientFieldsCached.get(field._id);
if (f && f != FieldWaiting) {
+ // console.log("Applying : " + field._id);
f.UpdateFromServer(field.data);
f.init(() => { });
+ } else {
+ // console.log("Not applying wa : " + field._id);
}
+ } else {
+ // console.log("Not applying mi : " + field._id);
}
}
}
-Server.Socket.on(MessageStore.Foo.Message, Server.connected);
-Server.Socket.on(MessageStore.SetField.Message, Server.updateField);
+Utils.AddServerHandler(Server.Socket, MessageStore.Foo, Server.connected);
+Utils.AddServerHandler(Server.Socket, MessageStore.SetField, Server.updateField);
diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts
index a0b89b7c9..5045037c5 100644
--- a/src/client/SocketStub.ts
+++ b/src/client/SocketStub.ts
@@ -7,6 +7,11 @@ import { Utils } from "../Utils";
import { Server } from "./Server";
import { ServerUtils } from "../server/ServerUtil";
+
+export interface FieldMap {
+ [id: string]: Opt<Field>;
+}
+
//TODO tfs: I think it might be cleaner to not have SocketStub deal with turning what the server gives it into Fields (in other words not call ServerUtils.FromJson), and leave that for the Server class.
export class SocketStub {
@@ -35,19 +40,26 @@ export class SocketStub {
Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(document.ToJson()))
}
- public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void) {
- if (fieldid) {
+ public static SEND_FIELD_REQUEST(fieldid: FieldId): Promise<Opt<Field>>;
+ public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void): void;
+ public static SEND_FIELD_REQUEST(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void {
+ let fn = function (cb: (field: Opt<Field>) => void) {
Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => {
if (field) {
- ServerUtils.FromJson(field).init(callback);
+ ServerUtils.FromJson(field).init(cb);
} else {
- callback(undefined);
+ cb(undefined);
}
})
}
+ if (callback) {
+ fn(callback);
+ } else {
+ return new Promise(res => fn(res))
+ }
}
- public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: { [key: string]: Field }) => any) {
+ public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: FieldMap) => any) {
Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => {
let fieldMap: any = {};
for (let field of fields) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cf6d7d503..1f0744782 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -11,6 +11,9 @@ import { PDFField } from "../../fields/PDFField";
import { TextField } from "../../fields/TextField";
import { VideoField } from "../../fields/VideoField";
import { WebField } from "../../fields/WebField";
+import { HistogramField } from "../northstar/dash-fields/HistogramField";
+import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
+import { HistogramOperation } from "../northstar/operations/HistogramOperation";
import { Server } from "../Server";
import { CollectionPDFView } from "../views/collections/CollectionPDFView";
import { CollectionVideoView } from "../views/collections/CollectionVideoView";
@@ -22,7 +25,6 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { PDFBox } from "../views/nodes/PDFBox";
import { VideoBox } from "../views/nodes/VideoBox";
import { WebBox } from "../views/nodes/WebBox";
-import { HistogramBox } from "../views/nodes/HistogramBox";
export interface DocumentOptions {
x?: number;
@@ -63,15 +65,14 @@ export namespace Documents {
const videoProtoId = "videoProto"
const audioProtoId = "audioProto";
- export function initProtos(callback: () => void) {
- Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => {
+ export function initProtos(): Promise<void> {
+ return Server.GetFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId]).then(fields => {
textProto = fields[textProtoId] as Document;
histoProto = fields[histoProtoId] as Document;
collProto = fields[collProtoId] as Document;
imageProto = fields[imageProtoId] as Document;
webProto = fields[webProtoId] as Document;
kvpProto = fields[kvpProtoId] as Document;
- callback();
});
}
function assignOptions(doc: Document, options: DocumentOptions): Document {
@@ -113,15 +114,19 @@ export namespace Documents {
function GetImagePrototype(): Document {
if (!imageProto) {
imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+ { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString());
+ imageProto.SetNumber(KeyStore.CurPage, 0);
}
return imageProto;
}
function GetHistogramPrototype(): Document {
- return histoProto ? histoProto :
- histoProto = setupPrototypeOptions(textProtoId, "HISTO PROTOTYPE", HistogramBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] });
+ if (!histoProto) {
+ histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("AnnotationsKey"),
+ { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
+ histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString());
+ }
+ return histoProto;
}
function GetTextPrototype(): Document {
return textProto ? textProto :
@@ -131,7 +136,7 @@ export namespace Documents {
function GetPdfPrototype(): Document {
if (!pdfProto) {
pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+ { x: 0, y: 0, nativeWidth: 1200, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
pdfProto.SetNumber(KeyStore.CurPage, 1);
pdfProto.SetText(KeyStore.BackgroundLayout, PDFBox.LayoutString());
}
@@ -156,7 +161,7 @@ export namespace Documents {
function GetVideoPrototype(): Document {
if (!videoProto) {
videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+ { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
videoProto.SetNumber(KeyStore.CurPage, 0);
videoProto.SetText(KeyStore.BackgroundLayout, VideoBox.LayoutString());
}
@@ -185,8 +190,8 @@ export namespace Documents {
return assignToDelegate(SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]), options);
}
- export function HistogramDocument(options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, ["", TextField]).MakeDelegate(), options);
+ export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}, id?: string, delegId?: string) {
+ return assignToDelegate(SetInstanceOptions(GetHistogramPrototype(), options, [histoOp, HistogramField], id).MakeDelegate(delegId), options);
}
export function TextDocument(options: DocumentOptions = {}) {
return assignToDelegate(SetInstanceOptions(GetTextPrototype(), options, ["", TextField]).MakeDelegate(), options);
@@ -219,6 +224,14 @@ export namespace Documents {
return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options)
}
+ export function CaptionDocument(doc: Document) {
+ const captionDoc = doc.CreateAlias();
+ captionDoc.SetText(KeyStore.OverlayLayout, FixedCaption());
+ captionDoc.SetNumber(KeyStore.Width, doc.GetNumber(KeyStore.Width, 0));
+ captionDoc.SetNumber(KeyStore.Height, doc.GetNumber(KeyStore.Height, 0));
+ return captionDoc;
+ }
+
// example of custom display string for an image that shows a caption.
function EmbeddedCaption() {
return `<div style="height:100%">
@@ -229,7 +242,7 @@ export namespace Documents {
+ FormattedTextBox.LayoutString("CaptionKey") +
`</div>
</div>` };
- function FixedCaption(fieldName: string = "Caption") {
+ export function FixedCaption(fieldName: string = "Caption") {
return `<div style="position:absolute; height:30px; bottom:0; width:100%">
<div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
+ FormattedTextBox.LayoutString(fieldName + "Key") +
diff --git a/src/client/northstar/core/brusher/BrushLinkModel.ts b/src/client/northstar/core/brusher/BrushLinkModel.ts
deleted file mode 100644
index e3ac43367..000000000
--- a/src/client/northstar/core/brusher/BrushLinkModel.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { IBaseBrushable } from '../brusher/IBaseBrushable'
-import { IBaseBrusher } from '../brusher/IBaseBrusher'
-import { Utils } from '../../utils/Utils'
-import { IEquatable } from '../../utils/IEquatable';
-
-export class BrushLinkModel<T> implements IEquatable {
-
- public From: IBaseBrusher<T>;
-
- public To: IBaseBrushable<T>;
-
- public Color: number = 0;
-
- constructor(from: IBaseBrusher<T>, to: IBaseBrushable<T>) {
- this.From = from;
- this.To = to;
- }
-
- public static overlaps(start: number, end: number, otherstart: number, otherend: number): boolean {
- if (start > otherend || otherstart > end)
- return false;
- return true;
- }
- public static Connected<T>(from: IBaseBrusher<T>, to: IBaseBrushable<T>): boolean {
- var connected = (Math.abs(from.Position.x + from.Size.x - to.Position.x) <= 60 &&
- this.overlaps(from.Position.y, from.Position.y + from.Size.y, to.Position.y, to.Position.y + to.Size.y)
- ) ||
- (Math.abs(to.Position.x + to.Size.x - from.Position.x) <= 60 &&
- this.overlaps(to.Position.y, to.Position.y + to.Size.y, from.Position.y, from.Position.y + from.Size.y)
- );
- return connected;
- }
-
- public Equals(other: Object): boolean {
- if (!Utils.EqualityHelper(this, other)) return false;
- if (!this.From.Equals((other as BrushLinkModel<T>).From)) return false;
- if (!this.To.Equals((other as BrushLinkModel<T>).To)) return false;
- return true;
- }
-} \ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrushable.ts b/src/client/northstar/core/brusher/IBaseBrushable.ts
index 07d4e7580..99a36636f 100644
--- a/src/client/northstar/core/brusher/IBaseBrushable.ts
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -1,9 +1,9 @@
-import { BrushLinkModel } from '../brusher/BrushLinkModel'
import { PIXIPoint } from '../../utils/MathUtil'
import { IEquatable } from '../../utils/IEquatable';
+import { Document } from '../../../../fields/Document'
export interface IBaseBrushable<T> extends IEquatable {
- BrusherModels: Array<BrushLinkModel<T>>;
+ BrusherModels: Array<Document>;
BrushColors: Array<number>;
Position: PIXIPoint;
Size: PIXIPoint;
diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts
index 3c4cfc4a7..aee99d2b6 100644
--- a/src/client/northstar/core/filter/FilterModel.ts
+++ b/src/client/northstar/core/filter/FilterModel.ts
@@ -1,5 +1,11 @@
import { ValueComparison } from "./ValueComparision";
import { Utils } from "../../utils/Utils";
+import { IBaseFilterProvider } from "./IBaseFilterProvider";
+import { FilterOperand } from "./FilterOperand";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { FieldWaiting } from "../../../../fields/Field";
+import { Document } from "../../../../fields/Document";
+import { HistogramField } from "../../dash-fields/HistogramField";
export class FilterModel {
public ValueComparisons: ValueComparison[];
@@ -27,55 +33,52 @@ export class FilterModel {
}
public ToPythonString(): string {
- let ret = "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")";
- return ret;
+ return "(" + this.ValueComparisons.map(vc => vc.ToPythonString()).join("&&") + ")";
}
public static And(filters: string[]): string {
let ret = filters.filter(f => f !== "").join(" && ");
return ret;
}
+ public static GetFilterModelsRecursive(baseOperation: IBaseFilterProvider, visitedFilterProviders: Set<IBaseFilterProvider>, filterModels: FilterModel[], isFirst: boolean): string {
+ let ret = "";
+ visitedFilterProviders.add(baseOperation);
+ let filtered = baseOperation.FilterModels.filter(fm => fm && fm.ValueComparisons.length > 0);
+ if (!isFirst && filtered.length > 0) {
+ filterModels.push(...filtered);
+ ret = "(" + baseOperation.FilterModels.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
+ }
+ if (Utils.isBaseFilterConsumer(baseOperation) && baseOperation.Links) {
+ let children = new Array<string>();
+ let linkedGraphNodes = baseOperation.Links;
+ linkedGraphNodes.map(linkVm => {
+ let filterDoc = linkVm.Get(KeyStore.LinkedFromDocs);
+ if (filterDoc && filterDoc != FieldWaiting && filterDoc instanceof Document) {
+ let filterHistogram = filterDoc.GetT(KeyStore.Data, HistogramField);
+ if (filterHistogram && filterHistogram != FieldWaiting) {
+ if (!visitedFilterProviders.has(filterHistogram.Data)) {
+ let child = FilterModel.GetFilterModelsRecursive(filterHistogram.Data, visitedFilterProviders, filterModels, false);
+ if (child !== "") {
+ // if (linkVm.IsInverted) {
+ // child = "! " + child;
+ // }
+ children.push(child);
+ }
+ }
+ }
+ }
+ });
- // public static GetFilterModelsRecursive(filterGraphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>,
- // visitedFilterProviders: Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>, filterModels: FilterModel[], isFirst: boolean): string {
- // let ret = "";
- // if (Utils.isBaseFilterProvider(filterGraphNode.Data)) {
- // visitedFilterProviders.add(filterGraphNode);
- // let filtered = filterGraphNode.Data.FilterModels.filter(fm => fm && fm.ValueComparisons.length > 0);
- // if (!isFirst && filtered.length > 0) {
- // filterModels.push(...filtered);
- // ret = "(" + filterGraphNode.Data.FilterModels.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
- // }
- // }
- // if (Utils.isBaseFilterConsumer(filterGraphNode.Data) && filterGraphNode.Links != null) {
- // let children = new Array<string>();
- // let linkedGraphNodes = filterGraphNode.Links.get(LinkType.Filter);
- // if (linkedGraphNodes != null) {
- // for (let i = 0; i < linkedGraphNodes.length; i++) {
- // let linkVm = linkedGraphNodes[i].Data;
- // let linkedGraphNode = linkedGraphNodes[i].Target;
- // if (!visitedFilterProviders.has(linkedGraphNode)) {
- // let child = FilterModel.GetFilterModelsRecursive(linkedGraphNode, visitedFilterProviders, filterModels, false);
- // if (child !== "") {
- // if (linkVm.IsInverted) {
- // child = "! " + child;
- // }
- // children.push(child);
- // }
- // }
- // }
- // }
-
- // let childrenJoined = children.join(filterGraphNode.Data.FilterOperand === FilterOperand.AND ? " && " : " || ");
- // if (children.length > 0) {
- // if (ret !== "") {
- // ret = "(" + ret + " && (" + childrenJoined + "))";
- // }
- // else {
- // ret = "(" + childrenJoined + ")";
- // }
- // }
- // }
- // return ret;
- // }
+ let childrenJoined = children.join(baseOperation.FilterOperand === FilterOperand.AND ? " && " : " || ");
+ if (children.length > 0) {
+ if (ret !== "") {
+ ret = "(" + ret + " && (" + childrenJoined + "))";
+ }
+ else {
+ ret = "(" + childrenJoined + ")";
+ }
+ }
+ }
+ return ret;
+ }
} \ No newline at end of file
diff --git a/src/client/northstar/core/filter/IBaseFilterConsumer.ts b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
index e687acb8a..93f66a154 100644
--- a/src/client/northstar/core/filter/IBaseFilterConsumer.ts
+++ b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
@@ -1,8 +1,10 @@
import { FilterOperand } from '../filter/FilterOperand'
import { IEquatable } from '../../utils/IEquatable'
+import { Document } from "../../../../fields/Document";
export interface IBaseFilterConsumer extends IEquatable {
FilterOperand: FilterOperand;
+ Links: Document[];
}
export function instanceOfIBaseFilterConsumer(object: any): object is IBaseFilterConsumer {
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
new file mode 100644
index 000000000..90be70b80
--- /dev/null
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -0,0 +1,73 @@
+import { action } from "mobx";
+import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel";
+import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel";
+import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation";
+import { BasicField } from "../../../fields/BasicField";
+import { Field, FieldId } from "../../../fields/Field";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Types } from "../../../server/Message";
+
+
+export class HistogramField extends BasicField<HistogramOperation> {
+ constructor(data?: HistogramOperation, id?: FieldId, save: boolean = true) {
+ super(data ? data : HistogramOperation.Empty, save, id);
+ }
+
+ omitKeys(obj: any, keys: any) {
+ var dup: any = {};
+ for (var key in obj) {
+ if (keys.indexOf(key) == -1) {
+ dup[key] = obj[key];
+ }
+ }
+ return dup;
+ }
+ toString(): string {
+ return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']));
+ }
+
+ Copy(): Field {
+ return new HistogramField(this.Data);
+ }
+
+ ToScriptString(): string {
+ return `new HistogramField("${this.Data}")`;
+ }
+
+
+ ToJson(): { type: Types, data: string, _id: string } {
+ return {
+ type: Types.HistogramOp,
+
+ data: this.toString(),
+ _id: this.Id
+ }
+ }
+
+ @action
+ static FromJson(id: string, data: any): HistogramField {
+ let jp = JSON.parse(data);
+ let X: AttributeTransformationModel | undefined;
+ let Y: AttributeTransformationModel | undefined;
+ let V: AttributeTransformationModel | undefined;
+
+ let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
+ if (schema) {
+ CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+ if (attr.displayName == jp.X.AttributeModel.Attribute.DisplayName) {
+ X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction);
+ }
+ if (attr.displayName == jp.Y.AttributeModel.Attribute.DisplayName) {
+ Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction);
+ }
+ if (attr.displayName == jp.V.AttributeModel.Attribute.DisplayName) {
+ V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction);
+ }
+ });
+ if (X && Y && V) {
+ return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization), id, false);
+ }
+ }
+ return new HistogramField(HistogramOperation.Empty, id, false);
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
new file mode 100644
index 000000000..43e768c62
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
@@ -0,0 +1,238 @@
+import React = require("react")
+import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
+import { ChartType } from '../../northstar/model/binRanges/VisualBinRange';
+import { AggregateFunction, Bin, Brush, DoubleValueAggregateResult, HistogramResult, MarginAggregateParameters, MarginAggregateResult } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+import { LABColor } from '../../northstar/utils/LABcolor';
+import { PIXIRectangle } from "../../northstar/utils/MathUtil";
+import { StyleConstants } from "../../northstar/utils/StyleContants";
+import { HistogramBox } from "./HistogramBox";
+import "./HistogramBoxPrimitives.scss";
+
+export class HistogramBinPrimitive {
+ constructor(init?: Partial<HistogramBinPrimitive>) {
+ Object.assign(this, init);
+ }
+ public DataValue: number = 0;
+ public Rect: PIXIRectangle = PIXIRectangle.EMPTY;
+ public MarginRect: PIXIRectangle = PIXIRectangle.EMPTY;
+ public MarginPercentage: number = 0;
+ public Color: number = StyleConstants.WARNING_COLOR;
+ public Opacity: number = 1;
+ public BrushIndex: number = 0;
+ public BarAxis: number = -1;
+}
+
+export class HistogramBinPrimitiveCollection {
+ private static TOLERANCE: number = 0.0001;
+
+ private _histoBox: HistogramBox;
+ private get histoOp() { return this._histoBox.HistoOp; }
+ private get histoResult() { return this.histoOp.Result as HistogramResult; }
+ private get sizeConverter() { return this._histoBox.SizeConverter!; }
+ public BinPrimitives: Array<HistogramBinPrimitive> = new Array<HistogramBinPrimitive>();
+ public HitGeom: PIXIRectangle = PIXIRectangle.EMPTY;
+
+ constructor(bin: Bin, histoBox: HistogramBox) {
+ this._histoBox = histoBox;
+ let brushing = this.setupBrushing(bin, this.histoOp.Normalization); // X= 0, Y = 1, V = 2
+
+ brushing.orderedBrushes.reduce((brushFactorSum, brush) => {
+ switch (histoBox.ChartType) {
+ case ChartType.VerticalBar: return this.createVerticalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
+ case ChartType.HorizontalBar: return this.createHorizontalBarChartBinPrimitives(bin, brush, brushing.maxAxis, this.histoOp.Normalization);
+ case ChartType.SinglePoint: return this.createSinglePointChartBinPrimitives(bin, brush);
+ case ChartType.HeatMap: return this.createHeatmapBinPrimitives(bin, brush, brushFactorSum);
+ }
+ }, 0);
+
+ // adjust brush rects (stacking or not)
+ var allBrushIndex = ModelHelpers.AllBrushIndex(this.histoResult);
+ var filteredBinPrims = this.BinPrimitives.filter(b => b.BrushIndex != allBrushIndex && b.DataValue != 0.0);
+ filteredBinPrims.reduce((sum, fbp) => {
+ if (histoBox.ChartType == ChartType.VerticalBar) {
+ if (this.histoOp.Y.AggregateFunction == AggregateFunction.Count) {
+ fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y - sum, fbp.Rect.width, fbp.Rect.height);
+ fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - sum, fbp.MarginRect.width, fbp.MarginRect.height);
+ return sum + fbp.Rect.height;
+ }
+ if (this.histoOp.Y.AggregateFunction == AggregateFunction.Avg) {
+ var w = fbp.Rect.width / 2.0;
+ fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width / filteredBinPrims.length, fbp.Rect.height);
+ fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x - w + sum + (fbp.Rect.width / 2.0), fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
+ return sum + fbp.Rect.width;
+ }
+ }
+ else if (histoBox.ChartType == ChartType.HorizontalBar) {
+ if (this.histoOp.X.AggregateFunction == AggregateFunction.Count) {
+ fbp.Rect = new PIXIRectangle(fbp.Rect.x + sum, fbp.Rect.y, fbp.Rect.width, fbp.Rect.height);
+ fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x + sum, fbp.MarginRect.y, fbp.MarginRect.width, fbp.MarginRect.height);
+ return sum + fbp.Rect.width;
+ }
+ if (this.histoOp.X.AggregateFunction == AggregateFunction.Avg) {
+ var h = fbp.Rect.height / 2.0;
+ fbp.Rect = new PIXIRectangle(fbp.Rect.x, fbp.Rect.y + sum, fbp.Rect.width, fbp.Rect.height / filteredBinPrims.length);
+ fbp.MarginRect = new PIXIRectangle(fbp.MarginRect.x, fbp.MarginRect.y - h + sum + (fbp.Rect.height / 2.0), fbp.MarginRect.width, fbp.MarginRect.height);
+ return sum + fbp.Rect.height;
+ }
+ }
+ return 0;
+ }, 0);
+ this.BinPrimitives = this.BinPrimitives.reverse();
+ var f = this.BinPrimitives.filter(b => b.BrushIndex == allBrushIndex);
+ this.HitGeom = f.length > 0 ? f[0].Rect : PIXIRectangle.EMPTY;
+ }
+
+ private setupBrushing(bin: Bin, normalization: number) {
+ var overlapBrushIndex = ModelHelpers.OverlapBrushIndex(this.histoResult);
+ var orderedBrushes = [this.histoResult.brushes![0], this.histoResult.brushes![overlapBrushIndex]];
+ this.histoResult.brushes!.map(brush => brush.brushIndex != 0 && brush.brushIndex != overlapBrushIndex && orderedBrushes.push(brush));
+ return {
+ orderedBrushes,
+ maxAxis: orderedBrushes.reduce((prev, Brush) => {
+ let aggResult = this.getBinValue(normalization, bin, Brush.brushIndex!);
+ return aggResult != undefined && aggResult > prev ? aggResult : prev;
+ }, Number.MIN_VALUE)
+ };
+ }
+
+ private createHeatmapBinPrimitives(bin: Bin, brush: Brush, brushFactorSum: number): number {
+
+ let unNormalizedValue = this.getBinValue(2, bin, brush.brushIndex!);
+ if (unNormalizedValue == undefined)
+ return brushFactorSum;
+
+ var normalizedValue = (unNormalizedValue - this._histoBox.ValueRange[0]) / (Math.abs((this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0])) < HistogramBinPrimitiveCollection.TOLERANCE ?
+ unNormalizedValue : this._histoBox.ValueRange[1] - this._histoBox.ValueRange[0]);
+
+ let allUnNormalizedValue = this.getBinValue(2, bin, ModelHelpers.AllBrushIndex(this.histoResult))
+
+ // bcz: are these calls needed?
+ let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
+ let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
+
+ var returnBrushFactorSum = brushFactorSum;
+ if (allUnNormalizedValue != undefined) {
+ var brushFactor = (unNormalizedValue / allUnNormalizedValue);
+ returnBrushFactorSum += brushFactor;
+ returnBrushFactorSum = Math.min(returnBrushFactorSum, 1.0);
+
+ var tempRect = new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo);
+ var ratio = (tempRect.width / tempRect.height);
+ var newHeight = Math.sqrt((1.0 / ratio) * ((tempRect.width * tempRect.height) * returnBrushFactorSum));
+ var newWidth = newHeight * ratio;
+
+ xFrom = (tempRect.x + (tempRect.width - newWidth) / 2.0);
+ yTo = (tempRect.y + (tempRect.height - newHeight) / 2.0);
+ xTo = (xFrom + newWidth);
+ yFrom = (yTo + newHeight);
+ }
+ var alpha = 0.0;
+ var color = this.baseColorFromBrush(brush);
+ var lerpColor = LABColor.Lerp(
+ LABColor.FromColor(StyleConstants.MIN_VALUE_COLOR),
+ LABColor.FromColor(color),
+ (alpha + Math.pow(normalizedValue, 1.0 / 3.0) * (1.0 - alpha)));
+ var dataColor = LABColor.ToColor(lerpColor);
+
+ this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, dataColor, 1, unNormalizedValue);
+ return returnBrushFactorSum;
+ }
+
+ private createSinglePointChartBinPrimitives(bin: Bin, brush: Brush): number {
+ let unNormalizedValue = this.getBinValue(2, bin, brush.brushIndex!);
+ if (unNormalizedValue != undefined) {
+ let [xFrom, xTo] = this.sizeConverter.DataToScreenPointRange(0, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.X, this.histoResult, brush.brushIndex!));
+ let [yFrom, yTo] = this.sizeConverter.DataToScreenPointRange(1, bin, ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, this.histoOp.Y, this.histoResult, brush.brushIndex!));
+
+ if (xFrom != undefined && yFrom != undefined && xTo != undefined && yTo != undefined)
+ this.createBinPrimitive(-1, brush, PIXIRectangle.EMPTY, 0, xFrom, xTo, yFrom, yTo, this.baseColorFromBrush(brush), 1, unNormalizedValue);
+ }
+ return 0;
+ }
+
+ private createVerticalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
+ let dataValue = this.getBinValue(1, bin, brush.brushIndex!);
+ if (dataValue != undefined) {
+ let [yFrom, yValue, yTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 1, binBrushMaxAxis);
+ let [xFrom, xTo] = this.sizeConverter.DataToScreenXAxisRange(this._histoBox.VisualBinRanges, 0, bin);
+
+ var yMarginAbsolute = this.getMargin(bin, brush, this.histoOp.Y);
+ var marginRect = new PIXIRectangle(xFrom + (xTo - xFrom) / 2.0 - 1,
+ this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute), 2,
+ this.sizeConverter.DataToScreenY(yValue - yMarginAbsolute) - this.sizeConverter.DataToScreenY(yValue + yMarginAbsolute));
+
+ this.createBinPrimitive(1, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
+ this.baseColorFromBrush(brush), normalization != 0 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[1] + 0.4, dataValue);
+ }
+ return 0;
+ }
+
+ private createHorizontalBarChartBinPrimitives(bin: Bin, brush: Brush, binBrushMaxAxis: number, normalization: number): number {
+ let dataValue = this.getBinValue(0, bin, brush.brushIndex!);
+ if (dataValue != undefined) {
+ let [xFrom, xValue, xTo] = this.sizeConverter.DataToScreenNormalizedRange(dataValue, normalization, 0, binBrushMaxAxis);
+ let [yFrom, yTo] = this.sizeConverter.DataToScreenYAxisRange(this._histoBox.VisualBinRanges, 1, bin);
+
+ var xMarginAbsolute = this.sizeConverter.IsSmall ? 0 : this.getMargin(bin, brush, this.histoOp.X);
+ var marginRect = new PIXIRectangle(this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
+ yTo + (yFrom - yTo) / 2.0 - 1,
+ this.sizeConverter.DataToScreenX(xValue + xMarginAbsolute) - this.sizeConverter.DataToScreenX(xValue - xMarginAbsolute),
+ 2.0);
+
+ this.createBinPrimitive(0, brush, marginRect, 0, xFrom, xTo, yFrom, yTo,
+ this.baseColorFromBrush(brush), normalization != 1 ? 1 : 0.6 * binBrushMaxAxis / this.sizeConverter.DataRanges[0] + 0.4, dataValue);
+ }
+ return 0;
+ }
+
+ public getBinValue(axis: number, bin: Bin, brushIndex: number) {
+ var aggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis == 0 ? this.histoOp.X : axis == 1 ? this.histoOp.Y : this.histoOp.V, this.histoResult, brushIndex);
+ let dataValue = ModelHelpers.GetAggregateResult(bin, aggregateKey) as DoubleValueAggregateResult;
+ return dataValue != null && dataValue.hasResult ? dataValue.result : undefined;
+ }
+
+ private getMargin(bin: Bin, brush: Brush, axis: AttributeTransformationModel) {
+ var marginParams = new MarginAggregateParameters();
+ marginParams.aggregateFunction = axis.AggregateFunction;
+ var marginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis, this.histoResult, brush.brushIndex!, marginParams);
+ var marginResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey) as MarginAggregateResult;
+ return !marginResult ? 0 : marginResult.absolutMargin!;
+ }
+
+ private createBinPrimitive(barAxis: number, brush: Brush, marginRect: PIXIRectangle,
+ marginPercentage: number, xFrom: number, xTo: number, yFrom: number, yTo: number, color: number, opacity: number, dataValue: number) {
+ var binPrimitive = new HistogramBinPrimitive(
+ {
+ Rect: new PIXIRectangle(xFrom, yTo, xTo - xFrom, yFrom - yTo),
+ MarginRect: marginRect,
+ MarginPercentage: marginPercentage,
+ BrushIndex: brush.brushIndex,
+ Color: color,
+ Opacity: opacity,
+ DataValue: dataValue,
+ BarAxis: barAxis
+ });
+ this.BinPrimitives.push(binPrimitive);
+ }
+
+ private baseColorFromBrush(brush: Brush): number {
+ let bc = StyleConstants.BRUSH_COLORS;
+ if (brush.brushIndex == ModelHelpers.RestBrushIndex(this.histoResult)) {
+ return StyleConstants.HIGHLIGHT_COLOR;
+ }
+ else if (brush.brushIndex == ModelHelpers.OverlapBrushIndex(this.histoResult)) {
+ return StyleConstants.OVERLAP_COLOR;
+ }
+ else if (brush.brushIndex == ModelHelpers.AllBrushIndex(this.histoResult)) {
+ return 0x00ff00;
+ }
+ else if (bc.length > 0) {
+ return bc[brush.brushIndex! % bc.length];
+ }
+ // else if (this.histoOp.BrushColors.length > 0) {
+ // return this.histoOp.BrushColors[brush.brushIndex! % this.histoOp.BrushColors.length];
+ // }
+ return StyleConstants.HIGHLIGHT_COLOR;
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBox.scss b/src/client/northstar/dash-nodes/HistogramBox.scss
new file mode 100644
index 000000000..94da36e29
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBox.scss
@@ -0,0 +1,39 @@
+.histogrambox-container {
+ padding: 0vw;
+ position: absolute;
+ top: 0;
+ left:0;
+ text-align: center;
+ width: 100%;
+ height: 100%;
+ background: black;
+ }
+ .histogrambox-xaxislabel {
+ position:absolute;
+ left:0;
+ width:100%;
+ text-align: center;
+ bottom:0;
+ background: lightgray;
+ font-size: 14;
+ font-weight: bold;
+ }
+ .histogrambox-yaxislabel {
+ position:absolute;
+ height:100%;
+ width: 25px;
+ left:0;
+ bottom:0;
+ background: lightgray;
+ }
+ .histogrambox-yaxislabel-text {
+ position:absolute;
+ left:0;
+ transform-origin: left;
+ transform: rotate(-90deg);
+ text-align: center;
+ font-size: 14;
+ font-weight: bold;
+ bottom: calc(50% - 25px);
+ }
+ \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
new file mode 100644
index 000000000..49ebe5ebc
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -0,0 +1,174 @@
+import React = require("react")
+import { action, computed, observable, reaction, runInAction, trace } from "mobx";
+import { observer } from "mobx-react";
+import Measure from "react-measure";
+import { FieldWaiting, Opt } from "../../../fields/Field";
+import { Document } from "../../../fields/Document";
+import { KeyStore } from "../../../fields/KeyStore";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';
+import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper";
+import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult, Result } from "../../northstar/model/idea/idea";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
+import { SizeConverter } from "../../northstar/utils/SizeConverter";
+import { DragManager } from "../../util/DragManager";
+import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
+import { HistogramField } from "../dash-fields/HistogramField";
+import "../utils/Extensions";
+import "./HistogramBox.scss";
+import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
+import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
+import { StyleConstants } from "../utils/StyleContants";
+
+
+@observer
+export class HistogramBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) }
+ private _dropXRef = React.createRef<HTMLDivElement>();
+ private _dropYRef = React.createRef<HTMLDivElement>();
+ private _dropXDisposer?: DragManager.DragDropDisposer;
+ private _dropYDisposer?: DragManager.DragDropDisposer;
+
+ @observable public PanelWidth: number = 100;
+ @observable public PanelHeight: number = 100;
+ @observable public HistoOp: HistogramOperation = HistogramOperation.Empty;
+ @observable public VisualBinRanges: VisualBinRange[] = [];
+ @observable public ValueRange: number[] = [];
+ @computed public get HistogramResult(): HistogramResult { return this.HistoOp.Result as HistogramResult; }
+ @observable public SizeConverter: SizeConverter = new SizeConverter();
+
+ @computed get createOperationParamsCache() { trace(); return this.HistoOp.CreateOperationParameters(); }
+ @computed get BinRanges() { return this.HistogramResult ? this.HistogramResult.binRanges : undefined; }
+ @computed get ChartType() {
+ return !this.BinRanges ? ChartType.SinglePoint : this.BinRanges[0] instanceof AggregateBinRange ?
+ (this.BinRanges[1] instanceof AggregateBinRange ? ChartType.SinglePoint : ChartType.HorizontalBar) :
+ this.BinRanges[1] instanceof AggregateBinRange ? ChartType.VerticalBar : ChartType.HeatMap;
+ }
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ }
+
+ @action
+ dropX = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
+ if (h && h != FieldWaiting) {
+ this.HistoOp.X = h.Data.X;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ @action
+ dropY = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
+ if (h && h != FieldWaiting) {
+ this.HistoOp.Y = h.Data.X;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ xLabelPointerDown = (e: React.PointerEvent) => {
+ this.HistoOp.X = new AttributeTransformationModel(this.HistoOp.X.AttributeModel, this.HistoOp.X.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
+ }
+ @action
+ yLabelPointerDown = (e: React.PointerEvent) => {
+ this.HistoOp.Y = new AttributeTransformationModel(this.HistoOp.Y.AttributeModel, this.HistoOp.Y.AggregateFunction == AggregateFunction.None ? AggregateFunction.Count : AggregateFunction.None);
+ }
+
+ componentDidMount() {
+ if (this._dropXRef.current) {
+ this._dropXDisposer = DragManager.MakeDropTarget(this._dropXRef.current, { handlers: { drop: this.dropX.bind(this) } });
+ }
+ if (this._dropYRef.current) {
+ this._dropYDisposer = DragManager.MakeDropTarget(this._dropYRef.current, { handlers: { drop: this.dropY.bind(this) } });
+ }
+ reaction(() => CurrentUserUtils.NorthstarDBCatalog, (catalog?: Catalog) => this.activateHistogramOperation(catalog), { fireImmediately: true });
+ reaction(() => [this.VisualBinRanges && this.VisualBinRanges.slice()], () => this.SizeConverter.SetVisualBinRanges(this.VisualBinRanges));
+ reaction(() => [this.PanelHeight, this.PanelWidth], () => this.SizeConverter.SetIsSmall(this.PanelWidth < 40 && this.PanelHeight < 40))
+ reaction(() => this.HistogramResult ? this.HistogramResult.binRanges : undefined,
+ (binRanges: BinRange[] | undefined) => {
+ if (binRanges) {
+ this.VisualBinRanges.splice(0, this.VisualBinRanges.length, ...binRanges.map((br, ind) =>
+ VisualBinRangeHelper.GetVisualBinRange(this.HistoOp.Schema!.distinctAttributeParameters, br, this.HistogramResult!, ind ? this.HistoOp.Y : this.HistoOp.X, this.ChartType)));
+
+ let valueAggregateKey = ModelHelpers.CreateAggregateKey(this.HistoOp.Schema!.distinctAttributeParameters, this.HistoOp.V, this.HistogramResult!, ModelHelpers.AllBrushIndex(this.HistogramResult!));
+ this.ValueRange = Object.values(this.HistogramResult!.bins!).reduce((prev, cur) => {
+ let value = ModelHelpers.GetAggregateResult(cur, valueAggregateKey) as DoubleValueAggregateResult;
+ return value && value.hasResult ? [Math.min(prev[0], value.result!), Math.max(prev[1], value.result!)] : prev;
+ }, [Number.MAX_VALUE, Number.MIN_VALUE]);
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ if (this._dropXDisposer)
+ this._dropXDisposer();
+ if (this._dropYDisposer)
+ this._dropYDisposer();
+ }
+
+ activateHistogramOperation(catalog?: Catalog) {
+ if (catalog) {
+ this.props.doc.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
+ this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
+ if (this.HistoOp != HistogramOperation.Empty) {
+ reaction(() => this.props.doc.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+ reaction(() => this.props.doc.GetList(KeyStore.BrushingDocs, []).length,
+ () => {
+ let brushingDocs = this.props.doc.GetList(KeyStore.BrushingDocs, [] as Document[]);
+ let proto = this.props.doc.GetPrototype() as Document;
+ this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
+ brush.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]);
+ let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
+ return { l: brush, b: brushed[0].Id == proto.Id ? brushed[1] : brushed[0] }
+ }));
+ }, { fireImmediately: true });
+ reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
+ }
+ }));
+ }
+ }
+ render() {
+ let labelY = this.HistoOp && this.HistoOp.Y ? this.HistoOp.Y.PresentedName : "<...>";
+ let labelX = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.PresentedName : "<...>";
+ var h = this.props.isTopMost ? this.PanelHeight : this.props.doc.GetNumber(KeyStore.Height, 0);
+ var w = this.props.isTopMost ? this.PanelWidth : this.props.doc.GetNumber(KeyStore.Width, 0);
+ let loff = this.SizeConverter.LeftOffset;
+ let toff = this.SizeConverter.TopOffset;
+ let roff = this.SizeConverter.RightOffset;
+ let boff = this.SizeConverter.BottomOffset;
+ return (
+ <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height })}>
+ {({ measureRef }) =>
+ <div className="histogrambox-container" ref={measureRef} style={{ transform: `translate(${-w / 2}px, ${-h / 2}px)` }}>
+ <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} >
+ <span className="histogrambox-yaxislabel-text">
+ {labelY}
+ </span>
+ </div>
+ <div className="histogrambox-primitives" style={{
+ transform: `translate(${loff + 25}px, ${toff}px)`,
+ width: `calc(100% - ${loff + roff + 25}px)`,
+ height: `calc(100% - ${toff + boff}px)`,
+ }}>
+ <HistogramLabelPrimitives HistoBox={this} />
+ <HistogramBoxPrimitives HistoBox={this} />
+ </div>
+ <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} >
+ {labelX}
+ </div>
+ </div>
+ }
+ </Measure>
+ )
+ }
+}
+
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss
new file mode 100644
index 000000000..ce9edd65e
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.scss
@@ -0,0 +1,30 @@
+.histogramboxprimitives-container {
+ width: 100%;
+ height: 100%;
+}
+.histogramboxprimitives-border {
+ border: 3px;
+ border-style: solid;
+ border-color: white;
+ pointer-events: none;
+ position: absolute;
+}
+.histogramboxprimitives-bar {
+ position: absolute;
+ border: 1px;
+ border-style: solid;
+ border-color: #282828;
+ pointer-events: all;
+}
+
+.histogramboxprimitives-placer {
+ position: absolute;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+}
+.histogramboxprimitives-line {
+ position: absolute;
+ background: darkGray;
+ opacity: 0.4;
+} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
new file mode 100644
index 000000000..e9adb3ce5
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -0,0 +1,124 @@
+import React = require("react")
+import { computed, observable, reaction, runInAction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Utils as DashUtils } from '../../../Utils';
+import { FilterModel } from "../../northstar/core/filter/FilterModel";
+import { ModelHelpers } from "../../northstar/model/ModelHelpers";
+import { ArrayUtil } from "../../northstar/utils/ArrayUtil";
+import { LABColor } from '../../northstar/utils/LABcolor';
+import { PIXIRectangle } from "../../northstar/utils/MathUtil";
+import { StyleConstants } from "../../northstar/utils/StyleContants";
+import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection";
+import { HistogramBox } from "./HistogramBox";
+import "./HistogramBoxPrimitives.scss";
+
+export interface HistogramPrimitivesProps {
+ HistoBox: HistogramBox;
+}
+@observer
+export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesProps> {
+ private get histoOp() { return this.props.HistoBox.HistoOp; }
+ private get renderDimension() { return this.props.HistoBox.SizeConverter.RenderDimension; }
+ @observable _selectedPrims: HistogramBinPrimitive[] = [];
+ @computed get xaxislines() { return this.renderGridLinesAndLabels(0); }
+ @computed get yaxislines() { return this.renderGridLinesAndLabels(1); }
+ @computed get selectedPrimitives() { return this._selectedPrims.map(bp => this.drawRect(bp.Rect, bp.BarAxis, undefined, "border")); }
+ @computed get binPrimitives() {
+ let histoResult = this.props.HistoBox.HistogramResult;
+ if (!histoResult || !histoResult.bins || !this.props.HistoBox.VisualBinRanges.length)
+ return (null);
+ trace();
+ let allBrushIndex = ModelHelpers.AllBrushIndex(histoResult);
+ return Object.keys(histoResult.bins).reduce((prims, key) => {
+ let drawPrims = new HistogramBinPrimitiveCollection(histoResult!.bins![key], this.props.HistoBox);
+ let toggle = this.getSelectionToggle(drawPrims.BinPrimitives, allBrushIndex,
+ ModelHelpers.GetBinFilterModel(histoResult!.bins![key], allBrushIndex, histoResult!, this.histoOp.X, this.histoOp.Y));
+ drawPrims.BinPrimitives.filter(bp => bp.DataValue && bp.BrushIndex !== allBrushIndex).map(bp =>
+ prims.push(...[{ r: bp.Rect, c: bp.Color }, { r: bp.MarginRect, c: StyleConstants.MARGIN_BARS_COLOR }].map(pair => this.drawRect(pair.r, bp.BarAxis, pair.c, "bar", toggle))));
+ return prims;
+ }, [] as JSX.Element[]);
+ }
+
+ componentDidMount() {
+ reaction(() => this.props.HistoBox.HistoOp.FilterString, () => this._selectedPrims.length = this.histoOp.FilterModels.length = 0);
+ }
+
+ private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) {
+ let allBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex == allBrushIndex);
+ return !allBrushPrim ? () => { } : () => runInAction(() => {
+ if (ArrayUtil.Contains(this.histoOp.FilterModels, filterModel)) {
+ this._selectedPrims.splice(this._selectedPrims.indexOf(allBrushPrim!), 1);
+ this.histoOp.RemoveFilterModels([filterModel]);
+ }
+ else {
+ this._selectedPrims.push(allBrushPrim!);
+ this.histoOp.AddFilterModels([filterModel]);
+ }
+ })
+ }
+
+ private renderGridLinesAndLabels(axis: number) {
+ if (!this.props.HistoBox.SizeConverter.Initialized)
+ return (null);
+ let labels = this.props.HistoBox.VisualBinRanges[axis].GetLabels();
+ return labels.reduce((prims, binLabel, i) => {
+ let r = this.props.HistoBox.SizeConverter.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis);
+ prims.push(this.drawLine(r.xFrom, r.yFrom, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0));
+ if (i == labels.length - 1)
+ prims.push(this.drawLine(axis == 0 ? r.xTo : r.xFrom, axis == 0 ? r.yFrom : r.yTo, axis == 0 ? 0 : r.xTo - r.xFrom, axis == 0 ? r.yTo - r.yFrom : 0));
+ return prims;
+ }, [] as JSX.Element[]);
+ }
+
+ drawEntity(xFrom: number, yFrom: number, entity: JSX.Element) {
+ let transXpercent = xFrom / this.renderDimension * 100;
+ let transYpercent = yFrom / this.renderDimension * 100;
+ return (<div key={DashUtils.GenerateGuid()} className={`histogramboxprimitives-placer`} style={{ transform: `translate(${transXpercent}%, ${transYpercent}%)` }}>
+ {entity}
+ </div>);
+ }
+ drawLine(xFrom: number, yFrom: number, width: number, height: number) {
+ if (height < 0) {
+ yFrom += height;
+ height = -height;
+ }
+ if (width < 0) {
+ xFrom += width;
+ width = -width;
+ }
+ let trans2Xpercent = width == 0 ? `1px` : `${(xFrom + width) / this.renderDimension * 100}%`;
+ let trans2Ypercent = height == 0 ? `1px` : `${(yFrom + height) / this.renderDimension * 100}%`;
+ let line = (<div className="histogramboxprimitives-line" style={{ width: trans2Xpercent, height: trans2Ypercent, }} />);
+ return this.drawEntity(xFrom, yFrom, line);
+ }
+ drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = () => { }) {
+ if (r.height < 0) {
+ r.y += r.height;
+ r.height = -r.height;
+ }
+ if (r.width < 0) {
+ r.x += r.width;
+ r.width = -r.width;
+ }
+ let widthPercent = r.width / this.renderDimension * 100;
+ let heightPercent = r.height / this.renderDimension * 100;
+ let rect = (<div className={`histogramboxprimitives-${classExt}`} onPointerDown={(e: React.PointerEvent) => { if (e.button == 0) tapHandler() }}
+ style={{
+ borderBottomStyle: barAxis == 1 ? "none" : "solid",
+ borderLeftStyle: barAxis == 0 ? "none" : "solid",
+ width: `${widthPercent}%`,
+ height: `${heightPercent}%`,
+ background: color ? `${LABColor.RGBtoHexString(color)}` : ""
+ }}
+ />);
+ return this.drawEntity(r.x, r.y, rect);
+ }
+ render() {
+ return <div className="histogramboxprimitives-container">
+ {this.xaxislines}
+ {this.yaxislines}
+ {this.binPrimitives}
+ {this.selectedPrimitives}
+ </div>
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss
new file mode 100644
index 000000000..304d33771
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.scss
@@ -0,0 +1,13 @@
+
+ .histogramLabelPrimitives-gridlabel {
+ position:absolute;
+ transform-origin: left top;
+ font-size: 11;
+ color:white;
+ }
+ .histogramLabelPrimitives-placer {
+ position:absolute;
+ width:100%;
+ height:100%;
+ pointer-events: none;
+ } \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx
new file mode 100644
index 000000000..93b237deb
--- /dev/null
+++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx
@@ -0,0 +1,79 @@
+import React = require("react")
+import { action, computed, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Utils as DashUtils } from '../../../Utils';
+import { NominalVisualBinRange } from "../model/binRanges/NominalVisualBinRange";
+import "../utils/Extensions";
+import { StyleConstants } from "../utils/StyleContants";
+import { HistogramBox } from "./HistogramBox";
+import "./HistogramLabelPrimitives.scss";
+import { HistogramPrimitivesProps } from "./HistogramBoxPrimitives";
+
+@observer
+export class HistogramLabelPrimitives extends React.Component<HistogramPrimitivesProps> {
+ componentDidMount() {
+ reaction(() => [this.props.HistoBox.PanelWidth, this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length],
+ (fields) => HistogramLabelPrimitives.computeLabelAngle(fields[0] as number, fields[1] as number, this.props.HistoBox), { fireImmediately: true });
+ }
+
+ @action
+ static computeLabelAngle(panelWidth: number, leftOffset: number, histoBox: HistogramBox) {
+ const textWidth = 30;
+ if (panelWidth > 0 && histoBox.VisualBinRanges.length && histoBox.VisualBinRanges[0] instanceof NominalVisualBinRange) {
+ let space = (panelWidth - leftOffset * 2) / histoBox.VisualBinRanges[0].GetBins().length;
+ histoBox.SizeConverter.SetLabelAngle(Math.min(Math.PI / 2, Math.max(Math.PI / 6, textWidth / space * Math.PI / 2)));
+ } else if (histoBox.SizeConverter.LabelAngle) {
+ histoBox.SizeConverter.SetLabelAngle(0);
+ }
+ }
+ @computed get xaxislines() { return this.renderGridLinesAndLabels(0); }
+ @computed get yaxislines() { return this.renderGridLinesAndLabels(1); }
+
+ private renderGridLinesAndLabels(axis: number) {
+ let sc = this.props.HistoBox.SizeConverter;
+ let vb = this.props.HistoBox.VisualBinRanges;
+ if (!vb.length || !sc.Initialized)
+ return (null);
+ let dim = (axis == 0 ? this.props.HistoBox.PanelWidth : this.props.HistoBox.PanelHeight) / ((axis == 0 && vb[axis] instanceof NominalVisualBinRange) ?
+ (12 + 5) : // (<number>FontStyles.AxisLabel.fontSize + 5)));
+ sc.MaxLabelSizes[axis].coords[axis] + 5);
+
+ let labels = vb[axis].GetLabels();
+ return labels.reduce((prims, binLabel, i) => {
+ let r = sc.DataToScreenRange(binLabel.minValue!, binLabel.maxValue!, axis);
+ if (i % Math.ceil(labels.length / dim) === 0 && binLabel.label) {
+ const label = binLabel.label.Truncate(StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS, "...");
+ const textHeight = 14; const textWidth = 30;
+ let xStart = (axis === 0 ? r.xFrom + (r.xTo - r.xFrom) / 2.0 : r.xFrom - 10 - textWidth);
+ let yStart = (axis === 1 ? r.yFrom - textHeight / 2 : r.yFrom);
+
+ if (axis == 0 && vb[axis] instanceof NominalVisualBinRange) {
+ let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.PanelWidth;
+ xStart += Math.max(textWidth / 2, (1 - textWidth / space) * textWidth / 2) - textHeight / 2;
+ }
+
+ let xPercent = axis == 1 ? `${xStart}px` : `${xStart / sc.RenderDimension * 100}%`
+ let yPercent = axis == 0 ? `${this.props.HistoBox.PanelHeight - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%`
+
+ prims.push(
+ <div className="histogramLabelPrimitives-placer" key={DashUtils.GenerateGuid()} style={{ transform: `translate(${xPercent}, ${yPercent})` }}>
+ <div className="histogramLabelPrimitives-gridlabel" style={{ transform: `rotate(${axis == 0 ? sc.LabelAngle : 0}rad)` }}>
+ {label}
+ </div>
+ </div>
+ )
+ }
+ return prims;
+ }, [] as JSX.Element[]);
+ }
+
+ render() {
+ let xaxislines = this.xaxislines;
+ let yaxislines = this.yaxislines;
+ return <div className="histogramLabelPrimitives-container">
+ {xaxislines}
+ {yaxislines}
+ </div>
+ }
+
+} \ No newline at end of file
diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts
index e1241b3ef..d0711fb69 100644
--- a/src/client/northstar/model/ModelHelpers.ts
+++ b/src/client/northstar/model/ModelHelpers.ts
@@ -9,15 +9,15 @@ import { AlphabeticVisualBinRange } from "./binRanges/AlphabeticVisualBinRange";
import { NominalVisualBinRange } from "./binRanges/NominalVisualBinRange";
import { VisualBinRangeHelper } from "./binRanges/VisualBinRangeHelper";
import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
-import { Main } from "../../views/Main";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
export class ModelHelpers {
- public static CreateAggregateKey(atm: AttributeTransformationModel, histogramResult: HistogramResult,
+ public static CreateAggregateKey(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel, histogramResult: HistogramResult,
brushIndex: number, aggParameters?: SingleDimensionAggregateParameters): AggregateKey {
{
if (aggParameters == undefined) {
- aggParameters = ModelHelpers.GetAggregateParameter(atm);
+ aggParameters = ModelHelpers.GetAggregateParameter(distinctAttributeParameters, atm);
}
else {
aggParameters.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
@@ -34,40 +34,40 @@ export class ModelHelpers {
return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters);
}
- public static GetAggregateParameter(atm: AttributeTransformationModel): AggregateParameters | undefined {
+ public static GetAggregateParameter(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel): AggregateParameters | undefined {
var aggParam: AggregateParameters | undefined;
if (atm.AggregateFunction === AggregateFunction.Avg) {
var avg = new AverageAggregateParameters();
avg.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
- avg.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ avg.distinctAttributeParameters = distinctAttributeParameters;
aggParam = avg;
}
else if (atm.AggregateFunction === AggregateFunction.Count) {
var cnt = new CountAggregateParameters();
cnt.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
- cnt.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ cnt.distinctAttributeParameters = distinctAttributeParameters;
aggParam = cnt;
}
else if (atm.AggregateFunction === AggregateFunction.Sum) {
var sum = new SumAggregateParameters();
sum.attributeParameters = ModelHelpers.GetAttributeParameters(atm.AttributeModel);
- sum.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ sum.distinctAttributeParameters = distinctAttributeParameters;
aggParam = sum;
}
return aggParam;
}
- public static GetAggregateParametersWithMargins(atms: Array<AttributeTransformationModel>): Array<AggregateParameters> {
+ public static GetAggregateParametersWithMargins(distinctAttributeParameters: AttributeParameters | undefined, atms: Array<AttributeTransformationModel>): Array<AggregateParameters> {
var aggregateParameters = new Array<AggregateParameters>();
atms.forEach(agg => {
- var aggParams = ModelHelpers.GetAggregateParameter(agg);
+ var aggParams = ModelHelpers.GetAggregateParameter(distinctAttributeParameters, agg);
if (aggParams) {
aggregateParameters.push(aggParams);
- var margin = new MarginAggregateParameters();
- margin.attributeParameters = ModelHelpers.GetAttributeParameters(agg.AttributeModel);
- margin.distinctAttributeParameters = Main.Instance.ActiveSchema!.distinctAttributeParameters;
+ var margin = new MarginAggregateParameters()
margin.aggregateFunction = agg.AggregateFunction;
+ margin.attributeParameters = ModelHelpers.GetAttributeParameters(agg.AttributeModel);
+ margin.distinctAttributeParameters = distinctAttributeParameters;
aggregateParameters.push(margin);
}
});
diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
index 9eae39800..53d585bb4 100644
--- a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
+++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts
@@ -1,4 +1,4 @@
-import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult } from "../idea/idea";
+import { BinRange, NominalBinRange, QuantitativeBinRange, Exception, AlphabeticBinRange, DateTimeBinRange, AggregateBinRange, DoubleValueAggregateResult, HistogramResult, AttributeParameters } from "../idea/idea";
import { VisualBinRange, ChartType } from "./VisualBinRange";
import { NominalVisualBinRange } from "./NominalVisualBinRange";
import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange";
@@ -30,13 +30,13 @@ export class VisualBinRangeHelper {
throw new Exception()
}
- public static GetVisualBinRange(dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange {
+ public static GetVisualBinRange(distinctAttributeParameters: AttributeParameters | undefined, dataBinRange: BinRange, histoResult: HistogramResult, attr: AttributeTransformationModel, chartType: ChartType): VisualBinRange {
if (!(dataBinRange instanceof AggregateBinRange)) {
return VisualBinRangeHelper.GetNonAggregateVisualBinRange(dataBinRange);
}
else {
- var aggregateKey = ModelHelpers.CreateAggregateKey(attr, histoResult, ModelHelpers.AllBrushIndex(histoResult));
+ var aggregateKey = ModelHelpers.CreateAggregateKey(distinctAttributeParameters, attr, histoResult, ModelHelpers.AllBrushIndex(histoResult));
var minValue = Number.MAX_VALUE;
var maxValue = Number.MIN_VALUE;
for (var b = 0; b < histoResult.brushes!.length; b++) {
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
index 4c0303a48..f545b2c58 100644
--- a/src/client/northstar/operations/BaseOperation.ts
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -10,9 +10,10 @@ export abstract class BaseOperation {
@observable public Error: string = "";
@observable public OverridingFilters: FilterModel[] = [];
- @observable public Result: Result | undefined;
+ //@observable
+ @observable public Result?: Result = undefined;
@observable public ComputationStarted: boolean = false;
- public OperationReference: OperationReference | undefined = undefined;
+ public OperationReference?: OperationReference = undefined;
private static _nextId = 0;
public RequestSalt: string = "";
@@ -25,6 +26,8 @@ export abstract class BaseOperation {
@computed
public get FilterString(): string {
+ // let filterModels: FilterModel[] = [];
+ // return FilterModel.GetFilterModelsRecursive(this, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true)
// if (this.OverridingFilters.length > 0) {
// return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")";
// }
@@ -59,7 +62,8 @@ export abstract class BaseOperation {
}
let operationParameters = this.CreateOperationParameters();
- this.Result = undefined;
+ if (this.Result)
+ this.Result.progress = 0; // bcz: used to set Result to undefined, but that causes the display to blink
this.Error = "";
let salt = Math.random().toString();
this.RequestSalt = salt;
diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index a4f5cac70..e63de1632 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -1,65 +1,86 @@
-import { reaction, computed, action } from "mobx";
-import { Attribute, DataType, QuantitativeBinRange, HistogramOperationParameters, AggregateParameters, AggregateFunction, AverageAggregateParameters } from "../model/idea/idea";
-import { ArrayUtil } from "../utils/ArrayUtil";
+import { action, computed, observable, trace } from "mobx";
+import { Document } from "../../../fields/Document";
+import { FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
+import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
import { CalculatedAttributeManager } from "../core/attribute/CalculatedAttributeModel";
+import { FilterModel } from "../core/filter/FilterModel";
+import { FilterOperand } from "../core/filter/FilterOperand";
+import { IBaseFilterConsumer } from "../core/filter/IBaseFilterConsumer";
+import { IBaseFilterProvider } from "../core/filter/IBaseFilterProvider";
+import { HistogramField } from "../dash-fields/HistogramField";
+import { SETTINGS_SAMPLE_SIZE, SETTINGS_X_BINS, SETTINGS_Y_BINS } from "../model/binRanges/VisualBinRangeHelper";
+import { AggregateFunction, AggregateParameters, Attribute, AverageAggregateParameters, Bin, DataType, DoubleValueAggregateResult, HistogramOperationParameters, HistogramResult, QuantitativeBinRange } from "../model/idea/idea";
import { ModelHelpers } from "../model/ModelHelpers";
-import { SETTINGS_X_BINS, SETTINGS_Y_BINS, SETTINGS_SAMPLE_SIZE } from "../model/binRanges/VisualBinRangeHelper";
-import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
-import { Main } from "../../views/Main";
+import { ArrayUtil } from "../utils/ArrayUtil";
import { BaseOperation } from "./BaseOperation";
+export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
+ public static Empty = new HistogramOperation("-empty schema-", new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())));
+ @observable public FilterOperand: FilterOperand = FilterOperand.AND;
+ @observable public Links: Document[] = [];
+ @observable public BrushLinks: { l: Document, b: Document }[] = [];
+ @observable public BrushColors: number[] = [];
+ @observable public FilterModels: FilterModel[] = [];
+
+ @observable public Normalization: number = -1;
+ @observable public X: AttributeTransformationModel;
+ @observable public Y: AttributeTransformationModel;
+ @observable public V: AttributeTransformationModel;
+ @observable public SchemaName: string;
+ @observable public QRange: QuantitativeBinRange | undefined;
+ @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); }
-export class HistogramOperation extends BaseOperation {
- public X: AttributeTransformationModel;
- public Y: AttributeTransformationModel;
- public V: AttributeTransformationModel;
- constructor(x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel) {
+ constructor(schemaName: string, x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) {
super();
this.X = x;
this.Y = y;
this.V = v;
- reaction(() => this.createOperationParamsCache, () => this.Update());
+ this.Normalization = normalized ? normalized : -1;
+ this.SchemaName = schemaName;
}
- @computed.struct
- public get BrushString() {
- return [];
- // let brushes = [];
- // this.TypedViewModel.BrusherModels.map(brushLinkModel => {
- // if (instanceOfIBaseFilterProvider(brushLinkModel.From) && brushLinkModel.From.FilterModels.some && brushLinkModel.From instanceof BaseOperationViewModel) {
- // let brushFilterModels = [];
- // let gnode = MainManager.Instance.MainViewModel.FilterDependencyGraph.has(brushLinkModel.From) ?
- // MainManager.Instance.MainViewModel.FilterDependencyGraph.get(brushLinkModel.From) :
- // new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(brushLinkModel.From);
- // let brush = FilterModel.GetFilterModelsRecursive(gnode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), brushFilterModels, false);
- // brushes.push(brush);
- // }
- // });
- // return brushes;
+ Equals(other: Object): boolean {
+ throw new Error("Method not implemented.");
+ }
+
+ @action
+ public AddFilterModels(filterModels: FilterModel[]): void {
+ filterModels.filter(f => f !== null).forEach(fm => this.FilterModels.push(fm));
+ }
+ @action
+ public RemoveFilterModels(filterModels: FilterModel[]): void {
+ ArrayUtil.RemoveMany(this.FilterModels, filterModels);
}
+ @computed
+ public get FilterString(): string {
+ let filterModels: FilterModel[] = [];
+ return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true)
+ }
- @computed.struct
- public get SelectionString() {
- return "";
- // let filterModels = new Array<FilterModel>();
- // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph;
- // let graphNode: GraphNode<BaseOperationViewModel, FilterLinkViewModel>;
- // if (rdg.has(this.TypedViewModel)) {
- // graphNode = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph.get(this.TypedViewModel);
- // }
- // else {
- // graphNode = new GraphNode<BaseOperationViewModel, FilterLinkViewModel>(this.TypedViewModel);
- // }
- // return FilterModel.GetFilterModelsRecursive(graphNode, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false);
+ @computed
+ public get BrushString(): string[] {
+ trace();
+ let brushes: string[] = [];
+ this.BrushLinks.map(brushLink => {
+ let brushHistogram = brushLink.b.GetT(KeyStore.Data, HistogramField);
+ if (brushHistogram && brushHistogram != FieldWaiting) {
+ let filterModels: FilterModel[] = [];
+ brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.Data, new Set<IBaseFilterProvider>(), filterModels, false));
+ }
+ });
+ return brushes;
}
- GetAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) {
+ private getAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) {
let allAttributes = new Array<AttributeTransformationModel>(histoX, histoY, histoValue);
allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None));
let numericDataTypes = [DataType.Int, DataType.Double, DataType.Float];
- let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(allAttributes);
+ let perBinAggregateParameters: AggregateParameters[] = ModelHelpers.GetAggregateParametersWithMargins(this.Schema!.distinctAttributeParameters, allAttributes);
let globalAggregateParameters: AggregateParameters[] = [];
[histoX, histoY]
.filter(a => a.AggregateFunction === AggregateFunction.None && ArrayUtil.Contains(numericDataTypes, a.AttributeModel.DataType))
@@ -71,19 +92,12 @@ export class HistogramOperation extends BaseOperation {
return [perBinAggregateParameters, globalAggregateParameters];
}
- @computed
- get createOperationParamsCache() {
- return this.CreateOperationParameters();
- }
-
- public QRange: QuantitativeBinRange | undefined;
-
public CreateOperationParameters(): HistogramOperationParameters | undefined {
if (this.X && this.Y && this.V) {
- let [perBinAggregateParameters, globalAggregateParameters] = this.GetAggregateParameters(this.X, this.Y, this.V);
+ let [perBinAggregateParameters, globalAggregateParameters] = this.getAggregateParameters(this.X, this.Y, this.V);
return new HistogramOperationParameters({
enableBrushComputation: true,
- adapterName: Main.Instance.ActiveSchema!.displayName,
+ adapterName: this.SchemaName,
filter: this.FilterString,
brushes: this.BrushString,
binningParameters: [ModelHelpers.GetBinningParameters(this.X, SETTINGS_X_BINS, this.QRange ? this.QRange.minValue : undefined, this.QRange ? this.QRange.maxValue : undefined),
@@ -100,10 +114,9 @@ export class HistogramOperation extends BaseOperation {
}
}
-
@action
public async Update(): Promise<void> {
- // this.TypedViewModel.BrushColors = this.TypedViewModel.BrusherModels.map(e => e.Color);
+ this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
return super.Update();
}
}
diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
index 71bcadf89..7c2b7fc9d 100644
--- a/src/client/northstar/utils/Extensions.ts
+++ b/src/client/northstar/utils/Extensions.ts
@@ -1,5 +1,6 @@
interface String {
ReplaceAll(toReplace: string, replacement: string): string;
+ Truncate(length: number, replacement: string): String;
}
String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
@@ -7,6 +8,14 @@ String.prototype.ReplaceAll = function (toReplace: string, replacement: string):
return target.split(toReplace).join(replacement);
}
+String.prototype.Truncate = function (length: number, replacement: string): String {
+ var target = this;
+ if (target.length >= length) {
+ target = target.slice(0, Math.max(0, length - replacement.length)) + replacement;
+ }
+ return target;
+}
+
interface Math {
log10(val: number): number;
}
diff --git a/src/client/northstar/utils/LABColor.ts b/src/client/northstar/utils/LABColor.ts
new file mode 100644
index 000000000..72e46fb7f
--- /dev/null
+++ b/src/client/northstar/utils/LABColor.ts
@@ -0,0 +1,90 @@
+
+export class LABColor {
+ public L: number;
+ public A: number;
+ public B: number;
+
+ // constructor - takes three floats for lightness and color-opponent dimensions
+ constructor(l: number, a: number, b: number) {
+ this.L = l;
+ this.A = a;
+ this.B = b;
+ }
+
+ // static function for linear interpolation between two LABColors
+ public static Lerp(a: LABColor, b: LABColor, t: number): LABColor {
+ return new LABColor(LABColor.LerpNumber(a.L, b.L, t), LABColor.LerpNumber(a.A, b.A, t), LABColor.LerpNumber(a.B, b.B, t));
+ }
+
+ public static LerpNumber(a: number, b: number, percent: number): number {
+ return a + percent * (b - a);
+ }
+
+ static hexToRGB(hex: number, alpha: number): number[] {
+ var r = (hex & (0xff << 16)) >> 16;
+ var g = (hex & (0xff << 8)) >> 8;
+ var b = (hex & (0xff << 0)) >> 0;
+ return [r, g, b];
+ }
+ static RGBtoHex(red: number, green: number, blue: number): number {
+ return blue | (green << 8) | (red << 16);
+ }
+
+ public static RGBtoHexString(rgb: number): string {
+ let str = "#" + this.hex((rgb & (0xff << 16)) >> 16) + this.hex((rgb & (0xff << 8)) >> 8) + this.hex((rgb & (0xff << 0)) >> 0);
+ return str;
+ }
+
+ static hex(x: number): string {
+ var hexDigits = new Array
+ ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
+ return isNaN(x) ? "00" : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
+ }
+
+ public static FromColor(c: number): LABColor {
+ var rgb = LABColor.hexToRGB(c, 0);
+ var r = LABColor.d3_rgb_xyz(rgb[0] * 255);
+ var g = LABColor.d3_rgb_xyz(rgb[1] * 255);
+ var b = LABColor.d3_rgb_xyz(rgb[2] * 255);
+
+ var x = LABColor.d3_xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / LABColor.d3_lab_X);
+ var y = LABColor.d3_xyz_lab((0.2126729 * r + 0.7151522 * g + 0.0721750 * b) / LABColor.d3_lab_Y);
+ var z = LABColor.d3_xyz_lab((0.0193339 * r + 0.1191920 * g + 0.9503041 * b) / LABColor.d3_lab_Z);
+ var lab = new LABColor(116 * y - 16, 500 * (x - y), 200 * (y - z));
+ return lab;
+ }
+
+ private static d3_lab_X: number = 0.950470;
+ private static d3_lab_Y: number = 1;
+ private static d3_lab_Z: number = 1.088830;
+
+ public static d3_lab_xyz(x: number): number {
+ return x > 0.206893034 ? x * x * x : (x - 4 / 29) / 7.787037;
+ }
+
+ public static d3_xyz_rgb(r: number): number {
+ return Math.round(255 * (r <= 0.00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - 0.055));
+ }
+
+ public static d3_rgb_xyz(r: number): number {
+ return (r /= 255) <= 0.04045 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
+ }
+
+ public static d3_xyz_lab(x: number): number {
+ return x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29;
+ }
+
+ public static ToColor(lab: LABColor): number {
+ var y = (lab.L + 16) / 116;
+ var x = y + lab.A / 500;
+ var z = y - lab.B / 200;
+ x = LABColor.d3_lab_xyz(x) * LABColor.d3_lab_X;
+ y = LABColor.d3_lab_xyz(y) * LABColor.d3_lab_Y;
+ z = LABColor.d3_lab_xyz(z) * LABColor.d3_lab_Z;
+
+ return LABColor.RGBtoHex(
+ LABColor.d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z) / 255,
+ LABColor.d3_xyz_rgb(-0.9692660 * x + 1.8760108 * y + 0.0415560 * z) / 255,
+ LABColor.d3_xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z) / 255);
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/MathUtil.ts b/src/client/northstar/utils/MathUtil.ts
index 3ed8628ee..bb7e73871 100644
--- a/src/client/northstar/utils/MathUtil.ts
+++ b/src/client/northstar/utils/MathUtil.ts
@@ -1,13 +1,17 @@
export class PIXIPoint {
- public x: number;
- public y: number;
+ public get x() { return this.coords[0]; }
+ public get y() { return this.coords[1]; }
+ public set x(value: number) { this.coords[0] = value; }
+ public set y(value: number) { this.coords[1] = value; }
+ public coords: number[] = [0, 0];
constructor(x: number, y: number) {
- this.x = x;
- this.y = y;
+ this.coords[0] = x;
+ this.coords[1] = y;
}
}
+
export class PIXIRectangle {
public x: number;
public y: number;
@@ -17,6 +21,7 @@ export class PIXIRectangle {
public get right() { return this.x + this.width; }
public get top() { return this.y }
public get bottom() { return this.top + this.height }
+ public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
constructor(x: number, y: number, width: number, height: number) {
this.x = x;
this.y = y;
diff --git a/src/client/northstar/utils/SizeConverter.ts b/src/client/northstar/utils/SizeConverter.ts
new file mode 100644
index 000000000..bb91ed4a7
--- /dev/null
+++ b/src/client/northstar/utils/SizeConverter.ts
@@ -0,0 +1,99 @@
+import { PIXIPoint } from "./MathUtil";
+import { VisualBinRange } from "../model/binRanges/VisualBinRange";
+import { Bin, DoubleValueAggregateResult, AggregateKey } from "../model/idea/idea";
+import { ModelHelpers } from "../model/ModelHelpers";
+import { observable, action, computed } from "mobx";
+
+export class SizeConverter {
+ public DataMins: Array<number> = new Array<number>(2);
+ public DataMaxs: Array<number> = new Array<number>(2);
+ public DataRanges: Array<number> = new Array<number>(2);
+ public MaxLabelSizes: Array<PIXIPoint> = new Array<PIXIPoint>(2);
+ public RenderDimension: number = 300;
+
+ @observable _leftOffset: number = 40;
+ @observable _rightOffset: number = 20;
+ @observable _topOffset: number = 20;
+ @observable _bottomOffset: number = 45;
+ @observable _labelAngle: number = 0;
+ @observable _isSmall: boolean = false;
+ @observable public Initialized = 0;
+
+ @action public SetIsSmall(isSmall: boolean) { this._isSmall = isSmall; }
+ @action public SetLabelAngle(angle: number) { this._labelAngle = angle; }
+ @computed public get IsSmall() { return this._isSmall; }
+ @computed public get LabelAngle() { return this._labelAngle; }
+ @computed public get LeftOffset() { return this.IsSmall ? 5 : this._leftOffset; }
+ @computed public get RightOffset() { return this.IsSmall ? 5 : !this._labelAngle ? this._bottomOffset : Math.max(this._rightOffset, Math.cos(this._labelAngle) * (this.MaxLabelSizes[0].x + 18)); }
+ @computed public get TopOffset() { return this.IsSmall ? 5 : this._topOffset; }
+ @computed public get BottomOffset() { return this.IsSmall ? 25 : !this._labelAngle ? this._bottomOffset : Math.max(this._bottomOffset, Math.sin(this._labelAngle) * (this.MaxLabelSizes[0].x + 18)) + 18; }
+
+ public SetVisualBinRanges(visualBinRanges: Array<VisualBinRange>) {
+ this.Initialized++;
+ var xLabels = visualBinRanges[0].GetLabels();
+ var yLabels = visualBinRanges[1].GetLabels();
+ var xLabelStrings = xLabels.map(l => l.label!).sort(function (a, b) { return b.length - a.length });
+ var yLabelStrings = yLabels.map(l => l.label!).sort(function (a, b) { return b.length - a.length });
+
+ var metricsX = { width: 75 }; // RenderUtils.MeasureText(FontStyles.Default.fontFamily.toString(), 12, // FontStyles.AxisLabel.fontSize as number,
+ //xLabelStrings[0]!.slice(0, 20)) // StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS));
+ var metricsY = { width: 22 }; // RenderUtils.MeasureText(FontStyles.Default.fontFamily.toString(), 12, // FontStyles.AxisLabel.fontSize as number,
+ // yLabelStrings[0]!.slice(0, 20)); // StyleConstants.MAX_CHAR_FOR_HISTOGRAM_LABELS));
+ this.MaxLabelSizes[0] = new PIXIPoint(metricsX.width, 12);// FontStyles.AxisLabel.fontSize as number);
+ this.MaxLabelSizes[1] = new PIXIPoint(metricsY.width, 12); // FontStyles.AxisLabel.fontSize as number);
+
+ this._leftOffset = Math.max(10, metricsY.width + 10 + 20);
+
+ this.DataMins[0] = xLabels.map(l => l.minValue!).reduce((m, c) => Math.min(m, c), Number.MAX_VALUE);
+ this.DataMins[1] = yLabels.map(l => l.minValue!).reduce((m, c) => Math.min(m, c), Number.MAX_VALUE);
+ this.DataMaxs[0] = xLabels.map(l => l.maxValue!).reduce((m, c) => Math.max(m, c), Number.MIN_VALUE);
+ this.DataMaxs[1] = yLabels.map(l => l.maxValue!).reduce((m, c) => Math.max(m, c), Number.MIN_VALUE);
+
+ this.DataRanges[0] = this.DataMaxs[0] - this.DataMins[0];
+ this.DataRanges[1] = this.DataMaxs[1] - this.DataMins[1];
+ }
+
+ public DataToScreenNormalizedRange(dataValue: number, normalization: number, axis: number, binBrushMaxAxis: number) {
+ var value = normalization != 1 - axis || binBrushMaxAxis == 0 ? dataValue : (dataValue - 0) / (binBrushMaxAxis - 0) * this.DataRanges[axis];
+ var from = this.DataToScreenCoord(Math.min(0, value), axis);
+ var to = this.DataToScreenCoord(Math.max(0, value), axis);
+ return [from, value, to];
+ }
+
+ public DataToScreenPointRange(axis: number, bin: Bin, aggregateKey: AggregateKey) {
+ var value = ModelHelpers.GetAggregateResult(bin, aggregateKey) as DoubleValueAggregateResult;
+ if (value && value.hasResult)
+ return [this.DataToScreenCoord(value.result!, axis) - 5,
+ this.DataToScreenCoord(value.result!, axis) + 5];
+ return [undefined, undefined];
+ }
+
+ public DataToScreenXAxisRange(visualBinRanges: VisualBinRange[], index: number, bin: Bin) {
+ var value = visualBinRanges[0].GetValueFromIndex(bin.binIndex!.indices![index]);
+ return [this.DataToScreenX(value), this.DataToScreenX(visualBinRanges[index].AddStep(value))]
+ }
+ public DataToScreenYAxisRange(visualBinRanges: VisualBinRange[], index: number, bin: Bin) {
+ var value = visualBinRanges[1].GetValueFromIndex(bin.binIndex!.indices![index]);
+ return [this.DataToScreenY(value), this.DataToScreenY(visualBinRanges[index].AddStep(value))]
+ }
+
+ public DataToScreenX(x: number): number {
+ return ((x - this.DataMins[0]) / this.DataRanges[0]) * this.RenderDimension;
+ }
+ public DataToScreenY(y: number, flip: boolean = true) {
+ var retY = ((y - this.DataMins[1]) / this.DataRanges[1]) * this.RenderDimension;
+ return flip ? (this.RenderDimension) - retY : retY;
+ }
+ public DataToScreenCoord(v: number, axis: number) {
+ if (axis == 0)
+ return this.DataToScreenX(v);
+ return this.DataToScreenY(v);
+ }
+ public DataToScreenRange(minVal: number, maxVal: number, axis: number) {
+ let xFrom = this.DataToScreenX(axis === 0 ? minVal : this.DataMins[0]);
+ let xTo = this.DataToScreenX(axis === 0 ? maxVal : this.DataMaxs[0]);
+ let yFrom = this.DataToScreenY(axis === 1 ? minVal : this.DataMins[1]);
+ let yTo = this.DataToScreenY(axis === 1 ? maxVal : this.DataMaxs[1]);
+ return { xFrom, yFrom, xTo, yTo }
+ }
+} \ No newline at end of file
diff --git a/src/client/northstar/utils/StyleContants.ts b/src/client/northstar/utils/StyleContants.ts
new file mode 100644
index 000000000..ac8617e3b
--- /dev/null
+++ b/src/client/northstar/utils/StyleContants.ts
@@ -0,0 +1,95 @@
+import { PIXIPoint } from "./MathUtil";
+
+export class StyleConstants {
+
+ static DEFAULT_FONT: string = "Roboto Condensed";
+
+ static MENU_SUBMENU_WIDTH: number = 85;
+ static MENU_SUBMENU_HEIGHT: number = 400;
+ static MENU_BOX_SIZE: PIXIPoint = new PIXIPoint(80, 35);
+ static MENU_BOX_PADDING: number = 10;
+
+ static OPERATOR_MENU_LARGE: number = 35;
+ static OPERATOR_MENU_SMALL: number = 25;
+ static BRUSH_PALETTE: number[] = [0x42b43c, 0xfa217f, 0x6a9c75, 0xfb5de7, 0x25b8ea, 0x9b5bc4, 0xda9f63, 0xe23209, 0xfb899b, 0x94a6fd]
+ static GAP: number = 3;
+
+ static BACKGROUND_COLOR: number = 0xF3F3F3;
+ static TOOL_TIP_BACKGROUND_COLOR: number = 0xffffff;
+ static LIGHT_TEXT_COLOR: number = 0xffffff;
+ static LIGHT_TEXT_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.LIGHT_TEXT_COLOR);
+ static DARK_TEXT_COLOR: number = 0x282828;
+ static HIGHLIGHT_TEXT_COLOR: number = 0xffcc00;
+ static FPS_TEXT_COLOR: number = StyleConstants.DARK_TEXT_COLOR;
+ static CORRELATION_LABEL_TEXT_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.DARK_TEXT_COLOR);
+ static LOADING_SCREEN_TEXT_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.DARK_TEXT_COLOR);
+ static ERROR_COLOR: number = 0x540E25;
+ static WARNING_COLOR: number = 0xE58F24;
+ static LOWER_THAN_NAIVE_COLOR: number = 0xee0000;
+ static HIGHLIGHT_COLOR: number = 0x82A8D9;
+ static HIGHLIGHT_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.HIGHLIGHT_COLOR);
+ static OPERATOR_BACKGROUND_COLOR: number = 0x282828;
+ static LOADING_ANIMATION_COLOR: number = StyleConstants.OPERATOR_BACKGROUND_COLOR;
+ static MENU_COLOR: number = 0x282828;
+ static MENU_FONT_COLOR: number = StyleConstants.LIGHT_TEXT_COLOR;
+ static MENU_SELECTED_COLOR: number = StyleConstants.HIGHLIGHT_COLOR;
+ static MENU_SELECTED_FONT_COLOR: number = StyleConstants.LIGHT_TEXT_COLOR;
+ static BRUSH_COLOR: number = 0xff0000;
+ static DROP_ACCEPT_COLOR: number = StyleConstants.HIGHLIGHT_COLOR;
+ static SELECTED_COLOR: number = 0xffffff;
+ static SELECTED_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.SELECTED_COLOR);
+ static PROGRESS_BACKGROUND_COLOR: number = 0x595959;
+ static GRID_LINES_COLOR: number = 0x3D3D3D;
+ static GRID_LINES_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.GRID_LINES_COLOR);
+
+ static MAX_CHAR_FOR_HISTOGRAM_LABELS: number = 20;
+
+ static OVERLAP_COLOR: number = 0x0000ff;//0x540E25;
+ static BRUSH_COLORS: Array<number> = new Array<number>(
+ 0xFFDA7E, 0xFE8F65, 0xDA5655, 0x8F2240
+ );
+
+ static MIN_VALUE_COLOR: number = 0x373d43; //32343d, 373d43, 3b4648
+ static MARGIN_BARS_COLOR: number = 0xffffff;
+ static MARGIN_BARS_COLOR_STR: string = StyleConstants.HexToHexString(StyleConstants.MARGIN_BARS_COLOR);
+
+ static HISTOGRAM_WIDTH: number = 200;
+ static HISTOGRAM_HEIGHT: number = 150;
+ static PREDICTOR_WIDTH: number = 150;
+ static PREDICTOR_HEIGHT: number = 100;
+ static RAWDATA_WIDTH: number = 150;
+ static RAWDATA_HEIGHT: number = 100;
+ static FREQUENT_ITEM_WIDTH: number = 180;
+ static FREQUENT_ITEM_HEIGHT: number = 100;
+ static CORRELATION_WIDTH: number = 555;
+ static CORRELATION_HEIGHT: number = 390;
+ static PROBLEM_FINDER_WIDTH: number = 450;
+ static PROBLEM_FINDER_HEIGHT: number = 150;
+ static PIPELINE_OPERATOR_WIDTH: number = 300;
+ static PIPELINE_OPERATOR_HEIGHT: number = 120;
+ static SLICE_WIDTH: number = 150;
+ static SLICE_HEIGHT: number = 45;
+ static BORDER_MENU_ITEM_WIDTH: number = 50;
+ static BORDER_MENU_ITEM_HEIGHT: number = 30;
+
+
+ static SLICE_BG_COLOR: string = StyleConstants.HexToHexString(StyleConstants.OPERATOR_BACKGROUND_COLOR);
+ static SLICE_EMPTY_COLOR: number = StyleConstants.OPERATOR_BACKGROUND_COLOR;
+ static SLICE_OCCUPIED_COLOR: number = 0xffffff;
+ static SLICE_OCCUPIED_BG_COLOR: string = StyleConstants.HexToHexString(StyleConstants.OPERATOR_BACKGROUND_COLOR);
+ static SLICE_HOVER_BG_COLOR: string = StyleConstants.HexToHexString(StyleConstants.HIGHLIGHT_COLOR);
+ static SLICE_HOVER_COLOR: number = 0xffffff;
+
+ static HexToHexString(hex: number): string {
+ if (hex === undefined) {
+ return "#000000";
+ }
+ var s = hex.toString(16);
+ while (s.length < 6) {
+ s = "0" + s;
+ }
+ return "#" + s;
+ }
+
+
+}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5b99b4ef8..bf59fbb43 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,11 @@
import React = require('react')
import { observer } from 'mobx-react';
-import { observable, action } from 'mobx';
+import { observable, action, computed } from 'mobx';
import { Document } from "../../fields/Document"
import { DocumentView } from '../views/nodes/DocumentView';
+import { KeyStore } from '../../fields/KeyStore';
+import { FieldWaiting } from '../../fields/Field';
+import { ListField } from '../../fields/ListField';
export class DocumentManager {
@@ -24,6 +27,11 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
+ public getAllDocumentViews(collection: Document) {
+ return this.DocumentViews.filter(dv =>
+ dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document == collection);
+ }
+
public getDocumentView(toFind: Document): DocumentView | null {
let toReturn: DocumentView | null;
@@ -33,19 +41,59 @@ export class DocumentManager {
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document;
// if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {
- // if (Object.is(doc, toFind)) {
- // toReturn = view;
- // return;
- // }
- // }
if (Object.is(doc, toFind)) {
toReturn = view;
return;
}
+ let docSrc = doc.GetT(KeyStore.Prototype, Document);
+ if (docSrc && docSrc != FieldWaiting && Object.is(docSrc, toFind)) {
+ toReturn = view;
+ }
+ })
+ return (toReturn);
+ }
+ public getDocumentViews(toFind: Document): DocumentView[] {
+
+ let toReturn: DocumentView[] = [];
+
+ //gets document view that is in a freeform canvas collection
+ DocumentManager.Instance.DocumentViews.map(view => {
+ let doc = view.props.Document;
+ // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {
+
+ if (Object.is(doc, toFind)) {
+ toReturn.push(view);
+ } else {
+ let docSrc = doc.GetT(KeyStore.Prototype, Document);
+ if (docSrc && docSrc != FieldWaiting && Object.is(docSrc, toFind)) {
+ toReturn.push(view);
+ }
+ }
})
return (toReturn);
}
+
+ @computed
+ public get LinkedDocumentViews() {
+ return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
+ let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
+ if (linksList && linksList != FieldWaiting && linksList.Data.length) {
+ pairs.push(...linksList.Data.reduce((pairs, link) => {
+ if (link instanceof Document) {
+ let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
+ if (linkToDoc && linkToDoc != FieldWaiting) {
+ DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ pairs.push({ a: dv, b: docView1, l: link })
+ })
+ }
+ }
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Document }[]));
+ }
+ return pairs;
+ }, [] as { a: DocumentView, b: DocumentView, l: Document }[]);
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 753115f76..e8f8cce7c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,40 +1,48 @@
import { DocumentDecorations } from "../views/DocumentDecorations";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { Document } from "../../fields/Document"
+import { Document } from "../../fields/Document";
import { action } from "mobx";
import { ImageField } from "../../fields/ImageField";
import { KeyStore } from "../../fields/KeyStore";
import { CollectionView } from "../views/collections/CollectionView";
import { DocumentView } from "../views/nodes/DocumentView";
-export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, removeFunc: (containingCollection: CollectionView) => void = () => { }) {
- let onRowMove = action((e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
-
- document.removeEventListener("pointermove", onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- var dragData = new DragManager.DocumentDragData(docFunc());
- dragData.removeDocument = removeFunc;
- DragManager.StartDocumentDrag(_reference.current!, dragData);
- });
- let onRowUp = action((e: PointerEvent): void => {
- document.removeEventListener("pointermove", onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- });
+export function setupDrag(
+ _reference: React.RefObject<HTMLDivElement>,
+ docFunc: () => Document,
+ removeFunc: (containingCollection: CollectionView) => void = () => { }
+) {
+ let onRowMove = action(
+ (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener("pointerup", onRowUp);
+ var dragData = new DragManager.DocumentDragData([docFunc()]);
+ dragData.removeDocument = removeFunc;
+ DragManager.StartDocumentDrag([_reference.current!], dragData);
+ }
+ );
+ let onRowUp = action(
+ (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener("pointerup", onRowUp);
+ }
+ );
let onItemDown = (e: React.PointerEvent) => {
// if (this.props.isSelected() || this.props.isTopMost) {
if (e.button == 0) {
e.stopPropagation();
if (e.shiftKey) {
- CollectionDockingView.Instance.StartOtherDrag(docFunc(), e);
+ CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
} else {
document.addEventListener("pointermove", onRowMove);
- document.addEventListener('pointerup', onRowUp);
+ document.addEventListener("pointerup", onRowUp);
}
}
//}
- }
+ };
return onItemDown;
}
@@ -50,7 +58,9 @@ export namespace DragManager {
let dragDiv: HTMLDivElement;
export enum DragButtons {
- Left = 1, Right = 2, Both = Left | Right
+ Left = 1,
+ Right = 2,
+ Both = Left | Right
}
interface DragOptions {
@@ -63,8 +73,7 @@ export namespace DragManager {
(): void;
}
- export class DragCompleteEvent {
- }
+ export class DragCompleteEvent { }
export interface DragHandlers {
dragComplete: (e: DragCompleteEvent) => void;
@@ -74,19 +83,25 @@ export namespace DragManager {
handlers: DropHandlers;
}
export class DropEvent {
- constructor(readonly x: number, readonly y: number, readonly data: { [id: string]: any }) { }
+ constructor(
+ readonly x: number,
+ readonly y: number,
+ readonly data: { [id: string]: any }
+ ) { }
}
-
-
export interface DropHandlers {
drop: (e: Event, de: DropEvent) => void;
}
-
- export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer {
+ export function MakeDropTarget(
+ element: HTMLElement,
+ options: DropOptions
+ ): DragDropDisposer {
if ("canDrop" in element.dataset) {
- throw new Error("Element is already droppable, can't make it droppable again");
+ throw new Error(
+ "Element is already droppable, can't make it droppable again"
+ );
}
element.dataset["canDrop"] = "true";
const handler = (e: Event) => {
@@ -96,78 +111,120 @@ export namespace DragManager {
element.addEventListener("dashOnDrop", handler);
return () => {
element.removeEventListener("dashOnDrop", handler);
- delete element.dataset["canDrop"]
+ delete element.dataset["canDrop"];
};
}
export class DocumentDragData {
- constructor(dragDoc: Document) {
- this.draggedDocument = dragDoc;
- this.droppedDocument = dragDoc;
+ constructor(dragDoc: Document[]) {
+ this.draggedDocuments = dragDoc;
+ this.droppedDocuments = dragDoc;
}
- draggedDocument: Document;
- droppedDocument: Document;
+ draggedDocuments: Document[];
+ droppedDocuments: Document[];
xOffset?: number;
yOffset?: number;
aliasOnDrop?: boolean;
removeDocument?: (collectionDrop: CollectionView) => void;
[id: string]: any;
}
- export function StartDocumentDrag(ele: HTMLElement, dragData: DocumentDragData, options?: DragOptions) {
- StartDrag(ele, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocument = dragData.aliasOnDrop ? dragData.draggedDocument.CreateAlias() : dragData.draggedDocument);
+
+ export function StartDocumentDrag(
+ eles: HTMLElement[],
+ dragData: DocumentDragData,
+ options?: DragOptions
+ ) {
+ StartDrag(
+ eles,
+ dragData,
+ options,
+ (dropData: { [id: string]: any }) =>
+ (dropData.droppedDocuments = dragData.aliasOnDrop
+ ? dragData.draggedDocuments.map(d => d.CreateAlias())
+ : dragData.draggedDocuments)
+ );
}
export class LinkDragData {
constructor(linkSourceDoc: DocumentView) {
this.linkSourceDocumentView = linkSourceDoc;
}
+ droppedDocuments: Document[] = [];
linkSourceDocumentView: DocumentView;
[id: string]: any;
}
- export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, options?: DragOptions) {
- StartDrag(ele, dragData, options);
+ export function StartLinkDrag(
+ ele: HTMLElement,
+ dragData: LinkDragData,
+ options?: DragOptions
+ ) {
+ StartDrag([ele], dragData, options);
}
- function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+ function StartDrag(
+ eles: HTMLElement[],
+ dragData: { [id: string]: any },
+ options?: DragOptions,
+ finishDrag?: (dropData: { [id: string]: any }) => void
+ ) {
if (!dragDiv) {
dragDiv = document.createElement("div");
- dragDiv.className = "dragManager-dragDiv"
+ dragDiv.className = "dragManager-dragDiv";
DragManager.Root().appendChild(dragDiv);
}
- const w = ele.offsetWidth, h = ele.offsetHeight;
- const rect = ele.getBoundingClientRect();
- const scaleX = rect.width / w, scaleY = rect.height / h;
- let x = rect.left, y = rect.top;
- // const offsetX = e.x - rect.left, offsetY = e.y - rect.top;
-
- let dragElement = ele.cloneNode(true) as HTMLElement;
- dragElement.style.opacity = "0.7";
- dragElement.style.position = "absolute";
- dragElement.style.bottom = "";
- dragElement.style.left = "";
- dragElement.style.transformOrigin = "0 0";
- dragElement.style.zIndex = "1000";
- dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
- dragElement.style.width = `${rect.width / scaleX}px`;
- dragElement.style.height = `${rect.height / scaleY}px`;
-
- // bcz: PDFs don't show up if you clone them because they contain a canvas.
- // however, PDF's have a thumbnail field that contains an image of their canvas.
- // So we replace the pdf's canvas with the image thumbnail
- const doc: Document = dragData["draggedDocument"];
- if (doc) {
- var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
- let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField);
- if (pdfBox && pdfBox.childElementCount && thumbnail) {
- let img = new Image();
- img!.src = thumbnail.toString();
- img!.style.position = "absolute";
- img!.style.width = `${rect.width / scaleX}px`;
- img!.style.height = `${rect.height / scaleY}px`;
- pdfBox.replaceChild(img!, pdfBox.children[0])
+
+ let scaleXs: number[] = [];
+ let scaleYs: number[] = [];
+ let xs: number[] = [];
+ let ys: number[] = [];
+
+ const docs: Document[] =
+ dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
+ let dragElements = eles.map(ele => {
+ const w = ele.offsetWidth,
+ h = ele.offsetHeight;
+ const rect = ele.getBoundingClientRect();
+ const scaleX = rect.width / w,
+ scaleY = rect.height / h;
+ let x = rect.left,
+ y = rect.top;
+ xs.push(x);
+ ys.push(y);
+ scaleXs.push(scaleX);
+ scaleYs.push(scaleY);
+ let dragElement = ele.cloneNode(true) as HTMLElement;
+ dragElement.style.opacity = "0.7";
+ dragElement.style.position = "absolute";
+ dragElement.style.margin = "0";
+ dragElement.style.top = "0";
+ dragElement.style.bottom = "";
+ dragElement.style.left = "0";
+ dragElement.style.transformOrigin = "0 0";
+ dragElement.style.zIndex = "1000";
+ dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+ dragElement.style.width = `${rect.width / scaleX}px`;
+ dragElement.style.height = `${rect.height / scaleY}px`;
+
+ // bcz: PDFs don't show up if you clone them because they contain a canvas.
+ // however, PDF's have a thumbnail field that contains an image of their canvas.
+ // So we replace the pdf's canvas with the image thumbnail
+ if (docs.length) {
+ var pdfBox = dragElement.getElementsByClassName(
+ "pdfBox-cont"
+ )[0] as HTMLElement;
+ let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField);
+ if (pdfBox && pdfBox.childElementCount && thumbnail) {
+ let img = new Image();
+ img!.src = thumbnail.toString();
+ img!.style.position = "absolute";
+ img!.style.width = `${rect.width / scaleX}px`;
+ img!.style.height = `${rect.height / scaleY}px`;
+ pdfBox.replaceChild(img!, pdfBox.children[0]);
+ }
}
- }
- dragDiv.appendChild(dragElement);
+ dragDiv.appendChild(dragElement);
+ return dragElement;
+ });
let hideSource = false;
if (options) {
@@ -177,63 +234,81 @@ export namespace DragManager {
hideSource = options.hideSource();
}
}
- const wasHidden = ele.hidden;
- if (hideSource) {
- ele.hidden = true;
- }
+ eles.map(ele => (ele.hidden = hideSource));
+
const moveHandler = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- x += e.movementX;
- y += e.movementY;
+ if (dragData instanceof DocumentDragData)
+ dragData.aliasOnDrop = e.ctrlKey || e.altKey;
if (e.shiftKey) {
abortDrag();
- CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
+ CollectionDockingView.Instance.StartOtherDrag(docs, {
+ pageX: e.pageX,
+ pageY: e.pageY,
+ preventDefault: () => { },
+ button: 0
+ });
}
- dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+ dragElements.map(
+ (dragElement, i) =>
+ (dragElement.style.transform = `translate(${(xs[i] +=
+ e.movementX)}px, ${(ys[i] += e.movementY)}px) scale(${
+ scaleXs[i]
+ }, ${scaleYs[i]})`)
+ );
};
const abortDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
- dragDiv.removeChild(dragElement);
- if (hideSource && !wasHidden) {
- ele.hidden = false;
- }
- }
+ dragElements.map(dragElement => dragDiv.removeChild(dragElement));
+ eles.map(ele => (ele.hidden = false));
+ };
const upHandler = (e: PointerEvent) => {
abortDrag();
- FinishDrag(ele, e, dragData, options, finishDrag);
+ FinishDrag(eles, e, dragData, options, finishDrag);
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
}
- function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
- let parent = dragEle.parentElement;
- if (parent)
- parent.removeChild(dragEle);
+ function FinishDrag(
+ dragEles: HTMLElement[],
+ e: PointerEvent,
+ dragData: { [index: string]: any },
+ options?: DragOptions,
+ finishDrag?: (dragData: { [index: string]: any }) => void
+ ) {
+ let removed = dragEles.map(dragEle => {
+ let parent = dragEle.parentElement;
+ if (parent) parent.removeChild(dragEle);
+ return [dragEle, parent];
+ });
const target = document.elementFromPoint(e.x, e.y);
- if (parent)
- parent.appendChild(dragEle);
- if (!target) {
- return;
- }
- if (finishDrag)
- finishDrag(dragData);
-
- target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
- bubbles: true,
- detail: {
- x: e.x,
- y: e.y,
- data: dragData
- }
- }));
+ removed.map(r => {
+ let dragEle: HTMLElement = r[0]!;
+ let parent: HTMLElement | null = r[1];
+ if (parent) parent.appendChild(dragEle);
+ });
+ if (target) {
+ if (finishDrag) finishDrag(dragData);
- if (options) {
- options.handlers.dragComplete({});
+ target.dispatchEvent(
+ new CustomEvent<DropEvent>("dashOnDrop", {
+ bubbles: true,
+ detail: {
+ x: e.x,
+ y: e.y,
+ data: dragData
+ }
+ })
+ );
+
+ if (options) {
+ options.handlers.dragComplete({});
+ }
}
DocumentDecorations.Instance.Hidden = false;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 46bd1a206..4e97b9401 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -15,6 +15,8 @@ import { ListField } from "../../fields/ListField";
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d'
+import { Documents } from "../documents/Documents";
+import { Key } from "../../fields/Key";
export interface ExecutableScript {
@@ -28,9 +30,9 @@ function Compile(script: string | undefined, diagnostics: Opt<any[]>, scope: { [
let func: () => Opt<Field>;
if (compiled && script) {
- let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField];
- let paramNames = ["KeyStore", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [KeyStore, ...fieldTypes]
+ let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key];
+ let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)];
+ let params: any[] = [KeyStore, Documents, ...fieldTypes]
for (let prop in scope) {
if (prop === "this") {
continue;
@@ -110,7 +112,7 @@ export function CompileScript(script: string, scope?: { [name: string]: any }, a
let host = new ScriptingCompilerHost;
let funcScript = `(function() {
${addReturn ? `return ${script};` : script}
- })()`
+ }).apply(this)`
host.writeFile("file.ts", funcScript);
host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
let program = ts.createProgram(["file.ts"], {}, host);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1a711ae64..958c14491 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,5 +1,6 @@
import { observable, action } from "mobx";
import { DocumentView } from "../views/nodes/DocumentView";
+import { Document } from "../../fields/Document";
export namespace SelectionManager {
class Manager {
@@ -14,26 +15,34 @@ export namespace SelectionManager {
}
if (manager.SelectedDocuments.indexOf(doc) === -1) {
- manager.SelectedDocuments.push(doc)
+ manager.SelectedDocuments.push(doc);
}
}
}
- const manager = new Manager;
+ const manager = new Manager();
export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
- manager.SelectDoc(doc, ctrlPressed)
+ manager.SelectDoc(doc, ctrlPressed);
}
export function IsSelected(doc: DocumentView): boolean {
return manager.SelectedDocuments.indexOf(doc) !== -1;
}
- export function DeselectAll(): void {
- manager.SelectedDocuments = []
+ export function DeselectAll(except?: Document): void {
+ let found: DocumentView | undefined = undefined;
+ if (except) {
+ for (let i = 0; i < manager.SelectedDocuments.length; i++) {
+ let view = manager.SelectedDocuments[i];
+ if (view.props.Document == except) found = view;
+ }
+ }
+ manager.SelectedDocuments.length = 0;
+ if (found) manager.SelectedDocuments.push(found);
}
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 679f73f42..4f69053b1 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -174,6 +174,7 @@ declare class ListField<T> extends BasicField<T[]>{
Copy(): Field;
}
declare class Key extends Field {
+ constructor(name:string);
Name: string;
TrySetValue(value: any): boolean;
GetValue(): any;
@@ -213,3 +214,12 @@ declare class Document extends Field {
GetAllPrototypes(): Document[];
MakeDelegate(): Document;
}
+
+declare const KeyStore: {
+ [name: string]: Key;
+}
+
+// @ts-ignore
+declare const console: any;
+
+declare const Documents: any;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 9109b56bb..cfa8ea7b7 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -49,7 +49,7 @@ export class ContextMenu extends React.Component {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
let maxX = window.innerWidth - 150;
- let maxY = window.innerHeight - (this._items.length * 34 + 30);
+ let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30);
this._pageX = x > maxX ? maxX : x;
this._pageY = y > maxY ? maxY : y;
@@ -83,11 +83,8 @@ 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>
- {this._items.filter(prop => {
- return prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1;
- }).map(prop => {
- return <ContextMenuItem {...prop} key={prop.description} />
- })}
+ {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
+ map(prop => <ContextMenuItem {...prop} key={prop.description} />)}
</div>
)
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 11595aa01..d7137d7a2 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,35 +1,92 @@
@import "global_variables";
+
#documentDecorations-container {
position: absolute;
+ top: 0;
+ left:0;
display: grid;
z-index: 1000;
- grid-template-rows: 8px 1fr 8px 30px;
- grid-template-columns: 8px 1fr 8px;
+ grid-template-rows: 20px 8px 1fr 8px;
+ grid-template-columns: 8px 8px 1fr 8px 8px;
pointer-events: none;
+
#documentDecorations-centerCont {
+ grid-column:3;
background: none;
}
+
.documentDecorations-resizer {
pointer-events: auto;
background: $alt-accent;
opacity: 0.8;
}
+
+ #documentDecorations-topLeftResizer,
+ #documentDecorations-leftResizer,
+ #documentDecorations-bottomLeftResizer {
+ grid-column: 1
+ }
+
+ #documentDecorations-topResizer,
+ #documentDecorations-bottomResizer {
+ grid-column-start: 2;
+ grid-column-end: 5;
+ }
+
+ #documentDecorations-bottomRightResizer,
+ #documentDecorations-topRightResizer,
+ #documentDecorations-rightResizer {
+ grid-column-start:5;
+ grid-column-end:7;
+ }
+
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
}
+
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
}
+
#documentDecorations-topResizer,
#documentDecorations-bottomResizer {
cursor: ns-resize;
}
+
#documentDecorations-leftResizer,
#documentDecorations-rightResizer {
cursor: ew-resize;
}
+ .title{
+ background: lightblue;
+ grid-column-start:3;
+ grid-column-end: 4;
+ pointer-events: auto;
+ }
+}
+
+.documentDecorations-closeButton {
+ background:$alt-accent;
+ opacity: 0.8;
+ grid-column-start: 4;
+ grid-column-end: 6;
+ pointer-events: all;
+ text-align: center;
+}
+.documentDecorations-minimizeButton {
+ background:$alt-accent;
+ opacity: 0.8;
+ grid-column-start: 1;
+ grid-column-end: 3;
+ pointer-events: all;
+ text-align: center;
+}
+.documentDecorations-background {
+ background: lightblue;
+ position: absolute;
+ opacity: 0.1;
}
// position: absolute;
@@ -64,7 +121,8 @@
// }
// }
.linkFlyout {
- grid-column: 1/4
+ grid-column: 1/4;
+ margin-left: 25px;
}
.linkButton-empty:hover {
@@ -79,6 +137,31 @@
cursor: pointer;
}
+.linkButton-linker {
+ position:absolute;
+ bottom:0px;
+ left: 0px;
+ height: 20px;
+ width: 20px;
+ margin-top: 10px;
+ margin-right: 5px;
+ border-radius: 50%;
+ opacity: 0.9;
+ pointer-events: auto;
+ color: $dark-color;
+ border: $dark-color 1px solid;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+.linkButton-tray {
+ grid-column: 1/4;
+}
.linkButton-empty {
height: 20px;
width: 20px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47098c3b5..8bf1a42d1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,36 +1,84 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../util/SelectionManager";
+import { action, computed, observable, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
-import './DocumentDecorations.scss'
-import { KeyStore } from '../../fields/KeyStore'
+import { Key } from "../../fields/Key";
+//import ContentEditable from 'react-contenteditable'
+import { KeyStore } from "../../fields/KeyStore";
+import { ListField } from "../../fields/ListField";
import { NumberField } from "../../fields/NumberField";
-import { props } from "bluebird";
+import { Document } from "../../fields/Document";
+import { TextField } from "../../fields/TextField";
import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { CollectionView } from "./collections/CollectionView";
+import './DocumentDecorations.scss';
+import { DocumentView } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
-import { ListField } from "../../fields/ListField";
+import React = require("react");
+import { FieldWaiting } from "../../fields/Field";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@observer
-export class DocumentDecorations extends React.Component {
+export class DocumentDecorations extends React.Component<{}, { value: string }> {
static Instance: DocumentDecorations
private _resizer = ""
private _isPointerDown = false;
-
+ private keyinput: React.RefObject<HTMLInputElement>;
+ private _documents: DocumentView[] = SelectionManager.SelectedDocuments();
private _resizeBorderWidth = 16;
+ private _linkBoxHeight = 30;
+ private _titleHeight = 20;
private _linkButton = React.createRef<HTMLDivElement>();
+ private _linkerButton = React.createRef<HTMLDivElement>();
+ //@observable private _title: string = this._documents[0].props.Document.Title;
+ @observable private _title: string = this._documents.length > 0 ? this._documents[0].props.Document.Title : "";
+ @observable private _fieldKey: Key = KeyStore.Title;
@observable private _hidden = false;
+ @observable private _opacity = 1;
+ @observable private _dragging = false;
+
constructor(props: Readonly<{}>) {
super(props)
-
DocumentDecorations.Instance = this
+ this.handleChange = this.handleChange.bind(this);
+ this.keyinput = React.createRef();
+ }
+
+ @action
+ handleChange = (event: any) => {
+ this._title = event.target.value;
+ };
+
+ @action
+ enterPressed = (e: any) => {
+ var key = e.keyCode || e.which;
+ // enter pressed
+ if (key == 13) {
+ var text = e.target.value;
+ if (text[0] == '#') {
+ let command = text.slice(1, text.length);
+ this._fieldKey = new Key(command)
+ // if (command == "Title" || command == "title") {
+ // this._fieldKey = KeyStore.Title;
+ // }
+ // else if (command == "Width" || command == "width") {
+ // this._fieldKey = KeyStore.Width;
+ // }
+ this._title = "changed"
+ // TODO: Change field with switch statement
+ }
+ else {
+ this._title = "changed";
+ }
+ e.target.blur();
+ }
}
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
+ this._documents = SelectionManager.SelectedDocuments();
return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
if (documentView.props.isTopMost) {
return bounds;
@@ -50,6 +98,97 @@ export class DocumentDecorations extends React.Component {
public get Hidden() { return this._hidden; }
public set Hidden(value: boolean) { this._hidden = value; }
+ _lastDrag: number[] = [0, 0];
+ onBackgroundDown = (e: React.PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.addEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ document.addEventListener("pointerup", this.onBackgroundUp);
+ this._lastDrag = [e.clientX, e.clientY]
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ onBackgroundMove = (e: PointerEvent): void => {
+ let dragDocView = SelectionManager.SelectedDocuments()[0];
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+ dragData.aliasOnDrop = false;
+ dragData.xOffset = e.x - left;
+ dragData.yOffset = e.y - top;
+ dragData.removeDocument = (dropCollectionView: CollectionView) =>
+ dragData.draggedDocuments.map(d => {
+ if (dragDocView.props.RemoveDocument && dragDocView.props.ContainingCollectionView !== dropCollectionView) {
+ dragDocView.props.RemoveDocument(d);
+ }
+ });
+ this._dragging = true;
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => (docView as any)._mainCont!.current!), dragData, {
+ handlers: {
+ dragComplete: action(() => this._dragging = false),
+ },
+ hideSource: true
+ })
+ e.stopPropagation();
+ }
+
+ onBackgroundUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onCloseDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ document.removeEventListener("pointermove", this.onCloseMove);
+ document.addEventListener("pointermove", this.onCloseMove);
+ document.removeEventListener("pointerup", this.onCloseUp);
+ document.addEventListener("pointerup", this.onCloseUp);
+ }
+ }
+ onCloseMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ }
+ }
+ @action
+ onCloseUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ SelectionManager.SelectedDocuments().map(dv => dv.props.RemoveDocument && dv.props.RemoveDocument(dv.props.Document));
+ SelectionManager.DeselectAll();
+ document.removeEventListener("pointermove", this.onCloseMove);
+ document.removeEventListener("pointerup", this.onCloseUp);
+ }
+ }
+ onMinimizeDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ document.removeEventListener("pointermove", this.onMinimizeMove);
+ document.addEventListener("pointermove", this.onMinimizeMove);
+ document.removeEventListener("pointerup", this.onMinimizeUp);
+ document.addEventListener("pointerup", this.onMinimizeUp);
+ }
+ }
+ onMinimizeMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ }
+ }
+ onMinimizeUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ SelectionManager.SelectedDocuments().map(dv => dv.minimize());
+ document.removeEventListener("pointermove", this.onMinimizeMove);
+ document.removeEventListener("pointerup", this.onMinimizeUp);
+ }
+ }
+
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
if (e.button === 0) {
@@ -62,18 +201,40 @@ export class DocumentDecorations extends React.Component {
}
}
- onLinkButtonDown = (e: React.PointerEvent): void => {
- // if ()
- // let linkMenu = new LinkMenu(SelectionManager.SelectedDocuments()[0]);
- // linkMenu.Hidden = false;
- console.log("down");
+ onLinkerButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+ document.addEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp)
+ document.addEventListener("pointerup", this.onLinkerButtonUp);
+ }
+ onLinkerButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+ document.removeEventListener("pointerup", this.onLinkerButtonUp)
+ e.stopPropagation();
+ }
+ onLinkerButtonMoved = (e: PointerEvent): void => {
+ if (this._linkerButton.current != null) {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+ document.removeEventListener("pointerup", this.onLinkerButtonUp)
+ let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]);
+ DragManager.StartLinkDrag(this._linkerButton.current, dragData, {
+ handlers: {
+ dragComplete: action(() => { }),
+ },
+ hideSource: false
+ })
+ }
+ e.stopPropagation();
+ }
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
document.removeEventListener("pointermove", this.onLinkButtonMoved)
document.addEventListener("pointermove", this.onLinkButtonMoved);
document.removeEventListener("pointerup", this.onLinkButtonUp)
document.addEventListener("pointerup", this.onLinkButtonUp);
-
}
onLinkButtonUp = (e: PointerEvent): void => {
@@ -82,18 +243,35 @@ export class DocumentDecorations extends React.Component {
e.stopPropagation();
}
-
- onLinkButtonMoved = (e: PointerEvent): void => {
+ onLinkButtonMoved = async (e: PointerEvent) => {
if (this._linkButton.current != null) {
document.removeEventListener("pointermove", this.onLinkButtonMoved)
- document.removeEventListener("pointerup", this.onLinkButtonUp)
- let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]);
- DragManager.StartLinkDrag(this._linkButton.current, dragData, {
- handlers: {
- dragComplete: action(() => { }),
- },
- hideSource: false
- })
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+ let sourceDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+ let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document)
+ let draggedDocs = (srcTarg && srcTarg != FieldWaiting) ?
+ srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc =>
+ (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : [];
+ let draggedFromDocs = (srcTarg && srcTarg != FieldWaiting) ?
+ srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc =>
+ (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : [];
+ draggedDocs.push(...draggedFromDocs);
+ if (draggedDocs.length) {
+ let moddrag = [] as Document[];
+ for (let i = 0; i < draggedDocs.length; i++) {
+ let doc = await draggedDocs[i].GetTAsync(KeyStore.AnnotationOn, Document);
+ if (doc)
+ moddrag.push(doc);
+ }
+ let dragData = new DragManager.DocumentDragData(moddrag);
+ DragManager.StartDocumentDrag([this._linkButton.current], dragData, {
+ handlers: {
+ dragComplete: action(() => { }),
+ },
+ hideSource: false
+ })
+ }
}
e.stopPropagation();
}
@@ -185,15 +363,29 @@ export class DocumentDecorations extends React.Component {
}
}
+ getValue = (): string => {
+ if (this._title === "changed" && this._documents.length > 0) {
+ let field = this._documents[0].props.Document.Get(this._fieldKey);
+ if (field instanceof TextField) {
+ return (field as TextField).GetValue();
+ }
+ else if (field instanceof NumberField) {
+ return (field as NumberField).GetValue().toString();
+ }
+ }
+ return this._title;
+ }
+
changeFlyoutContent = (): void => {
}
// buttonOnPointerUp = (e: React.PointerEvent): void => {
// e.stopPropagation();
// }
-
render() {
var bounds = this.Bounds;
+ // console.log(this._documents.length)
+ // let test = this._documents[0].props.Document.Title;
if (this.Hidden) {
return (null);
}
@@ -211,19 +403,31 @@ export class DocumentDecorations extends React.Component {
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={
- <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}>
- </LinkMenu>
+ <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent} />
}>
<div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
</Flyout>);
}
- return (
- <div id="documentDecorations-container" style={{
+ return (<div className="documentDecorations">
+ <div className="documentDecorations-background" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2,
+ pointerEvents: this._dragging ? "none" : "all",
+ zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+ }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation() }} >
+ </div>
+ <div id="documentDecorations-container" style={{
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
+ opacity: this._opacity
}}>
+ <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>...</div>
+ <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} />
+ <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -234,9 +438,10 @@ export class DocumentDecorations extends React.Component {
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
-
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
+ <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div>
</div >
+ </div>
)
}
} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 579d6e6ad..29bf6add7 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -16,6 +16,8 @@ export interface EditableProps {
* */
SetValue(value: string): boolean;
+ OnFillDown?(value: string): void;
+
/**
* The contents to render when not editing
*/
@@ -36,8 +38,13 @@ export class EditableView extends React.Component<EditableProps> {
@action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key == "Enter" && !e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value)) {
+ if (e.key == "Enter") {
+ if (!e.ctrlKey) {
+ if (this.props.SetValue(e.currentTarget.value)) {
+ this.editing = false;
+ }
+ } else if (this.props.OnFillDown) {
+ this.props.OnFillDown(e.currentTarget.value);
this.editing = false;
}
} else if (e.key == "Escape") {
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index e79b146b9..c5a2a50cb 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,149 +1,32 @@
@import "global_variables";
-.inking-canvas {
+
+.inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect {
position: absolute;
- top: -50000px;
- left: -50000px; // z-index: 99; //overlays ink on top of everything
- svg {
- position:absolute;
- width: 100000px;
- height: 100000px;
- .highlight {
- mix-blend-mode: multiply;
- }
- }
+ top: 0;
+ left:0;
+ width: 8192px;
+ height: 8192px;
+ cursor:"crosshair";
+ pointer-events: auto;
+
+}
+.inkingCanvas-canSelect,
+.inkingCanvas-noSelect {
+ top:-50000px;
+ left:-50000px;
+ width: 100000px;
+ height: 100000px;
+}
+.inkingCanvas-noSelect {
+ pointer-events: none;
+ cursor: "arrow";
+}
+.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;
}
-.inking-control {
- position: absolute;
- left: 70px;
- bottom: 70px;
- margin: 0;
- padding: 0;
- display: flex;
- label,
- input,
- option {
- font-size: 12px;
- }
- input[type="range"] {
- -webkit-appearance: none;
- background-color: transparent;
- vertical-align: middle;
- margin-top: 8px;
- &:focus {
- outline: none;
- }
- &::-webkit-slider-runnable-track {
- width: 100%;
- height: 3px;
- border-radius: 1.5px;
- cursor: pointer;
- background: $intermediate-color;
- }
- &::-webkit-slider-thumb {
- height: 12px;
- width: 12px;
- border: 1px solid $intermediate-color;
- border-radius: 6px;
- background: $light-color;
- cursor: pointer;
- -webkit-appearance: none;
- margin-top: -4px;
- }
- &::-moz-range-track {
- width: 100%;
- height: 3px;
- border-radius: 1.5px;
- cursor: pointer;
- background: $light-color;
- }
- &::-moz-range-thumb {
- height: 12px;
- width: 12px;
- border: 1px solid $intermediate-color;
- border-radius: 6px;
- background: $light-color;
- cursor: pointer;
- -webkit-appearance: none;
- margin-top: -4px;
- }
- }
- input[type="text"] {
- border: none;
- padding: 0 0px;
- background: transparent;
- color: $dark-color;
- font-size: 12px;
- margin-top: 4px;
- }
- .ink-panel {
- margin: 6px 12px 6px 0;
- height: 30px;
- vertical-align: middle;
- line-height: 36px;
- padding: 0 10px;
- color: $intermediate-color;
- &:first {
- margin-top: 0;
- }
- }
- .ink-tools {
- display: flex;
- background-color: transparent;
- border-radius: 0;
- padding: 0;
- button {
- height: 36px;
- padding: 0px;
- padding-bottom: 3px;
- margin-left: 10px;
- background-color: transparent;
- color: $intermediate-color;
- }
- button:hover {
- transform: scale(1.15);
- }
- }
- .ink-size {
- display: flex;
- justify-content: space-between;
- input[type="text"] {
- width: 42px;
- }
- >* {
- margin-right: 6px;
- &:last-child {
- margin-right: 0;
- }
- }
- }
- .ink-color {
- display: flex;
- position: relative;
- padding-right: 0;
- label {
- margin-right: 6px;
- }
- .ink-color-display {
- border-radius: 11px;
- width: 22px;
- height: 22px;
- margin-top: 6px;
- cursor: pointer;
- text-align: center; // span {
- // color: $light-color;
- // font-size: 8px;
- // user-select: none;
- // }
- }
- .ink-color-picker {
- background-color: $light-color;
- border-radius: 5px;
- padding: 12px;
- position: absolute;
- bottom: 36px;
- left: -3px;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 36a8834a0..45ca52d00 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,121 +1,112 @@
+import { action, computed, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { observable } from "mobx";
-import { action, computed } from "mobx";
-import { InkingControl } from "./InkingControl";
-import React = require("react");
-import { Transform } from "../util/Transform";
import { Document } from "../../fields/Document";
-import { KeyStore } from "../../fields/KeyStore";
+import { FieldWaiting } from "../../fields/Field";
import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { InkingStroke } from "./InkingStroke";
-import "./InkingCanvas.scss"
+import { KeyStore } from "../../fields/KeyStore";
import { Utils } from "../../Utils";
-import { FieldWaiting } from "../../fields/Field";
+import { Transform } from "../util/Transform";
+import "./InkingCanvas.scss";
+import { InkingControl } from "./InkingControl";
+import { InkingStroke } from "./InkingStroke";
+import React = require("react");
interface InkCanvasProps {
getScreenTransform: () => Transform;
Document: Document;
+ children: () => JSX.Element[];
}
@observer
export class InkingCanvas extends React.Component<InkCanvasProps> {
- static InkOffset: number = 50000;
+ maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
+ @observable inkMidX: number = 0;
+ @observable inkMidY: number = 0;
+ private _currentStrokeId: string = "";
public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean {
- let inside = false;
- stroke.pathData.map(val => {
- if (selRect.left < val.x - InkingCanvas.InkOffset && selRect.left + selRect.width > val.x - InkingCanvas.InkOffset &&
- selRect.top < val.y - InkingCanvas.InkOffset && selRect.top + selRect.height > val.y - InkingCanvas.InkOffset)
- inside = true;
- });
- return inside
+ return stroke.pathData.reduce((inside: boolean, val) => inside ||
+ (selRect.left < val.x && selRect.left + selRect.width > val.x &&
+ selRect.top < val.y && selRect.top + selRect.height > val.y)
+ , false);
}
- private _isDrawing: boolean = false;
- private _idGenerator: string = "";
- constructor(props: Readonly<InkCanvasProps>) {
- super(props);
+ componentDidMount() {
+ this.props.Document.GetTAsync(KeyStore.Ink, InkField, ink => runInAction(() => {
+ if (ink) {
+ let bounds = Array.from(ink.Data).reduce(([mix, max, miy, may], [id, strokeData]) =>
+ strokeData.pathData.reduce(([mix, max, miy, may], p) =>
+ [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)],
+ [mix, max, miy, may]),
+ [Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]);
+ this.inkMidX = (bounds[0] + bounds[1]) / 2;
+ this.inkMidY = (bounds[2] + bounds[3]) / 2;
+ }
+ }));
}
@computed
get inkData(): StrokeMap {
let map = this.props.Document.GetT(KeyStore.Ink, InkField);
- if (!map || map === FieldWaiting) {
- return new Map;
- }
- return new Map(map.Data);
+ return !map || map === FieldWaiting ? new Map : new Map(map.Data);
}
set inkData(value: StrokeMap) {
this.props.Document.SetDataOnPrototype(KeyStore.Ink, value, InkField);
}
- componentDidMount() {
- document.addEventListener("mouseup", this.handleMouseUp);
- }
-
- componentWillUnmount() {
- document.removeEventListener("mouseup", this.handleMouseUp);
- }
-
@action
- handleMouseDown = (e: React.PointerEvent): void => {
- if (e.button != 0 ||
- InkingControl.Instance.selectedTool === InkTool.None) {
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button != 0 || e.altKey || e.ctrlKey || InkingControl.Instance.selectedTool === InkTool.None) {
return;
}
- e.stopPropagation()
- if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
- return
- }
- const point = this.relativeCoordinatesForEvent(e);
-
- // start the new line, saves a uuid to represent the field of the stroke
- this._idGenerator = Utils.GenerateGuid();
- let data = this.inkData;
- data.set(this._idGenerator,
- {
- pathData: [point],
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (InkingControl.Instance.selectedTool != InkTool.Eraser) {
+ // start the new line, saves a uuid to represent the field of the stroke
+ this._currentStrokeId = Utils.GenerateGuid();
+ this.inkData.set(this._currentStrokeId, {
+ pathData: [this.relativeCoordinatesForEvent(e.clientX, e.clientY)],
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
});
- this.inkData = data;
- this._isDrawing = true;
+ }
}
@action
- handleMouseMove = (e: React.PointerEvent): void => {
- if (!this._isDrawing ||
- InkingControl.Instance.selectedTool === InkTool.None) {
- return;
- }
- e.stopPropagation()
- if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
- return
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ let coord = this.relativeCoordinatesForEvent(e.clientX, e.clientY);
+ if (Math.abs(coord.x - this.inkMidX) > 500 || Math.abs(coord.y - this.inkMidY) > 500) {
+ this.inkMidX = coord.x;
+ this.inkMidY = coord.y;
}
- const point = this.relativeCoordinatesForEvent(e);
-
- // add points to new line as it is being drawn
- let data = this.inkData;
- let strokeData = data.get(this._idGenerator);
- if (strokeData) {
- strokeData.pathData.push(point);
- data.set(this._idGenerator, strokeData);
- }
-
- this.inkData = data;
+ e.stopPropagation();
+ e.preventDefault();
}
@action
- handleMouseUp = (e: MouseEvent): void => {
- this._isDrawing = false;
+ onPointerMove = (e: PointerEvent): void => {
+ e.stopPropagation()
+ e.preventDefault();
+ if (InkingControl.Instance.selectedTool != InkTool.Eraser) {
+ let data = this.inkData; // add points to new line as it is being drawn
+ let strokeData = data.get(this._currentStrokeId);
+ if (strokeData) {
+ strokeData.pathData.push(this.relativeCoordinatesForEvent(e.clientX, e.clientY));
+ data.set(this._currentStrokeId, strokeData);
+ }
+ this.inkData = data;
+ }
}
- relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => {
- let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY);
- x += InkingCanvas.InkOffset;
- y += InkingCanvas.InkOffset;
+ relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => {
+ let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey);
return { x, y };
}
@@ -126,49 +117,35 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
this.inkData = data;
}
- render() {
- // styling for cursor
- let canvasStyle = {};
- if (InkingControl.Instance.selectedTool === InkTool.None) {
- canvasStyle = { pointerEvents: "none" };
- } else {
- canvasStyle = { pointerEvents: "auto", cursor: "crosshair" };
- }
-
- // get data from server
- // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField);
- // if (!inkField || inkField == FieldWaiting) {
- // return (<div className="inking-canvas" style={canvasStyle}
- // onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} >
- // <svg>
- // </svg>
- // </div >)
- // }
-
- let lines = this.inkData;
-
- // parse data from server
- let paths: Array<JSX.Element> = []
+ @computed
+ get drawnPaths() {
let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1)
- Array.from(lines).map(item => {
- let id = item[0];
- let strokeData = item[1];
+ let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
if (strokeData.page == -1 || strokeData.page == curPage)
- paths.push(<InkingStroke key={id} id={id}
- line={strokeData.pathData}
- color={strokeData.color}
- width={strokeData.width}
- tool={strokeData.tool}
- deleteCallback={this.removeLine} />)
- })
+ paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData}
+ offsetX={this.maxCanvasDim - this.inkMidX}
+ offsetY={this.maxCanvasDim - this.inkMidY}
+ color={strokeData.color} width={strokeData.width}
+ tool={strokeData.tool} deleteCallback={this.removeLine} />)
+ return paths;
+ }, [] as JSX.Element[]);
+ return [<svg className={`inkingCanvas-paths-markers`} key="Markers"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
+ {paths.filter(path => path.props.tool == InkTool.Highlighter)}
+ </svg>,
+ <svg className={`inkingCanvas-paths-ink`} key="Pens"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
+ {paths.filter(path => path.props.tool != InkTool.Highlighter)}
+ </svg>];
+ }
+ render() {
+ let svgCanvasStyle = InkingControl.Instance.selectedTool != InkTool.None ? "canSelect" : "noSelect";
return (
-
- <div className="inking-canvas" style={canvasStyle}
- onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
- <svg>
- {paths}
- </svg>
+ <div className="inkingCanvas" >
+ <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
+ {this.props.children()}
+ {this.drawnPaths}
</div >
)
}
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
new file mode 100644
index 000000000..0d8fd8784
--- /dev/null
+++ b/src/client/views/InkingControl.scss
@@ -0,0 +1,135 @@
+@import "global_variables";
+.inking-control {
+ position: absolute;
+ left: 70px;
+ bottom: 70px;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ label,
+ input,
+ option {
+ font-size: 12px;
+ }
+ input[type="range"] {
+ -webkit-appearance: none;
+ background-color: transparent;
+ vertical-align: middle;
+ margin-top: 8px;
+ &:focus {
+ outline: none;
+ }
+ &::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 3px;
+ border-radius: 1.5px;
+ cursor: pointer;
+ background: $intermediate-color;
+ }
+ &::-webkit-slider-thumb {
+ height: 12px;
+ width: 12px;
+ border: 1px solid $intermediate-color;
+ border-radius: 6px;
+ background: $light-color;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -4px;
+ }
+ &::-moz-range-track {
+ width: 100%;
+ height: 3px;
+ border-radius: 1.5px;
+ cursor: pointer;
+ background: $light-color;
+ }
+ &::-moz-range-thumb {
+ height: 12px;
+ width: 12px;
+ border: 1px solid $intermediate-color;
+ border-radius: 6px;
+ background: $light-color;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -4px;
+ }
+ }
+ input[type="text"] {
+ border: none;
+ padding: 0 0px;
+ background: transparent;
+ color: $dark-color;
+ font-size: 12px;
+ margin-top: 4px;
+ }
+ .ink-panel {
+ margin: 6px 12px 6px 0;
+ height: 30px;
+ vertical-align: middle;
+ line-height: 36px;
+ padding: 0 10px;
+ color: $intermediate-color;
+ &:first {
+ margin-top: 0;
+ }
+ }
+ .ink-tools {
+ display: flex;
+ background-color: transparent;
+ border-radius: 0;
+ padding: 0;
+ button {
+ height: 36px;
+ padding: 0px;
+ padding-bottom: 3px;
+ margin-left: 10px;
+ background-color: transparent;
+ color: $intermediate-color;
+ }
+ button:hover {
+ transform: scale(1.15);
+ }
+ }
+ .ink-size {
+ display: flex;
+ justify-content: space-between;
+ input[type="text"] {
+ width: 42px;
+ }
+ >* {
+ margin-right: 6px;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ .ink-color {
+ display: flex;
+ position: relative;
+ padding-right: 0;
+ label {
+ margin-right: 6px;
+ }
+ .ink-color-display {
+ border-radius: 11px;
+ width: 22px;
+ height: 22px;
+ margin-top: 6px;
+ cursor: pointer;
+ text-align: center; // span {
+ // color: $light-color;
+ // font-size: 8px;
+ // user-select: none;
+ // }
+ }
+ .ink-color-picker {
+ background-color: $light-color;
+ border-radius: 5px;
+ padding: 12px;
+ position: absolute;
+ bottom: 36px;
+ left: -3px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index eb2172d03..c1519dff8 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -2,10 +2,9 @@ import { observable, action, computed } from "mobx";
import { CirclePicker, ColorResult } from 'react-color'
import React = require("react");
-import "./InkingCanvas.scss"
import { InkTool } from "../../fields/InkField";
import { observer } from "mobx-react";
-import "./InkingCanvas.scss"
+import "./InkingControl.scss"
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 87b5c43d8..615f8af7e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -6,6 +6,8 @@ import React = require("react");
interface StrokeProps {
+ offsetX: number;
+ offsetY: number;
id: string;
line: Array<{ x: number, y: number }>;
color: string;
@@ -21,21 +23,14 @@ export class InkingStroke extends React.Component<StrokeProps> {
@observable private _strokeColor: string = this.props.color;
@observable private _strokeWidth: string = this.props.width;
- deleteStroke = (e: React.MouseEvent): void => {
+ deleteStroke = (e: React.PointerEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
}
}
parseData = (line: Array<{ x: number, y: number }>): string => {
- if (line.length === 0) {
- return "";
- }
- const pathData = "M " +
- line.map(p => {
- return p.x + " " + p.y;
- }).join(" L ");
- return pathData;
+ return !line.length ? "" : "M " + line.map(p => (p.x + this.props.offsetX) + " " + (p.y + this.props.offsetY)).join(" L ");
}
createStyle() {
@@ -50,15 +45,14 @@ export class InkingStroke extends React.Component<StrokeProps> {
}
}
-
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
+ let pointerEvents: any = InkingControl.Instance.selectedTool == InkTool.Eraser ? "all" : "none";
return (
- <path className={(this._strokeTool === InkTool.Highlighter) ? "highlight" : ""}
- d={pathData} style={pathStyle} strokeLinejoin="round" strokeLinecap="round"
- onMouseOver={this.deleteStroke} onMouseDown={this.deleteStroke} />
+ <path d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
+ onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
)
}
} \ No newline at end of file
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 698a9c617..594803aab 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -7,6 +7,9 @@ body {
overflow: hidden;
font-family: $sans-serif;
margin: 0;
+ position:absolute;
+ top: 0;
+ left:0;
}
#dash-title {
@@ -158,11 +161,15 @@ button:hover {
width:100%;
height:100%;
position:absolute;
+ top: 0;
+ left:0;
}
#mainContent-div {
width:100%;
height:100%;
position:absolute;
+ top: 0;
+ left:0;
}
#add-options-content {
display: table;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 53a5aba6e..6f66f8f38 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,7 @@ import "./Main.scss";
import { MessageStore } from '../../server/Message';
import { Utils } from '../../Utils';
import * as request from 'request'
+import * as rp from 'request-promise'
import { Documents } from '../documents/Documents';
import { Server } from '../Server';
import { setupDrag } from '../util/DragManager';
@@ -22,7 +23,7 @@ import "./Main.scss";
import { observer } from 'mobx-react';
import { InkingControl } from './InkingControl';
import { RouteStore } from '../../server/RouteStore';
-import { library } from '@fortawesome/fontawesome-svg-core';
+import { library, IconName } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFont } from '@fortawesome/free-solid-svg-icons';
import { faImage } from '@fortawesome/free-solid-svg-icons';
@@ -40,27 +41,40 @@ import Measure from 'react-measure';
import { DashUserModel } from '../../server/authentication/models/user_model';
import { ServerUtils } from '../../server/ServerUtil';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { Field, Opt } from '../../fields/Field';
+import { Field, Opt, FieldWaiting } from '../../fields/Field';
import { ListField } from '../../fields/ListField';
import { Gateway, Settings } from '../northstar/manager/Gateway';
-import { Catalog, Schema, Attribute, AttributeGroup } from '../northstar/model/idea/idea';
+import { Catalog, Schema, Attribute, AttributeGroup, AggregateFunction } from '../northstar/model/idea/idea';
import { ArrayUtil } from '../northstar/utils/ArrayUtil';
import '../northstar/model/ModelExtensions'
import '../northstar/utils/Extensions'
+import { HistogramOperation } from '../northstar/operations/HistogramOperation';
+import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel';
+import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel';
@observer
export class Main extends React.Component {
// dummy initializations keep the compiler happy
- @observable private mainContainer?: Document;
@observable private mainfreeform?: Document;
- @observable private userWorkspaces: Document[] = [];
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
- @observable ActiveSchema: Schema | undefined;
- private _northstarColumns: Document[] = [];
+ private _northstarSchemas: Document[] = [];
+
+ @computed private get mainContainer(): Document | undefined {
+ let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document);
+ return doc == FieldWaiting ? undefined : doc;
+ }
+
+ private set mainContainer(doc: Document | undefined) {
+ if (doc) {
+ this.userDocument.Set(KeyStore.ActiveWorkspace, doc);
+ }
+ }
+
+ private get userDocument(): Document {
+ return CurrentUserUtils.UserDocument;
+ }
- public mainDocId: string | undefined;
- private currentUser?: DashUserModel;
public static Instance: Main;
constructor(props: Readonly<{}>) {
@@ -70,12 +84,11 @@ export class Main extends React.Component {
configure({ enforceActions: "observed" });
if (window.location.pathname !== RouteStore.home) {
let pathname = window.location.pathname.split("/");
- this.mainDocId = pathname[pathname.length - 1];
+ if (pathname.length > 1 && pathname[pathname.length - 2] == 'doc') {
+ CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
+ }
};
- let y = "";
- y.ReplaceAll("a", "B");
-
CurrentUserUtils.loadCurrentUser();
library.add(faFont);
@@ -92,7 +105,7 @@ export class Main extends React.Component {
library.add(faTree);
this.initEventListeners();
- Documents.initProtos(() => this.initAuthenticationRouters());
+ this.initAuthenticationRouters();
this.initializeNorthstar();
}
@@ -100,8 +113,8 @@ export class Main extends React.Component {
onHistory = () => {
if (window.location.pathname !== RouteStore.home) {
let pathname = window.location.pathname.split("/");
- this.mainDocId = pathname[pathname.length - 1];
- Server.GetField(this.mainDocId, action((field: Opt<Field>) => {
+ CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
+ Server.GetField(CurrentUserUtils.MainDocId, action((field: Opt<Field>) => {
if (field instanceof Document) {
this.openWorkspace(field, true);
}
@@ -131,63 +144,50 @@ export class Main extends React.Component {
initAuthenticationRouters = () => {
// Load the user's active workspace, or create a new one if initial session after signup
- request.get(ServerUtils.prepend(RouteStore.getActiveWorkspace), (error, response, body) => {
- if (this.mainDocId || body) {
- Server.GetField(this.mainDocId || body, field => {
- if (field instanceof Document) {
- this.openWorkspace(field);
- this.populateWorkspaces();
- } else {
- this.createNewWorkspace(true, this.mainDocId);
- }
- });
- } else {
- this.createNewWorkspace(true, this.mainDocId);
- }
- });
- }
-
- @action
- createNewWorkspace = (init: boolean, id?: string): void => {
- let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` }, id);
- let newId = mainDoc.Id;
- request.post(ServerUtils.prepend(RouteStore.addWorkspace), {
- body: { target: newId },
- json: true
- }, () => { if (init) this.populateWorkspaces(); });
-
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] };
- mainDoc.SetText(KeyStore.Data, JSON.stringify(dockingLayout));
- mainDoc.Set(KeyStore.ActiveFrame, freeformDoc);
- this.openWorkspace(mainDoc);
- let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" })
- mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument);
- }, 0);
- this.userWorkspaces.push(mainDoc);
+ if (!CurrentUserUtils.MainDocId) {
+ this.userDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => {
+ if (doc) {
+ CurrentUserUtils.MainDocId = doc.Id;
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
+ })
+ } else {
+ Server.GetField(CurrentUserUtils.MainDocId).then(field => {
+ if (field instanceof Document) {
+ this.openWorkspace(field)
+ } else {
+ this.createNewWorkspace(CurrentUserUtils.MainDocId);
+ }
+ })
+ }
}
@action
- populateWorkspaces = () => {
- // retrieve all workspace documents from the server
- request.get(ServerUtils.prepend(RouteStore.getAllWorkspaces), (error, res, body) => {
- let ids = JSON.parse(body) as string[];
- Server.GetFields(ids, action((fields: { [id: string]: Field }) => this.userWorkspaces = ids.map(id => fields[id] as Document)));
- });
+ createNewWorkspace = (id?: string): void => {
+ this.userDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => {
+ if (list) {
+ let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] };
+ let mainDoc = Documents.DockDocument(JSON.stringify(dockingLayout), { title: `Main Container ${list.Data.length + 1}` }, id);
+ list.Data.push(mainDoc);
+ CurrentUserUtils.MainDocId = mainDoc.Id;
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => {
+ this.openWorkspace(mainDoc);
+ let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" })
+ mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument);
+ }, 0);
+ }
+ }));
}
@action
openWorkspace = (doc: Document, fromHistory = false): void => {
- request.post(ServerUtils.prepend(RouteStore.setActiveWorkspace), {
- body: { target: doc.Id },
- json: true
- });
this.mainContainer = doc;
fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id);
- this.mainContainer.GetTAsync(KeyStore.ActiveFrame, Document, field => this.mainfreeform = field);
- this.mainContainer.GetTAsync(KeyStore.OptionalRightCollection, Document, col => {
+ this.userDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col => {
// 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(() => {
if (col) {
@@ -201,10 +201,15 @@ export class Main extends React.Component {
});
}
+ @observable
+ workspacesShown: boolean = false;
+
+ areWorkspacesShown = () => {
+ return this.workspacesShown;
+ }
+ @action
toggleWorkspaces = () => {
- if (WorkspacesMenu.Instance) {
- WorkspacesMenu.Instance.toggle()
- }
+ this.workspacesShown = !this.workspacesShown;
}
screenToLocalTransform = () => Transform.Identity
@@ -213,7 +218,8 @@ export class Main extends React.Component {
focusDocument = (doc: Document) => { }
noScaling = () => 1;
- get content() {
+ @computed
+ get mainContent() {
return !this.mainContainer ? (null) :
<DocumentView Document={this.mainContainer}
AddDocument={undefined}
@@ -240,14 +246,14 @@ export class Main extends React.Component {
let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }))
let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => Documents.TreeDocument(this._northstarColumns, { width: 200, height: 200, title: "a tree collection" }));
+ let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas" }));
let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" }));
let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" }));
let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" }));
let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }))
- let btns = [
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Document][] = [
[React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
[React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
[React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
@@ -259,8 +265,6 @@ export class Main extends React.Component {
[React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
]
- let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator()));
-
return < div id="add-nodes-menu" >
<input type="checkbox" id="add-menu-toggle" />
<label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
@@ -268,9 +272,9 @@ export class Main extends React.Component {
<div id="add-options-content">
<ul id="add-options-list">
{btns.map(btn =>
- <li key={btn[1] as string} ><div ref={btn[0] as React.RefObject<HTMLDivElement>}>
- <button className="round-button add-button" title={btn[2] as string} onPointerDown={setupDrag(btn[0] as React.RefObject<HTMLDivElement>, btn[3] as any)} onClick={addClick(btn[3] as any)}>
- <FontAwesomeIcon icon={btn[1] as any} size="sm" />
+ <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>
@@ -300,23 +304,29 @@ export class Main extends React.Component {
}
render() {
+ let workspaceMenu: any = null;
+ let workspaces = this.userDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField);
+ if (workspaces && workspaces !== FieldWaiting) {
+ workspaceMenu = <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={workspaces.Data}
+ isShown={this.areWorkspacesShown} toggle={this.toggleWorkspaces} />
+ }
return (
<div id="main-div">
+ <DocumentDecorations />
<Measure onResize={(r: any) => runInAction(() => {
this.pwidth = r.entry.width;
this.pheight = r.entry.height;
})}>
{({ measureRef }) =>
<div ref={measureRef} id="mainContent-div">
- {this.content}
+ {this.mainContent}
</div>
}
</Measure>
- <DocumentDecorations />
<ContextMenu />
{this.nodesMenu}
{this.miscButtons}
- <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} />
+ {workspaceMenu}
<InkingControl />
</div>
);
@@ -325,9 +335,27 @@ export class Main extends React.Component {
// --------------- Northstar hooks ------------- /
@action SetNorthstarCatalog(ctlog: Catalog) {
+ CurrentUserUtils.NorthstarDBCatalog = ctlog;
if (ctlog && ctlog.schemas) {
- this.ActiveSchema = ArrayUtil.FirstOrDefault<Schema>(ctlog.schemas!, (s: Schema) => s.displayName === "mimic");
- this._northstarColumns = this.GetAllNorthstarColumnAttributes().map(a => Documents.HistogramDocument({ width: 200, height: 200, title: a.displayName! }));
+ this._northstarSchemas = ctlog.schemas.map(schema => {
+ let schemaDoc = Documents.TreeDocument([], { width: 50, height: 100, title: schema.displayName! });
+ let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]);
+ CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+ Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => {
+ if (field instanceof Document) {
+ 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(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias"));
+ }
+ }));
+ });
+ return schemaDoc;
+ })
}
}
async initializeNorthstar(): Promise<void> {
@@ -342,22 +370,10 @@ export class Main extends React.Component {
let cat = Gateway.Instance.ClearCatalog();
cat.then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog()));
}
- public GetAllNorthstarColumnAttributes() {
- if (!this.ActiveSchema || !this.ActiveSchema.rootAttributeGroup) {
- return [];
- }
- const recurs = (attrs: Attribute[], g: AttributeGroup) => {
- if (g.attributes) {
- attrs.push.apply(attrs, g.attributes);
- if (g.attributeGroups) {
- g.attributeGroups.forEach(ng => recurs(attrs, ng));
- }
- }
- };
- const allAttributes: Attribute[] = new Array<Attribute>();
- recurs(allAttributes, this.ActiveSchema.rootAttributeGroup);
- return allAttributes;
- }
}
-ReactDOM.render(<Main />, document.getElementById('root'));
+Documents.initProtos().then(() => {
+ return CurrentUserUtils.loadCurrentUser()
+}).then(() => {
+ ReactDOM.render(<Main />, document.getElementById('root'));
+});
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 2706c3272..583d50c5b 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -3,7 +3,7 @@
}
.collectiondockingview-container {
- position: relative;
+ position: absolute;
top: 0;
left: 0;
overflow: hidden;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fd0810242..39e0dd989 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -17,6 +17,8 @@ import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
import React = require("react");
import { SubCollectionViewProps } from "./CollectionViewBase";
import { ServerUtils } from "../../../server/ServerUtil";
+import { DragManager } from "../../util/DragManager";
+import { TextField } from "../../../fields/TextField";
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -45,9 +47,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
}
- public StartOtherDrag(dragDoc: Document, e: any) {
- this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
- onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })
+ public StartOtherDrag(dragDocs: Document[], e: any) {
+ dragDocs.map(dragDoc =>
+ this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+ onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }));
}
@action
@@ -190,6 +193,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@action
onPointerDown = (e: React.PointerEvent): void => {
var className = (e.target as any).className;
+ if ((className == "lm_title" || className == "lm_tab lm_active") && (e.ctrlKey || e.altKey)) {
+ e.stopPropagation();
+ e.preventDefault();
+ let docid = (e.target as any).DashDocId;
+ let tab = (e.target as any).parentElement as HTMLElement;
+ Server.GetField(docid, action((f: Opt<Field>) => {
+ if (f instanceof Document)
+ DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f as Document]),
+ {
+ handlers: {
+ dragComplete: action(() => { }),
+ },
+ hideSource: false
+ })
+ }));
+ }
if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") {
this._flush = true;
}
@@ -208,6 +227,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this.stateChanged();
}
tabCreated = (tab: any) => {
+ if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type != "stack") {
+ if (tab.titleElement[0].textContent.indexOf("-waiting") != -1) {
+ Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => {
+ if (f != undefined && f instanceof Document) {
+ f.GetTAsync(KeyStore.Title, TextField, (tfield) => {
+ if (tfield != undefined) {
+ tab.titleElement[0].textContent = f.Title;
+ }
+ })
+ }
+ }));
+ tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
+ }
+ tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
+ }
tab.closeElement.off('click') //unbind the current click handler
.click(function () {
tab.contentItem.remove();
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index 0144625c1..0eca3f1cd 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -9,6 +9,8 @@
width: 100%;
height: 100%;
position: absolute;
+ top: 0;
+ left:0;
}
.collectionPdfView-backward {
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index e64b4c945..4d2daf149 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -38,7 +38,7 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> {
public SelectedDocs: FieldId[] = []
public active: () => boolean = () => CollectionView.Active(this);
- addDocument = (doc: Document, allowDuplicates: boolean): void => { CollectionView.AddDocument(this.props, doc, allowDuplicates); }
+ addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
specificContextMenu = (e: React.MouseEvent): void => {
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 34b019244..0ff6c3b40 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,6 +1,6 @@
import React = require("react")
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCog } from '@fortawesome/free-solid-svg-icons';
+import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
@@ -8,7 +8,7 @@ import Measure from "react-measure";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
import "react-table/react-table.css";
import { Document } from "../../../fields/Document";
-import { Field, Opt } from "../../../fields/Field";
+import { Field, Opt, FieldWaiting } from "../../../fields/Field";
import { Key } from "../../../fields/Key";
import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
@@ -24,6 +24,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView";
import { CollectionViewBase } from "./CollectionViewBase";
+import { TextField } from "../../../fields/TextField";
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@@ -85,12 +86,31 @@ export class CollectionSchemaView extends CollectionViewBase {
)
let reference = React.createRef<HTMLDivElement>();
let onItemDown = setupDrag(reference, () => props.doc, (containingCollection: CollectionView) => this.props.removeDocument(props.doc));
+ let applyToDoc = (doc: Document, value: string) => {
+ let script = CompileScript(value, { this: doc }, true);
+ if (!script.compiled) {
+ return false;
+ }
+ let field = script();
+ if (field instanceof Field) {
+ doc.Set(props.fieldKey, field);
+ return true;
+ } else {
+ let dataField = ToField(field);
+ if (dataField) {
+ doc.Set(props.fieldKey, dataField);
+ return true;
+ }
+ }
+ return false;
+ }
return (
- <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "36px" }} key={props.doc.Id} ref={reference}>
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "56px" }} key={props.doc.Id} ref={reference}>
<EditableView
display={"inline"}
contents={contents}
- height={36} GetValue={() => {
+ height={56}
+ GetValue={() => {
let field = props.doc.Get(props.fieldKey);
if (field && field instanceof Field) {
return field.ToScriptString();
@@ -98,22 +118,14 @@ export class CollectionSchemaView extends CollectionViewBase {
return field || "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value);
- if (!script.compiled) {
- return false;
- }
- let field = script();
- if (field instanceof Field) {
- props.doc.Set(props.fieldKey, field);
- return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- props.doc.Set(props.fieldKey, dataField);
- return true;
+ return applyToDoc(props.doc, value);
+ }}
+ OnFillDown={(value: string) => {
+ this.props.Document.GetTAsync<ListField<Document>>(this.props.fieldKey, ListField).then((val) => {
+ if (val) {
+ val.Data.forEach(doc => applyToDoc(doc, value));
}
- }
- return false;
+ })
}}>
</EditableView>
</div>
@@ -238,23 +250,52 @@ export class CollectionSchemaView extends CollectionViewBase {
}
}
+ @action
+ addColumn = () => {
+ this.columns.push(new Key(this.newKeyName));
+ this.newKeyName = "";
+ }
+
+ @observable
+ newKeyName: string = "";
+
+ @action
+ newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.newKeyName = e.currentTarget.value;
+ }
+ onWheel = (e: React.WheelEvent): void => {
+ if (this.props.active())
+ e.stopPropagation();
+ }
+
@observable _optionsActivated: number = 0;
@action
OptionsMenuDown = (e: React.PointerEvent) => {
this._optionsActivated++;
}
+
+ @observable previewScript: string = "this";
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.previewScript = e.currentTarget.value;
+ }
+
render() {
library.add(faCog);
+ library.add(faPlus);
const columns = this.columns;
const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
//all the keys/columns that will be displayed in the schema
const allKeys = this.findAllDocumentKeys;
+ let doc: any = selected ? selected.Get(new Key(this.previewScript)) : undefined;
+
+ // let doc = CompileScript(this.previewScript, { this: selected }, true)();
let content = this._selectedIndex == -1 || !selected ? (null) : (
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
<div className="collectionSchemaView-content" ref={measureRef}>
- <DocumentView Document={selected}
+ {doc instanceof Document ? <DocumentView Document={doc}
AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
isTopMost={false}
SelectOnLoad={false}
@@ -264,7 +305,9 @@ export class CollectionSchemaView extends CollectionViewBase {
PanelHeight={this.getPanelHeight}
ContainingCollectionView={this.props.CollectionView}
focus={this.focusDocument}
- />
+ /> : null}
+ <input value={this.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ position: 'absolute', bottom: '0px' }} />
</div>
}
</Measure>
@@ -286,6 +329,8 @@ export class CollectionSchemaView extends CollectionViewBase {
return (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />)
})}
</ul>
+ <input value={this.newKeyName} onChange={this.newKeyChange} />
+ <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>
</div>
</div>
}>
@@ -293,7 +338,7 @@ export class CollectionSchemaView extends CollectionViewBase {
</Flyout>);
return (
- <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
+ <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
<div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
<Measure onResize={this.setTableDimensions}>
{({ measureRef }) =>
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 6cc14ebcb..70790af18 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -125,7 +125,7 @@ export class CollectionTreeView extends CollectionViewBase {
)
return (
- <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}>
+ <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}>
<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 cbb981b13..ed56ad268 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -3,6 +3,8 @@
width: 100%;
height: 100%;
position: absolute;
+ top: 0;
+ left:0;
}
.collectionVideoView-time{
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 05f759967..470a853e3 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
import { KeyStore } from "../../../fields/KeyStore";
@@ -12,24 +12,26 @@ import "./CollectionVideoView.scss"
@observer
export class CollectionVideoView extends React.Component<CollectionViewProps> {
+ private _intervalTimer: any = undefined;
+ private _player: HTMLVideoElement | undefined = undefined;
+
+ @observable _currentTimecode: number = 0;
+ @observable _isPlaying: boolean = false;
public static LayoutString(fieldKey: string = "DataKey") {
return `<${CollectionVideoView.name} Document={Document}
ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
}
-
- private _mainCont = React.createRef<HTMLDivElement>();
-
private get uIButtons() {
let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().transformDirection(1, 1)[0]);
return ([
<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- <span>{"" + Math.round(this.ctime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((this.ctime - Math.trunc(this.ctime)) * 100)}</span>
+ <span>{"" + Math.round(this._currentTimecode)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span>
</div>,
<div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- {this.playing ? "\"" : ">"}
+ {this._isPlaying ? "\"" : ">"}
</div>,
<div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
F
@@ -37,64 +39,54 @@ export class CollectionVideoView extends React.Component<CollectionViewProps> {
]);
}
-
- // "inherited" CollectionView API starts here...
-
- @observable
- public SelectedDocs: FieldId[] = []
- public active: () => boolean = () => CollectionView.Active(this);
-
- addDocument = (doc: Document, allowDuplicates: boolean): void => { CollectionView.AddDocument(this.props, doc, allowDuplicates); }
- removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
-
- specificContextMenu = (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: () => { } });
+ @action
+ mainCont = (ele: HTMLDivElement | null) => {
+ if (ele) {
+ this._player = ele!.getElementsByTagName("video")[0];
+ if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) {
+ this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+ }
}
}
- get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
- get subView(): any { return CollectionView.SubView(this); }
-
componentDidMount() {
- this.updateTimecode();
+ this._intervalTimer = setInterval(this.updateTimecode, 1000);
}
- get player(): HTMLVideoElement | undefined {
- return this._mainCont.current ? this._mainCont.current.getElementsByTagName("video")[0] : undefined;
+ componentWillUnmount() {
+ clearInterval(this._intervalTimer);
}
@action
updateTimecode = () => {
- if (this.player) {
- this.ctime = this.player.currentTime;
- this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this.ctime));
+ if (this._player) {
+ if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero != -1) {
+ this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero;
+ (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1;
+ } else {
+ this._currentTimecode = this._player.currentTime;
+ this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this._currentTimecode));
+ }
}
- setTimeout(() => this.updateTimecode(), 100)
}
-
- @observable
- ctime: number = 0
- @observable
- playing: boolean = false;
-
@action
onPlayDown = () => {
- if (this.player) {
- if (this.player.paused) {
- this.player.play();
- this.playing = true;
+ if (this._player) {
+ if (this._player.paused) {
+ this._player.play();
+ this._isPlaying = true;
} else {
- this.player.pause();
- this.playing = false;
+ this._player.pause();
+ this._isPlaying = false;
}
}
}
+
@action
onFullDown = (e: React.PointerEvent) => {
- if (this.player) {
- this.player.requestFullscreen();
+ if (this._player) {
+ this._player.requestFullscreen();
e.stopPropagation();
e.preventDefault();
}
@@ -102,15 +94,35 @@ export class CollectionVideoView extends React.Component<CollectionViewProps> {
@action
onResetDown = () => {
- if (this.player) {
- this.player.pause();
- this.player.currentTime = 0;
+ if (this._player) {
+ this._player.pause();
+ this._player.currentTime = 0;
}
}
+ // "inherited" CollectionView API starts here...
+
+ @observable
+ public SelectedDocs: FieldId[] = []
+ public active: () => boolean = () => CollectionView.Active(this);
+
+ addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
+ removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
+
+ specificContextMenu = (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: () => { } });
+ }
+ }
+
+ get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
+ get subView(): any { return CollectionView.SubView(this); }
+
+
render() {
- return (<div className="collectionVideoView-cont" ref={this._mainCont} onContextMenu={this.specificContextMenu}>
+ trace();
+ return (<div className="collectionVideoView-cont" ref={this.mainCont} onContextMenu={this.specificContextMenu}>
{this.subView}
{this.props.isSelected() ? this.uIButtons : (null)}
</div>)
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7e1d31018..014aa1d8f 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -7,13 +7,13 @@ import { ContextMenu } from "../ContextMenu";
import React = require("react");
import { KeyStore } from "../../../fields/KeyStore";
import { NumberField } from "../../../fields/NumberField";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { CollectionDockingView } from "./CollectionDockingView";
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionViewProps } from "./CollectionViewBase";
import { CollectionTreeView } from "./CollectionTreeView";
import { Field, FieldId, FieldWaiting } from "../../../fields/Field";
-import { Main } from "../Main";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
export enum CollectionViewType {
Invalid,
@@ -37,7 +37,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
@observable
public SelectedDocs: FieldId[] = [];
public active: () => boolean = () => CollectionView.Active(this);
- addDocument = (doc: Document, allowDuplicates: boolean): void => { CollectionView.AddDocument(this.props, doc, allowDuplicates); }
+ addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
get subView() { return CollectionView.SubView(this); }
@@ -48,17 +48,49 @@ export class CollectionView extends React.Component<CollectionViewProps> {
return isSelected || childSelected || topMost;
}
+ static createsCycle(documentToAdd: Document, containerDocument: Document): boolean {
+ let data = documentToAdd.GetList<Document>(KeyStore.Data, []);
+ for (let i = 0; i < data.length; i++) {
+ if (CollectionView.createsCycle(data[i], containerDocument))
+ return true;
+ }
+ let annots = documentToAdd.GetList<Document>(KeyStore.Annotations, []);
+ for (let i = 0; i < annots.length; i++) {
+ if (CollectionView.createsCycle(annots[i], containerDocument))
+ return true;
+ }
+ for (let containerProto: any = containerDocument; containerProto && containerProto != FieldWaiting; containerProto = containerProto.GetPrototype()) {
+ if (containerProto.Id == documentToAdd.Id)
+ return true;
+ }
+ return false;
+ }
+
@action
- public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean) {
- doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, -1));
+ public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean): boolean {
+ var curPage = props.Document.GetNumber(KeyStore.CurPage, -1);
+ doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage));
+ if (curPage >= 0) {
+ doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document);
+ }
if (props.Document.Get(props.fieldKey) instanceof Field) {
//TODO This won't create the field if it doesn't already exist
const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
- if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
- value.push(doc);
+ if (!CollectionView.createsCycle(doc, props.Document)) {
+ if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
+ value.push(doc);
+ }
+ else
+ return false;
} else {
- props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
+ let proto = props.Document.GetPrototype();
+ if (!proto || proto == FieldWaiting || !CollectionView.createsCycle(proto, doc)) {
+ props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
+ }
+ else
+ return false;
}
+ return true;
}
@action
@@ -72,11 +104,16 @@ export class CollectionView extends React.Component<CollectionViewProps> {
break;
}
}
+ doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => {
+ if (annotationOn == props.Document) {
+ doc.Set(KeyStore.AnnotationOn, undefined, true);
+ }
+ })
if (index !== -1) {
value.splice(index, 1)
- SelectionManager.DeselectAll()
+ //SelectionManager.DeselectAll()
ContextMenu.Instance.clearItems()
return true;
}
@@ -96,7 +133,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
}
specificContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document.Id != Main.Instance.mainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ 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: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) })
ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) })
ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) })
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index f33007196..458bae7ab 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -14,9 +14,11 @@ import { RouteStore } from "../../../server/RouteStore";
import { TupleField } from "../../../fields/TupleField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { NumberField } from "../../../fields/NumberField";
-import { DocumentManager } from "../../util/DocumentManager";
import request = require("request");
import { ServerUtils } from "../../../server/ServerUtil";
+import { Server } from "../../Server";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { runReactions } from "mobx/lib/internal";
export interface CollectionViewProps {
fieldKey: Key;
@@ -33,7 +35,7 @@ export interface CollectionViewProps {
export interface SubCollectionViewProps extends CollectionViewProps {
active: () => boolean;
- addDocument: (doc: Document, allowDuplicates: boolean) => void;
+ addDocument: (doc: Document, allowDuplicates: boolean) => boolean;
removeDocument: (doc: Document) => boolean;
CollectionView: CollectionView;
}
@@ -59,41 +61,58 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
let email = CurrentUserUtils.email;
if (id && email) {
let textInfo: [string, string] = [id, email];
- doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => {
- let cursors = field.Data;
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
- } else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
- cursors.push(entry);
+ doc.GetTAsync(KeyStore.Prototype, Document).then(proto => {
+ if (!proto) {
+ return;
}
+ proto.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, action((field: ListField<CursorEntry>) => {
+ let cursors = field.Data;
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
+ cursors[ind].Data[1] = position;
+ } else {
+ let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+ cursors.push(entry);
+ }
+ }))
})
-
-
}
}
- protected getCursors(): CursorEntry[] {
- let doc = this.props.Document;
- let id = CurrentUserUtils.id;
- let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
- let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
- return id ? notMe : [];
- }
-
@undoBatch
@action
- protected drop(e: Event, de: DragManager.DropEvent) {
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.aliasOnDrop) {
[KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key =>
- de.data.draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null));
- } else if (de.data.removeDocument) {
+ de.data.draggedDocuments.map((draggedDocument: Document, i: number) =>
+ draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null)));
+ }
+ let added = de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, false), true);
+ if (added && de.data.removeDocument && !de.data.aliasOnDrop) {
de.data.removeDocument(this.props.CollectionView);
}
- this.props.addDocument(de.data.droppedDocument, false);
e.stopPropagation();
+ return added;
}
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc: Document = de.data.linkSourceDocumentView.props.Document;
+ if (sourceDoc) runInAction(() => {
+ let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document)
+ if (srcTarg && srcTarg != FieldWaiting) {
+ let linkDocs = srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]);
+ linkDocs.map(linkDoc => {
+ let targDoc = linkDoc.GetT(KeyStore.LinkedToDocs, Document);
+ if (targDoc && targDoc != FieldWaiting) {
+ let dropdoc = targDoc.MakeDelegate();
+ de.data.droppedDocuments.push(dropdoc);
+ this.props.addDocument(dropdoc, false);
+ }
+ })
+ }
+ })
+ return true;
+ }
+ return false;
}
protected getDocumentFromType(type: string, path: string, options: DocumentOptions): Opt<Document> {
@@ -109,10 +128,26 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
}
if (type.indexOf("pdf") !== -1) {
ctor = Documents.PdfDocument;
+ options.nativeWidth = 1200;
}
if (type.indexOf("html") !== -1) {
+ if (path.includes('localhost')) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ Server.GetField(id).then(field => {
+ if (field instanceof Document) {
+ let alias = field.CreateAlias();
+ alias.SetNumber(KeyStore.X, options.x || 0);
+ alias.SetNumber(KeyStore.Y, options.y || 0);
+ alias.SetNumber(KeyStore.Width, options.width || 300);
+ alias.SetNumber(KeyStore.Height, options.height || options.width || 300);
+ this.props.addDocument(alias, false);
+ }
+ })
+ return undefined;
+ }
ctor = Documents.WebDocument;
- options = { height: options.width, ...options, };
+ options = { height: options.width, ...options, title: path };
}
return ctor ? ctor(path, options) : undefined;
}
@@ -130,7 +165,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
e.stopPropagation()
e.preventDefault()
- if (html && html.indexOf("<img") != 0) {
+ if (html && html.indexOf("<img") != 0 && !html.startsWith("<a")) {
console.log("not good");
let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 });
htmlDoc.SetText(KeyStore.DocumentText, text);
@@ -143,7 +178,6 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
let item = e.dataTransfer.items[i];
if (item.kind === "string" && item.type.indexOf("uri") != -1) {
e.dataTransfer.items[i].getAsString(action((s: string) => {
- let document: Document;
request.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + s), (err, res, body) => {
let type = res.headers["content-type"];
if (type) {
diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx
deleted file mode 100644
index 8c2f3443c..000000000
--- a/src/client/views/collections/MarqueeView.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting, Opt } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { Documents } from "../../documents/Documents";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Transform } from "../../util/Transform";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
-import "./MarqueeView.scss";
-import React = require("react");
-import { InkField, StrokeData } from "../../../fields/InkField";
-import { Utils } from "../../../Utils";
-import { InkingCanvas } from "../InkingCanvas";
-
-interface MarqueeViewProps {
- getMarqueeTransform: () => Transform;
- getTransform: () => Transform;
- container: CollectionFreeFormView;
- addDocument: (doc: Document, allowDuplicates: false) => void;
- activeDocuments: () => Document[];
- selectDocuments: (docs: Document[]) => void;
- removeDocument: (doc: Document) => boolean;
-}
-
-@observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
-{
- private _reactionDisposer: Opt<IReactionDisposer>;
-
- @observable _lastX: number = 0;
- @observable _lastY: number = 0;
- @observable _downX: number = 0;
- @observable _downY: number = 0;
-
- componentDidMount() {
- this._reactionDisposer = reaction(
- () => this.props.container.MarqueeVisible,
- (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY))
- }
- componentWillUnmount() {
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
- this.cleanupInteractions();
- }
-
- @action
- cleanupInteractions = () => {
- document.removeEventListener("pointermove", this.onPointerMove, true)
- document.removeEventListener("pointerup", this.onPointerUp, true);
- document.removeEventListener("keydown", this.marqueeCommand, true);
- }
-
- @action
- onPointerDown = (visible: boolean, downX: number, downY: number): void => {
- if (visible) {
- this._downX = this._lastX = downX;
- this._downY = this._lastY = downY;
- document.addEventListener("pointermove", this.onPointerMove, true)
- document.addEventListener("pointerup", this.onPointerUp, true);
- document.addEventListener("keydown", this.marqueeCommand, true);
- }
- }
-
- @action
- onPointerMove = (e: PointerEvent): void => {
- this._lastX = e.pageX;
- this._lastY = e.pageY;
- }
-
- @action
- onPointerUp = (e: PointerEvent): void => {
- this.cleanupInteractions();
- if (!e.shiftKey) {
- SelectionManager.DeselectAll();
- }
- this.props.selectDocuments(this.marqueeSelect());
- }
-
- intersectRect(r1: { left: number, top: number, width: number, height: number },
- r2: { left: number, top: number, width: number, height: number }) {
- return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
- }
-
- get Bounds() {
- let left = this._downX < this._lastX ? this._downX : this._lastX;
- let top = this._downY < this._lastY ? this._downY : this._lastY;
- let topLeft = this.props.getTransform().transformPoint(left, top);
- let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
- }
-
- @action
- marqueeCommand = (e: KeyboardEvent) => {
- if (e.key == "Backspace" || e.key == "Delete") {
- this.marqueeSelect().map(d => this.props.removeDocument(d));
- this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
- this.cleanupInteractions();
- }
- if (e.key == "c") {
- let bounds = this.Bounds;
- let selected = this.marqueeSelect().map(d => {
- this.props.removeDocument(d);
- d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
- d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
- d.SetNumber(KeyStore.Page, 0);
- d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
- return d;
- });
- let liftedInk = this.marqueeInkSelect(true);
- this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
- //setTimeout(() => {
- let newCollection = Documents.FreeformDocument(selected, {
- x: bounds.left,
- y: bounds.top,
- panx: 0,
- pany: 0,
- width: bounds.width,
- height: bounds.height,
- backgroundColor: "Transparent",
- ink: liftedInk,
- title: "a nested collection"
- });
- this.props.addDocument(newCollection, false);
- // }, 100);
- this.cleanupInteractions();
- }
- }
- marqueeInkSelect(select: boolean) {
- let selRect = this.Bounds;
- let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
- let centerShiftY = 0 - (selRect.top + selRect.height / 2);
- let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
- if (ink && ink != FieldWaiting && ink.Data) {
- let idata = new Map();
- ink.Data.forEach((value: StrokeData, key: string, map: any) => {
- let inside = InkingCanvas.IntersectStrokeRect(value, selRect);
- if (inside && select) {
- idata.set(key,
- {
- pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }),
- color: value.color,
- width: value.width,
- tool: value.tool,
- page: -1
- });
- } else if (!inside && !select) {
- idata.set(key, value);
- }
- })
- return idata;
- }
- }
-
- marqueeSelect() {
- let selRect = this.Bounds;
- let selection: Document[] = [];
- this.props.activeDocuments().map(doc => {
- var x = doc.GetNumber(KeyStore.X, 0);
- var y = doc.GetNumber(KeyStore.Y, 0);
- var w = doc.GetNumber(KeyStore.Width, 0);
- var h = doc.GetNumber(KeyStore.Height, 0);
- if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect))
- selection.push(doc)
- })
- return selection;
- }
-
- render() {
- let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
- let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return (!this.props.container.MarqueeVisible ? (null) : <div className="marqueeView" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />);
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/PreviewCursor.scss b/src/client/views/collections/PreviewCursor.scss
deleted file mode 100644
index a797411f6..000000000
--- a/src/client/views/collections/PreviewCursor.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-
-.previewCursor {
- color: black;
- position: absolute;
- transform-origin: left top;
- pointer-events: none;
-}
-
-//this is an animation for the blinking cursor!
-@keyframes blink {
- 0% {opacity: 0}
- 49%{opacity: 0}
- 50% {opacity: 1}
-}
-
-#previewCursor {
- animation: blink 1s infinite;
-} \ No newline at end of file
diff --git a/src/client/views/collections/PreviewCursor.tsx b/src/client/views/collections/PreviewCursor.tsx
deleted file mode 100644
index cbf36cf9e..000000000
--- a/src/client/views/collections/PreviewCursor.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Opt } from "../../../fields/Field";
-import { Documents } from "../../documents/Documents";
-import { Transform } from "../../util/Transform";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
-import "./PreviewCursor.scss";
-import React = require("react");
-
-
-export interface PreviewCursorProps {
- getTransform: () => Transform;
- container: CollectionFreeFormView;
- addLiveTextDocument: (doc: Document) => void;
-}
-
-@observer
-export class PreviewCursor extends React.Component<PreviewCursorProps> {
- private _reactionDisposer: Opt<IReactionDisposer>;
-
- @observable _lastX: number = 0;
- @observable _lastY: number = 0;
-
- componentDidMount() {
- this._reactionDisposer = reaction(
- () => this.props.container.PreviewCursorVisible,
- (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY))
- }
- componentWillUnmount() {
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
- this.cleanupInteractions();
- }
-
-
- @action
- cleanupInteractions = () => {
- document.removeEventListener("keypress", this.onKeyPress, true);
- }
-
- @action
- onCursorPlaced = (visible: boolean, downX: number, downY: number): void => {
- if (visible) {
- document.addEventListener("keypress", this.onKeyPress, true);
- this._lastX = downX;
- this._lastY = downY;
- } else
- this.cleanupInteractions();
- }
-
- @action
- onKeyPress = (e: KeyboardEvent) => {
- //if not these keys, make a textbox if preview cursor is active!
- if (!e.ctrlKey && !e.altKey && !e.defaultPrevented) {
- //make textbox and add it to this collection
- let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
- let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" });
- this.props.addLiveTextDocument(newBox);
- e.stopPropagation();
- }
- }
-
- render() {
- //get local position and place cursor there!
- let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
- return (
- !this.props.container.PreviewCursorVisible ? (null) :
- <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${x}px, ${y}px)` }}>I</div>)
-
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
new file mode 100644
index 000000000..3b2f79be1
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -0,0 +1,6 @@
+.collectionfreeformlinkview-linkLine {
+ stroke: black;
+ stroke-width: 3;
+ transform: translate(10000px,10000px);
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
new file mode 100644
index 000000000..3dfd74ec8
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -0,0 +1,37 @@
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Utils } from "../../../../Utils";
+import "./CollectionFreeFormLinkView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+export interface CollectionFreeFormLinkViewProps {
+ A: Document;
+ B: Document;
+ LinkDocs: Document[];
+}
+
+@observer
+export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
+
+ onPointerDown = (e: React.PointerEvent) => {
+ this.props.LinkDocs.map(l =>
+ console.log("Link:" + l.Title));
+ }
+ render() {
+ let l = this.props.LinkDocs;
+ let a = this.props.A;
+ let b = this.props.B;
+ let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Width, 0) / 2);
+ let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Height, 0) / 2);
+ let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Width, 0) / 2);
+ let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Height, 0) / 2);
+ return (
+ <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown}
+ style={{ strokeWidth: `${l.length * 5}` }}
+ x1={`${x1}`} y1={`${y1}`}
+ x2={`${x2}`} y2={`${y2}`} />
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
new file mode 100644
index 000000000..30e158603
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -0,0 +1,12 @@
+.collectionfreeformlinksview-svgCanvas{
+ transform: translate(-10000px,-10000px);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20000px;
+ height: 20000px;
+ pointer-events: none;
+ }
+ .collectionfreeformlinksview-container {
+ pointer-events: none;
+ } \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
new file mode 100644
index 000000000..eb20b3100
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -0,0 +1,106 @@
+import { computed, reaction, runInAction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { ListField } from "../../../../fields/ListField";
+import { Utils } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DocumentView } from "../../nodes/DocumentView";
+import { CollectionViewProps } from "../CollectionViewBase";
+import "./CollectionFreeFormLinksView.scss";
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+@observer
+export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+
+ componentDidMount() {
+ reaction(() => {
+ return DocumentManager.Instance.getAllDocumentViews(this.props.Document).map(dv => dv.props.Document.GetNumber(KeyStore.X, 0))
+ }, () => {
+ let views = DocumentManager.Instance.getAllDocumentViews(this.props.Document);
+ for (let i = 0; i < views.length; i++) {
+ for (let j = i + 1; j < views.length; j++) {
+ let srcDoc = views[j].props.Document;
+ let dstDoc = views[i].props.Document;
+ let x1 = srcDoc.GetNumber(KeyStore.X, 0);
+ let x1w = srcDoc.GetNumber(KeyStore.Width, -1);
+ let x2 = dstDoc.GetNumber(KeyStore.X, 0);
+ let x2w = dstDoc.GetNumber(KeyStore.Width, -1);
+ if (x1w < 0 || x2w < 0)
+ continue;
+ dstDoc.GetTAsync(KeyStore.Prototype, Document).then((protoDest) =>
+ srcDoc.GetTAsync(KeyStore.Prototype, Document).then((protoSrc) => runInAction(() => {
+ let dstTarg = (protoDest ? protoDest : dstDoc);
+ let srcTarg = (protoSrc ? protoSrc : srcDoc);
+ let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => {
+ let bdocs = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
+ return (bdocs.length == 0 || (bdocs[0] == dstTarg && bdocs[1] == srcTarg) || (bdocs[0] == srcTarg && bdocs[1] == dstTarg))
+ });
+ let brushAction = (field: ListField<Document>) => {
+ let found = findBrush(field);
+ if (found != -1)
+ field.Data.splice(found, 1);
+ };
+ if (Math.abs(x1 + x1w - x2) < 20 || Math.abs(x2 + x2w - x1) < 20) {
+ let linkDoc: Document = new Document();
+ linkDoc.SetText(KeyStore.Title, "Histogram Brush");
+ linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
+ linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
+
+ brushAction = brushAction = (field: ListField<Document>) => (findBrush(field) == -1) && field.Data.push(linkDoc);
+ }
+ dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ }
+ )))
+ }
+ }
+ })
+ }
+ documentAnchors(view: DocumentView) {
+ let equalViews = [view];
+ let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document);
+ if (containerDoc && containerDoc != FieldWaiting && containerDoc instanceof Document) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype() as Document)
+ }
+ return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == this.props.Document);
+ }
+
+ @computed
+ get uniqueConnections() {
+ let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+ let srcViews = this.documentAnchors(connection.a);
+ let targetViews = this.documentAnchors(connection.b);
+ let possiblePairs: { a: Document, b: Document, }[] = [];
+ srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
+ possiblePairs.map(possiblePair => {
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match = (possiblePair.a == drawnPair.a && possiblePair.b == drawnPair.b);
+ if (match) {
+ if (!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] as Document[] });
+ }
+ })
+ return drawnPairs
+ }, [] as { a: Document, b: Document, l: Document[] }[]);
+ return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
+ }
+
+ render() {
+ return (
+ <div className="collectionfreeformlinksview-container">
+ <svg className="collectionfreeformlinksview-svgCanvas">
+ {this.uniqueConnections}
+ </svg>
+ {this.props.children}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
new file mode 100644
index 000000000..19382e66f
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -0,0 +1,115 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { TextField } from "../../../../fields/TextField";
+import { DragManager } from "../../../util/DragManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
+import { COLLECTION_BORDER_WIDTH } from "../CollectionView";
+import { CollectionViewBase, CollectionViewProps, CursorEntry } from "../CollectionViewBase";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+
+@observer
+export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
+ protected getCursors(): CursorEntry[] {
+ let doc = this.props.Document;
+ let id = CurrentUserUtils.id;
+ let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
+ let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
+ return id ? notMe : [];
+ }
+
+ private crosshairs?: HTMLCanvasElement;
+ drawCrosshairs = (backgroundColor: string) => {
+ if (this.crosshairs) {
+ let c = this.crosshairs;
+ let ctx = c.getContext('2d');
+ if (ctx) {
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, 20, 20);
+
+ ctx.fillStyle = "black";
+ ctx.lineWidth = 0.5;
+
+ ctx.beginPath();
+
+ ctx.moveTo(10, 0);
+ ctx.lineTo(10, 8);
+
+ ctx.moveTo(10, 20);
+ ctx.lineTo(10, 12);
+
+ ctx.moveTo(0, 10);
+ ctx.lineTo(8, 10);
+
+ ctx.moveTo(20, 10);
+ ctx.lineTo(12, 10);
+
+ ctx.stroke();
+
+ // ctx.font = "10px Arial";
+ // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
+ }
+ }
+ }
+ @computed
+ get sharedCursors() {
+ return this.getCursors().map(entry => {
+ if (entry.Data.length > 0) {
+ let id = entry.Data[0][0];
+ let email = entry.Data[0][1];
+ let point = entry.Data[1];
+ this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22")
+ return (
+ <div
+ key={id}
+ style={{
+ position: "absolute",
+ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
+ zIndex: 10000,
+ transformOrigin: 'center center',
+ }}
+ >
+ <canvas
+ ref={(el) => { if (el) this.crosshairs = el }}
+ width={20}
+ height={20}
+ style={{
+ position: 'absolute',
+ width: "20px",
+ height: "20px",
+ opacity: 0.5,
+ borderRadius: "50%",
+ border: "2px solid black"
+ }}
+ />
+ <p
+ style={{
+ fontSize: 14,
+ color: "black",
+ // fontStyle: "italic",
+ marginLeft: -12,
+ marginTop: 4
+ }}
+ >{email[0].toUpperCase()}</p>
+ </div>
+ );
+ }
+ })
+ }
+
+ render() {
+ return this.sharedCursors;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index bdc597a25..79d520069 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,5 +1,13 @@
-@import "../global_variables";
+@import "../../global_variables";
+.collectionfreeformview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+}
.collectionfreeformview-container {
.collectionfreeformview > .jsx-parser {
position: absolute;
@@ -7,9 +15,6 @@
width: 100%;
}
- .inking-canvas {
- transform-origin: 50000px 50000px;
- }
//nested freeform views
// .collectionfreeformview-container {
// background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
@@ -21,22 +26,12 @@
border: 0px solid $light-color-secondary;
border-radius: $border-radius;
box-sizing: border-box;
- position: relative;
+ position: absolute;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
- .collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- .inking-canvas {
- transform-origin: 50000px 50000px;
- }
- }
}
.collectionfreeformview-overlay {
.collectionfreeformview > .jsx-parser {
@@ -46,30 +41,18 @@
.formattedTextBox-cont {
background: $light-color-secondary;
}
-
- .inking-canvas {
- transform-origin: 50000px 50000px;
- }
-
+
opacity: 0.99;
border: 0px solid transparent;
border-radius: $border-radius;
box-sizing: border-box;
- position:relative;
+ position:absolute;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
.collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- .inking-canvas {
- transform-origin: 50000px 50000px;
- }
.formattedTextBox-cont {
background:yellow;
}
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index da9f7b392..1ddb84a99 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,24 +1,26 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { TextField } from "../../../fields/TextField";
-import { DragManager } from "../../util/DragManager";
-import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
-import { InkingCanvas } from "../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "../nodes/DocumentContentsView";
-import { DocumentViewProps } from "../nodes/DocumentView";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { TextField } from "../../../../fields/TextField";
+import { DragManager } from "../../../util/DragManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
+import { COLLECTION_BORDER_WIDTH } from "../CollectionView";
+import { CollectionViewBase } from "../CollectionViewBase";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import "./CollectionFreeFormView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { CollectionViewBase } from "./CollectionViewBase";
import { MarqueeView } from "./MarqueeView";
-import { PreviewCursor } from "./PreviewCursor";
import React = require("react");
import v5 = require("uuid/v5");
+import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
+import { PreviewCursor } from "./PreviewCursor";
+import { NumberField } from "../../../../fields/NumberField";
@observer
export class CollectionFreeFormView extends CollectionViewBase {
@@ -26,12 +28,15 @@ export class CollectionFreeFormView extends CollectionViewBase {
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
public addLiveTextBox = (newBox: Document) => {
- // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself
+ // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself and receive text input
this._selectOnLoaded = newBox.Id;
- //set text to be the typed key and get focus on text box
- this.props.addDocument(newBox, false);
- //remove cursor from screen
- this.PreviewCursorVisible = false;
+ this.addDocument(newBox, false);
+ }
+
+ public addDocument = (newBox: Document, allowDuplicates: boolean) => {
+ let added = this.props.addDocument(newBox, false);
+ this.bringToFront(newBox);
+ return added;
}
public selectDocuments = (docs: Document[]) => {
@@ -41,23 +46,15 @@ export class CollectionFreeFormView extends CollectionViewBase {
public getActiveDocuments = () => {
var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
- let active: Document[] = [];
- if (lvalue && lvalue != FieldWaiting) {
- lvalue.Data.map(doc => {
- var page = doc.GetNumber(KeyStore.Page, -1);
- if (page == curPage || page == -1) {
- active.push(doc);
- }
- })
- }
-
- return active;
+ return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => {
+ var page = doc.GetNumber(KeyStore.Page, -1);
+ if (page == curPage || page == -1) {
+ active.push(doc);
+ }
+ return active;
+ }, [] as Document[]);
}
- //determines whether the blinking cursor for indicating whether a text will be made on key down is visible
- @observable public PreviewCursorVisible: boolean = false;
- @observable public MarqueeVisible = false;
@observable public DownX: number = 0;
@observable public DownY: number = 0;
@observable private _lastX: number = 0;
@@ -66,7 +63,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
@computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
@computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
@computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
- @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
@computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
@computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
@computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
@@ -76,15 +73,36 @@ export class CollectionFreeFormView extends CollectionViewBase {
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- super.drop(e, de);
- if (de.data instanceof DragManager.DocumentDragData) {
- let screenX = de.x - (de.data.xOffset as number || 0);
- let screenY = de.y - (de.data.yOffset as number || 0);
- const [x, y] = this.getTransform().transformPoint(screenX, screenY);
- de.data.droppedDocument.SetNumber(KeyStore.X, x);
- de.data.droppedDocument.SetNumber(KeyStore.Y, y);
- this.bringToFront(de.data.droppedDocument);
+ if (super.drop(e, de)) {
+ let droppedDocs = de.data.droppedDocuments as Document[];
+ let xoff = de.data.xOffset as number || 0;
+ let yoff = de.data.yOffset as number || 0;
+ if (droppedDocs && droppedDocs.length) {
+ let screenX = de.x - xoff;
+ let screenY = de.y - yoff;
+ const [x, y] = this.getTransform().transformPoint(screenX, screenY);
+ let dragDoc = de.data.droppedDocuments[0];
+ let dragX = dragDoc.GetNumber(KeyStore.X, 0);
+ let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
+ droppedDocs.map(async d => {
+ let docX = d.GetNumber(KeyStore.X, 0);
+ let docY = d.GetNumber(KeyStore.Y, 0);
+ d.SetNumber(KeyStore.X, x + (docX - dragX));
+ d.SetNumber(KeyStore.Y, y + (docY - dragY));
+ let docW = await d.GetTAsync(KeyStore.Width, NumberField);
+ let docH = await d.GetTAsync(KeyStore.Height, NumberField);
+ if (!docW) {
+ d.SetNumber(KeyStore.Width, 300);
+ }
+ if (!docH) {
+ d.SetNumber(KeyStore.Height, 300);
+ }
+ this.bringToFront(d);
+ })
+ }
+ return true;
}
+ return false;
}
@@ -92,19 +110,19 @@ export class CollectionFreeFormView extends CollectionViewBase {
cleanupInteractions = () => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.MarqueeVisible = false;
}
@action
onPointerDown = (e: React.PointerEvent): void => {
- this.PreviewCursorVisible = false;
- if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) {
+ if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
this._lastX = this.DownX = e.pageX;
this._lastY = this.DownY = e.pageY;
+ if (this.props.isSelected())
+ e.stopPropagation();
}
}
@@ -112,28 +130,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
onPointerUp = (e: PointerEvent): void => {
e.stopPropagation();
- if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) {
- //show preview text cursor on tap
- this.PreviewCursorVisible = true;
- //select is not already selected
- if (!this.props.isSelected()) {
- this.props.select(false);
- }
- }
this.cleanupInteractions();
}
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.props.active()) {
- if (e.buttons == 1 && !e.altKey && !e.metaKey) {
- this.MarqueeVisible = true;
- }
- if (this.MarqueeVisible) {
- e.stopPropagation();
- e.preventDefault();
- }
- else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
+ if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
@@ -150,7 +153,6 @@ export class CollectionFreeFormView extends CollectionViewBase {
onPointerWheel = (e: React.WheelEvent): void => {
this.props.select(false);
e.stopPropagation();
- e.preventDefault();
let coefficient = 1000;
if (e.ctrlKey) {
@@ -256,150 +258,62 @@ export class CollectionFreeFormView extends CollectionViewBase {
@computed
get views() {
var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
- if (lvalue && lvalue != FieldWaiting) {
- return lvalue.Data.map(doc => {
- var page = doc.GetNumber(KeyStore.Page, 0);
- return (page != curPage && page != 0) ? (null) :
- (<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
- })
- }
- return null;
+ return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {
+ var page = doc.GetNumber(KeyStore.Page, -1);
+ if (page == curPage || page == -1)
+ prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+ return prev;
+ }, [] as JSX.Element[])
}
@computed
get backgroundView() {
return !this.backgroundLayout ? (null) :
(<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
- layoutKey={KeyStore.BackgroundLayout} isSelected={() => false} select={() => { }} />);
+ layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
}
@computed
get overlayView() {
return !this.overlayLayout ? (null) :
(<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
- layoutKey={KeyStore.OverlayLayout} isSelected={() => false} select={() => { }} />);
+ layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
}
getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform())
- getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH)
+ getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH)
getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY);
noScaling = () => 1;
-
- //when focus is lost, this will remove the preview cursor
- @action
- onBlur = (): void => {
- this.PreviewCursorVisible = false;
- }
-
- private crosshairs?: HTMLCanvasElement;
- drawCrosshairs = (backgroundColor: string) => {
- if (this.crosshairs) {
- let c = this.crosshairs;
- let ctx = c.getContext('2d');
- if (ctx) {
- ctx.fillStyle = backgroundColor;
- ctx.fillRect(0, 0, 20, 20);
-
- ctx.fillStyle = "black";
- ctx.lineWidth = 0.5;
-
- ctx.beginPath();
-
- ctx.moveTo(10, 0);
- ctx.lineTo(10, 8);
-
- ctx.moveTo(10, 20);
- ctx.lineTo(10, 12);
-
- ctx.moveTo(0, 10);
- ctx.lineTo(8, 10);
-
- ctx.moveTo(20, 10);
- ctx.lineTo(12, 10);
-
- ctx.stroke();
-
- // ctx.font = "10px Arial";
- // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
- }
- }
- }
+ childViews = () => this.views;
render() {
let [dx, dy] = [this.centeringShiftX, this.centeringShiftY];
const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0);
- // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX;
- // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY;
- // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY));
return (
<div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
- onPointerDown={this.onPointerDown}
- onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
- onWheel={this.onPointerWheel}
- onDrop={this.onDrop.bind(this)}
- onDragOver={this.onDragOver}
- onBlur={this.onBlur}
- style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }}
- tabIndex={0}
- ref={this.createDropTarget}>
- <div className="collectionfreeformview"
- style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
- ref={this._canvasRef}>
- {this.backgroundView}
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
- <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} />
- {this.views}
- {super.getCursors().map(entry => {
- if (entry.Data.length > 0) {
- let id = entry.Data[0][0];
- let email = entry.Data[0][1];
- let point = entry.Data[1];
- this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22")
- return (
- <div
- key={id}
- style={{
- position: "absolute",
- transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
- zIndex: 10000,
- transformOrigin: 'center center',
- }}
- >
- <canvas
- ref={(el) => { if (el) this.crosshairs = el }}
- width={20}
- height={20}
- style={{
- position: 'absolute',
- width: "20px",
- height: "20px",
- opacity: 0.5,
- borderRadius: "50%",
- border: "2px solid black"
- }}
- />
- <p
- style={{
- fontSize: 14,
- color: "black",
- // fontStyle: "italic",
- marginLeft: -12,
- marginTop: 4
- }}
- >{email[0].toUpperCase()}</p>
- </div>
- );
- }
- })}
- </div>
+ onPointerDown={this.onPointerDown} onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
+ onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel}
+ style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} ref={this.createDropTarget}>
<MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
- getMarqueeTransform={this.getMarqueeTransform} getTransform={this.getTransform} />
- {this.overlayView}
-
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} >
+ <div className="collectionfreeformview" ref={this._canvasRef}
+ style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
+ {this.backgroundView}
+ <CollectionFreeFormLinksView {...this.props}>
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+ {this.childViews}
+ </InkingCanvas>
+ </CollectionFreeFormLinksView>
+ <CollectionFreeFormRemoteCursors {...this.props} />
+ </div>
+ {this.overlayView}
+ </PreviewCursor>
+ </MarqueeView>
</div>
);
}
diff --git a/src/client/views/collections/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 6d9a79344..0b406e722 100644
--- a/src/client/views/collections/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,8 +1,16 @@
.marqueeView {
+ position: absolute;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.marquee {
border-style: dashed;
box-sizing: border-box;
position: absolute;
border-width: 1px;
border-color: black;
+ pointer-events: none;
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
new file mode 100644
index 000000000..df150a045
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -0,0 +1,202 @@
+import { action, computed, observable, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { InkField, StrokeData } from "../../../../fields/InkField";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Documents } from "../../../documents/Documents";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./MarqueeView.scss";
+import { PreviewCursor } from "./PreviewCursor";
+import React = require("react");
+
+interface MarqueeViewProps {
+ getContainerTransform: () => Transform;
+ getTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addDocument: (doc: Document, allowDuplicates: false) => boolean;
+ activeDocuments: () => Document[];
+ selectDocuments: (docs: Document[]) => void;
+ removeDocument: (doc: Document) => boolean;
+}
+
+@observer
+export class MarqueeView extends React.Component<MarqueeViewProps>
+{
+ @observable _lastX: number = 0;
+ @observable _lastY: number = 0;
+ @observable _downX: number = 0;
+ @observable _downY: number = 0;
+ @observable _used: boolean = false;
+ @observable _visible: boolean = false;
+ static DRAG_THRESHOLD = 4;
+
+ @action
+ cleanupInteractions = (all: boolean = false) => {
+ if (all) {
+ document.removeEventListener("pointermove", this.onPointerMove, true)
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ } else {
+ this._used = true;
+ }
+ document.removeEventListener("keydown", this.marqueeCommand, true);
+ this._visible = false;
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.buttons == 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) {
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
+ this._used = false;
+ document.addEventListener("pointermove", this.onPointerMove, true)
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ if (!e.cancelBubble) {
+ if (!this._used && e.buttons == 1 && !e.altKey && !e.metaKey &&
+ (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) {
+ this._visible = true;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ this.cleanupInteractions(true);
+ this._visible = false;
+ let mselect = this.marqueeSelect();
+ if (!e.shiftKey) {
+ SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+ }
+ this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+ }
+
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+ }
+
+ @computed
+ get Bounds() {
+ let left = this._downX < this._lastX ? this._downX : this._lastX;
+ let top = this._downY < this._lastY ? this._downY : this._lastY;
+ let topLeft = this.props.getTransform().transformPoint(left, top);
+ let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
+ }
+
+ @action
+ marqueeCommand = (e: KeyboardEvent) => {
+ if (e.key == "Backspace" || e.key == "Delete") {
+ this.marqueeSelect().map(d => this.props.removeDocument(d));
+ let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+ if (ink && ink != FieldWaiting) {
+ this.marqueeInkDelete(ink.Data);
+ }
+ this.cleanupInteractions();
+ }
+ if (e.key == "c") {
+ let bounds = this.Bounds;
+ let selected = this.marqueeSelect().map(d => {
+ this.props.removeDocument(d);
+ d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
+ d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
+ d.SetNumber(KeyStore.Page, -1);
+ d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
+ return d;
+ });
+ let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+ let inkData = ink && ink != FieldWaiting ? ink.Data : undefined;
+ //setTimeout(() => {
+ let newCollection = Documents.FreeformDocument(selected, {
+ x: bounds.left,
+ y: bounds.top,
+ panx: 0,
+ pany: 0,
+ width: bounds.width,
+ height: bounds.height,
+ backgroundColor: "Transparent",
+ ink: inkData ? this.marqueeInkSelect(inkData) : undefined,
+ title: "a nested collection"
+ });
+ this.props.addDocument(newCollection, false);
+ this.marqueeInkDelete(inkData);
+ // }, 100);
+ this.cleanupInteractions();
+ SelectionManager.DeselectAll();
+ }
+ }
+ @action
+ marqueeInkSelect(ink: Map<any, any>) {
+ let idata = new Map();
+ let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+ let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
+ ink.forEach((value: StrokeData, key: string, map: any) => {
+ if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+ idata.set(key,
+ {
+ pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }),
+ color: value.color,
+ width: value.width,
+ tool: value.tool,
+ page: -1
+ });
+ }
+ });
+ return idata;
+ }
+
+ @action
+ marqueeInkDelete(ink?: Map<any, any>) {
+ // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
+ // ink.forEach((value: StrokeData, key: string, map: any) =>
+ // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
+
+ if (ink) {
+ let idata = new Map();
+ ink.forEach((value: StrokeData, key: string, map: any) =>
+ !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+ this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField);
+ }
+ }
+
+ marqueeSelect() {
+ let selRect = this.Bounds;
+ let selection: Document[] = [];
+ this.props.activeDocuments().map(doc => {
+ var x = doc.GetNumber(KeyStore.X, 0);
+ var y = doc.GetNumber(KeyStore.Y, 0);
+ var w = doc.GetNumber(KeyStore.Width, 0);
+ var h = doc.GetNumber(KeyStore.Height, 0);
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect))
+ selection.push(doc)
+ })
+ return selection;
+ }
+
+ @computed
+ get marqueeDiv() {
+ let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+ let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />
+ }
+
+ render() {
+ return <div className="marqueeView" onPointerDown={this.onPointerDown}>
+ {this.props.children}
+ {!this._visible ? (null) : this.marqueeDiv}
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
new file mode 100644
index 000000000..7a67c29bf
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
@@ -0,0 +1,27 @@
+
+.previewCursor {
+ color: black;
+ position: absolute;
+ transform-origin: left top;
+ top: 0;
+ left:0;
+ pointer-events: none;
+}
+.previewCursorView {
+ top: 0;
+ left:0;
+ position: absolute;
+ width:100%;
+ height:100%;
+}
+
+//this is an animation for the blinking cursor!
+// @keyframes blink {
+// 0% {opacity: 0}
+// 49%{opacity: 0}
+// 50% {opacity: 1}
+// }
+
+// #previewCursor {
+// animation: blink 1s infinite;
+// } \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
new file mode 100644
index 000000000..93c98f7b0
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
@@ -0,0 +1,119 @@
+import { action, observable, trace, computed, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { Documents } from "../../../documents/Documents";
+import { Transform } from "../../../util/Transform";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./PreviewCursor.scss";
+import React = require("react");
+import { interfaceDeclaration } from "babel-types";
+
+
+export interface PreviewCursorProps {
+ getTransform: () => Transform;
+ getContainerTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addLiveTextDocument: (doc: Document) => void;
+}
+
+@observer
+export class PreviewCursor extends React.Component<PreviewCursorProps> {
+ @observable _lastX: number = 0;
+ @observable _lastY: number = 0;
+ @observable public _visible: boolean = false;
+ @observable public DownX: number = 0;
+ @observable public DownY: number = 0;
+ _showOnUp: boolean = false;
+
+ @action
+ cleanupInteractions = () => {
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.button == 0 && this.props.container.props.active()) {
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ this._showOnUp = true;
+ this.DownX = e.pageX;
+ this.DownY = e.pageY;
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ }
+ }
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) {
+ this._showOnUp = false;
+ this._visible = false;
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ if (this._showOnUp) {
+ document.addEventListener("keypress", this.onKeyPress, false);
+ this._lastX = this.DownX;
+ this._lastY = this.DownY;
+ this._visible = true;
+ }
+ this.cleanupInteractions();
+ }
+
+ @action
+ onKeyPress = (e: KeyboardEvent) => {
+ // Mixing events between React and Native is finicky. In FormattedTextBox, we set the
+ // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
+ // the keyPress here.
+ //if not these keys, make a textbox if preview cursor is active!
+ if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ //make textbox and add it to this collection
+ let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
+ let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" });
+ this.props.addLiveTextDocument(newBox);
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ this._visible = false;
+ e.stopPropagation();
+ }
+ }
+
+ getPoint = () => this.props.getContainerTransform().transformPoint(this._lastX, this._lastY);
+ getVisible = () => this._visible;
+ setVisible = (v: boolean) => {
+ this._visible = v;
+ document.removeEventListener("keypress", this.onKeyPress, false);
+ }
+ render() {
+ return (
+ <div className="previewCursorView" onPointerDown={this.onPointerDown}>
+ {this.props.children}
+ <PreviewCursorPrompt setVisible={this.setVisible} getPoint={this.getPoint} getVisible={this.getVisible} />
+ </div>
+ )
+ }
+}
+
+export interface PromptProps {
+ getPoint: () => number[];
+ getVisible: () => boolean;
+ setVisible: (v: boolean) => void;
+}
+
+@observer
+export class PreviewCursorPrompt extends React.Component<PromptProps> {
+ private _promptRef = React.createRef<HTMLDivElement>();
+
+ //when focus is lost, this will remove the preview cursor
+ @action onBlur = (): void => this.props.setVisible(false);
+
+ render() {
+ let p = this.props.getPoint();
+ if (this.props.getVisible() && this._promptRef.current)
+ this._promptRef.current.focus();
+ return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._promptRef}
+ style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, opacity: this.props.getVisible() ? 1 : 0 }}>
+ I
+ </div >;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 50dc5a619..1a0f0cbbd 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { computed, trace } from "mobx";
+import { computed } from "mobx";
import { observer } from "mobx-react";
import { KeyStore } from "../../../fields/KeyStore";
import { NumberField } from "../../../fields/NumberField";
@@ -6,6 +6,7 @@ import { Transform } from "../../util/Transform";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
+import { thisExpression } from "babel-types";
@observer
@@ -65,14 +66,19 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView
return <DocumentView {...this.props}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
/>
}
+ panelWidth = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelWidth();
+ panelHeight = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelHeight();
render() {
return (
<div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{
transformOrigin: "left top",
transform: this.transform,
+ pointerEvents: "all",
width: this.width,
height: this.height,
position: "absolute",
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 2f0459f88..77551649c 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -5,7 +5,7 @@ import { Key } from "../../../fields/Key";
import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
@@ -19,7 +19,7 @@ import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
-import { HistogramBox } from "./HistogramBox";
+import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -34,6 +34,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
@computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
@computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
+
CreateBindings(): JsxBindings {
let bindings: JsxBindings = { ...this.props, };
for (const key of this.layoutKeys) {
@@ -47,8 +48,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
render() {
+ let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
+ if (!lkeys || lkeys === FieldWaiting) {
+ return <p>Error loading layout keys</p>;
+ }
return <JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} bindings={this.CreateBindings()}
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ bindings={this.CreateBindings()}
jsx={this.layout}
showWarnings={true}
onError={(test: any) => { console.log(test) }}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 85a115f1c..5126e69f9 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,23 +1,43 @@
@import "../global_variables";
+
.documentView-node {
position: absolute;
+ top: 0;
+ left:0;
background: $light-color; //overflow: hidden;
+
&.minimized {
width: 30px;
height: 30px;
}
+
.top {
background: #232323;
height: 20px;
cursor: pointer;
}
+
.content {
padding: 20px 20px;
height: auto;
box-sizing: border-box;
}
+
.scroll-box {
overflow-y: scroll;
height: calc(100% - 20px);
}
}
+
+.minimized-box {
+ height: 10px;
+ width: 10px;
+ border-radius: 2px;
+ background: $dark-color
+}
+
+.minimized-box:hover {
+ background: $main-accent;
+ transform: scale(1.15);
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fec451b09..b9329f269 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,11 +1,13 @@
import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
-import { Field, Opt, FieldWaiting } from "../../../fields/Field";
+import { Field, FieldWaiting, Opt } from "../../../fields/Field";
import { Key } from "../../../fields/Key";
import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
+import { BooleanField } from "../../../fields/BooleanField";
import { TextField } from "../../../fields/TextField";
+import { ServerUtils } from "../../../server/ServerUtil";
import { Utils } from "../../../Utils";
import { Documents } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
@@ -19,11 +21,10 @@ import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView>;
Document: Document;
- AddDocument?: (doc: Document, allowDuplicates: boolean) => void;
+ AddDocument?: (doc: Document, allowDuplicates: boolean) => boolean;
RemoveDocument?: (doc: Document) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
@@ -34,8 +35,8 @@ export interface DocumentViewProps {
SelectOnLoad: boolean;
}
export interface JsxArgs extends DocumentViewProps {
- Keys: { [name: string]: Key }
- Fields: { [name: string]: Field }
+ Keys: { [name: string]: Key };
+ Fields: { [name: string]: Field };
}
/*
@@ -54,16 +55,16 @@ Example usage of this function:
}
*/
export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
- let Keys: { [name: string]: any } = {}
- let Fields: { [name: string]: any } = {}
+ let Keys: { [name: string]: any } = {};
+ let Fields: { [name: string]: any } = {};
for (const key of keys) {
- let fn = () => { }
- Object.defineProperty(fn, "name", { value: key + "Key" })
+ let fn = () => { };
+ Object.defineProperty(fn, "name", { value: key + "Key" });
Keys[key] = fn;
}
for (const field of fields) {
- let fn = () => { }
- Object.defineProperty(fn, "name", { value: field })
+ let fn = () => { };
+ Object.defineProperty(fn, "name", { value: field });
Fields[field] = fn;
}
let args: JsxArgs = {
@@ -84,58 +85,94 @@ export interface JsxBindings {
[prop: string]: any;
}
-
-
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
private _downX: number = 0;
private _downY: number = 0;
+
private _reactionDisposer: Opt<IReactionDisposer>;
- @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); }
- @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; }
- @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
- @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
- @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
- screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ @computed get active(): boolean {
+ return (
+ SelectionManager.IsSelected(this) ||
+ !this.props.ContainingCollectionView ||
+ this.props.ContainingCollectionView.active()
+ );
+ }
+ @computed get topMost(): boolean {
+ return (
+ !this.props.ContainingCollectionView ||
+ this.props.ContainingCollectionView.collectionViewType ==
+ CollectionViewType.Docking
+ );
+ }
+ @computed get layout(): string {
+ return this.props.Document.GetText(
+ KeyStore.Layout,
+ "<p>Error loading layout data</p>"
+ );
+ }
+ @computed get layoutKeys(): Key[] {
+ return this.props.Document.GetData(
+ KeyStore.LayoutKeys,
+ ListField,
+ new Array<Key>()
+ );
+ }
+ @computed get layoutFields(): Key[] {
+ return this.props.Document.GetData(
+ KeyStore.LayoutFields,
+ ListField,
+ new Array<Key>()
+ );
+ }
+ screenRect = (): ClientRect | DOMRect =>
+ this._mainCont.current
+ ? this._mainCont.current.getBoundingClientRect()
+ : new DOMRect();
onPointerDown = (e: React.PointerEvent): void => {
this._downX = e.clientX;
this._downY = e.clientY;
if (e.shiftKey && e.buttons === 2) {
if (this.props.isTopMost) {
this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey);
- }
- else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e);
+ } else
+ CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
e.stopPropagation();
} else {
if (this.active && !e.isDefaultPrevented()) {
e.stopPropagation();
- document.removeEventListener("pointermove", this.onPointerMove)
+ document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp)
+ document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
}
}
- }
+ };
private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
-
- }
componentDidMount() {
if (this._mainCont.current) {
- this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
}
- runInAction(() => {
- DocumentManager.Instance.DocumentViews.push(this);
- })
+ runInAction(() => DocumentManager.Instance.DocumentViews.push(this));
this._reactionDisposer = reaction(
- () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(),
+ () =>
+ this.props.ContainingCollectionView &&
+ this.props.ContainingCollectionView.SelectedDocs.slice(),
() => {
- if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.indexOf(this.props.Document.Id) != -1)
+ if (
+ this.props.ContainingCollectionView &&
+ this.props.ContainingCollectionView.SelectedDocs.indexOf(
+ this.props.Document.Id
+ ) != -1
+ )
SelectionManager.SelectDoc(this, true);
- });
+ }
+ );
}
componentDidUpdate() {
@@ -143,7 +180,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
this.dropDisposer();
}
if (this._mainCont.current) {
- this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
}
}
@@ -151,10 +190,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
if (this.dropDisposer) {
this.dropDisposer();
}
- runInAction(() => {
- DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
-
- })
+ runInAction(() =>
+ DocumentManager.Instance.DocumentViews.splice(
+ DocumentManager.Instance.DocumentViews.indexOf(this),
+ 1
+ )
+ );
if (this._reactionDisposer) {
this._reactionDisposer();
}
@@ -162,22 +203,28 @@ export class DocumentView extends React.Component<DocumentViewProps> {
startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
if (this._mainCont.current) {
- const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData(this.props.Document);
+ const [left, top] = this.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(0, 0);
+ let dragData = new DragManager.DocumentDragData([this.props.Document]);
dragData.aliasOnDrop = dropAliasOfDraggedDoc;
dragData.xOffset = x - left;
dragData.yOffset = y - top;
dragData.removeDocument = (dropCollectionView: CollectionView) => {
- if (this.props.RemoveDocument && this.props.ContainingCollectionView !== dropCollectionView) {
+ if (
+ this.props.RemoveDocument &&
+ this.props.ContainingCollectionView !== dropCollectionView
+ ) {
this.props.RemoveDocument(this.props.Document);
}
- }
- DragManager.StartDocumentDrag(this._mainCont.current, dragData, {
+ };
+ DragManager.StartDocumentDrag([this._mainCont.current], dragData, {
handlers: {
- dragComplete: action(() => { }),
+ dragComplete: action(() => { })
},
hideSource: !dropAliasOfDraggedDoc
- })
+ });
}
}
@@ -185,8 +232,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
if (e.cancelBubble) {
return;
}
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- document.removeEventListener("pointermove", this.onPointerMove)
+ 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 (!this.topMost || e.buttons == 2 || e.altKey) {
this.startDragging(e.x, e.y, e.ctrlKey || e.altKey);
@@ -194,43 +244,65 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
e.stopPropagation();
e.preventDefault();
- }
+ };
onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onPointerMove)
- document.removeEventListener("pointerup", this.onPointerUp)
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
e.stopPropagation();
- if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
+ if (!SelectionManager.IsSelected(this) &&
+ Math.abs(e.clientX - this._downX) < 4 &&
+ Math.abs(e.clientY - this._downY) < 4
+ ) {
SelectionManager.SelectDoc(this, e.ctrlKey);
}
- }
+ };
stopPropogation = (e: React.SyntheticEvent) => {
e.stopPropagation();
- }
+ };
deleteClicked = (): void => {
if (this.props.RemoveDocument) {
this.props.RemoveDocument(this.props.Document);
}
- }
+ };
fieldsClicked = (e: React.MouseEvent): void => {
if (this.props.AddDocument) {
- this.props.AddDocument(Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }), false);
+ this.props.AddDocument(
+ Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }),
+ false
+ );
}
- }
+ };
fullScreenClicked = (e: React.MouseEvent): void => {
CollectionDockingView.Instance.OpenFullScreen(this.props.Document);
ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked });
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- }
+ ContextMenu.Instance.addItem({
+ description: "Close Full Screen",
+ event: this.closeFullScreenClicked
+ });
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ };
closeFullScreenClicked = (e: React.MouseEvent): void => {
CollectionDockingView.Instance.CloseFullScreen();
ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- }
+ ContextMenu.Instance.addItem({
+ description: "Full Screen",
+ event: this.fullScreenClicked
+ });
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ };
+
+ @action
+ public minimize = (): void => {
+ this.props.Document.SetData(
+ KeyStore.Minimized,
+ true as boolean,
+ BooleanField
+ );
+ SelectionManager.DeselectAll();
+ };
@action
drop = (e: Event, de: DragManager.DropEvent) => {
@@ -242,18 +314,37 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
let linkDoc: Document = new Document();
- linkDoc.Set(KeyStore.Title, new TextField("New Link"));
- linkDoc.Set(KeyStore.LinkDescription, new TextField(""));
- linkDoc.Set(KeyStore.LinkTags, new TextField("Default"));
-
- sourceDoc.GetOrCreateAsync(KeyStore.LinkedToDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) });
- linkDoc.Set(KeyStore.LinkedToDocs, destDoc);
- destDoc.GetOrCreateAsync(KeyStore.LinkedFromDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) });
- linkDoc.Set(KeyStore.LinkedFromDocs, sourceDoc);
+ destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest =>
+ sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc =>
+ runInAction(() => {
+ linkDoc.Set(KeyStore.Title, new TextField("New Link"));
+ linkDoc.Set(KeyStore.LinkDescription, new TextField(""));
+ linkDoc.Set(KeyStore.LinkTags, new TextField("Default"));
+ let dstTarg = protoDest ? protoDest : destDoc;
+ let srcTarg = protoSrc ? protoSrc : sourceDoc;
+ linkDoc.Set(KeyStore.LinkedToDocs, dstTarg);
+ linkDoc.Set(KeyStore.LinkedFromDocs, srcTarg);
+ dstTarg.GetOrCreateAsync(
+ KeyStore.LinkedFromDocs,
+ ListField,
+ field => {
+ (field as ListField<Document>).Data.push(linkDoc);
+ }
+ );
+ srcTarg.GetOrCreateAsync(
+ KeyStore.LinkedToDocs,
+ ListField,
+ field => {
+ (field as ListField<Document>).Data.push(linkDoc);
+ }
+ );
+ })
+ )
+ );
e.stopPropagation();
}
- }
+ };
onDrop = (e: React.DragEvent) => {
if (e.isDefaultPrevented()) {
@@ -267,22 +358,49 @@ export class DocumentView extends React.Component<DocumentViewProps> {
e.stopPropagation();
e.preventDefault();
}
- }
+ };
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
- let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3;
+ let moved =
+ Math.abs(this._downX - e.clientX) > 3 ||
+ Math.abs(this._downY - e.clientY) > 3;
if (moved || e.isDefaultPrevented()) {
- e.preventDefault()
+ e.preventDefault();
return;
}
- e.preventDefault()
+ e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
- ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked })
- ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) })
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) })
+ if (!this.isMinimized()) {
+ ContextMenu.Instance.addItem({
+ description: "Minimize",
+ event: this.minimize
+ });
+ }
+ ContextMenu.Instance.addItem({
+ description: "Full Screen",
+ event: this.fullScreenClicked
+ });
+ ContextMenu.Instance.addItem({
+ description: "Fields",
+ event: this.fieldsClicked
+ });
+ ContextMenu.Instance.addItem({
+ description: "Center",
+ event: () => this.props.focus(this.props.Document)
+ });
+ ContextMenu.Instance.addItem({
+ description: "Open Right",
+ event: () =>
+ CollectionDockingView.Instance.AddRightSplit(this.props.Document)
+ });
+ ContextMenu.Instance.addItem({
+ description: "Copy URL",
+ event: () => {
+ Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id));
+ }
+ });
ContextMenu.Instance.addItem({
description: "Copy ID",
event: () => {
@@ -290,52 +408,95 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
});
//ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
}
- ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ ContextMenu.Instance.addItem({
+ description: "Delete",
+ event: this.deleteClicked
+ });
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
SelectionManager.SelectDoc(this, e.ctrlKey);
- }
+ };
+ isMinimized = () => {
+ let field = this.props.Document.GetT(KeyStore.Minimized, BooleanField);
+ if (field && field !== FieldWaiting) {
+ return field.Data;
+ }
+ };
+
+ @action
+ expand = () => {
+ this.props.Document.SetData(
+ KeyStore.Minimized,
+ false as boolean,
+ BooleanField
+ );
+ };
isSelected = () => {
return SelectionManager.IsSelected(this);
- }
+ };
select = (ctrlPressed: boolean) => {
- SelectionManager.SelectDoc(this, ctrlPressed)
- }
+ SelectionManager.SelectDoc(this, ctrlPressed);
+ };
render() {
if (!this.props.Document) {
- return (null);
- }
- let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
- if (!lkeys || lkeys === FieldWaiting) {
- return <p>Error loading layout keys</p>;
+ return null;
}
+
var scaling = this.props.ContentScaling();
var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
- var backgroundcolor = this.props.Document.GetText(KeyStore.BackgroundColor, "");
- return (
- <div className="documentView-node" ref={this._mainCont}
- style={{
- background: backgroundcolor,
- width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%",
- height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%",
- transformOrigin: "left top",
- transform: `scale(${scaling} , ${scaling})`
- }}
- onDrop={this.onDrop}
- onContextMenu={this.onContextMenu}
- onPointerDown={this.onPointerDown} >
- <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />
- </div >
- )
+
+ if (this.isMinimized()) {
+ return (
+ <div
+ className="minimized-box"
+ ref={this._mainCont}
+ style={{
+ transformOrigin: "left top",
+ transform: `scale(${scaling} , ${scaling})`
+ }}
+ onClick={this.expand}
+ onDrop={this.onDrop}
+ onPointerDown={this.onPointerDown}
+ />
+ );
+ } else {
+ var backgroundcolor = this.props.Document.GetText(
+ KeyStore.BackgroundColor,
+ ""
+ );
+ return (
+ <div
+ className="documentView-node"
+ ref={this._mainCont}
+ style={{
+ background: backgroundcolor,
+ width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%",
+ height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%",
+ transformOrigin: "left top",
+ transform: `scale(${scaling} , ${scaling})`
+ }}
+ onDrop={this.onDrop}
+ onContextMenu={this.onContextMenu}
+ onPointerDown={this.onPointerDown}
+ >
+ <DocumentContentsView
+ {...this.props}
+ isSelected={this.isSelected}
+ select={this.select}
+ layoutKey={KeyStore.Layout}
+ />
+ </div>
+ );
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index f6343c631..4e83ec7b9 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -16,6 +16,9 @@ import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
import { AudioField } from "../../../fields/AudioField";
import { ListField } from "../../../fields/ListField";
+import { DocumentContentsView } from "./DocumentContentsView";
+import { Transform } from "../../util/Transform";
+import { KeyStore } from "../../../fields/KeyStore";
//
@@ -65,7 +68,20 @@ export class FieldView extends React.Component<FieldViewProps> {
return <AudioBox {...this.props} />
}
else if (field instanceof Document) {
- return <div>{field.Title}</div>
+ return (<DocumentContentsView Document={field}
+ AddDocument={undefined}
+ RemoveDocument={undefined}
+ ScreenToLocalTransform={() => Transform.Identity}
+ ContentScaling={() => 1}
+ PanelWidth={() => 100}
+ PanelHeight={() => 100}
+ isTopMost={true}
+ SelectOnLoad={false}
+ focus={() => { }}
+ isSelected={() => false}
+ select={() => false}
+ layoutKey={KeyStore.Layout}
+ ContainingCollectionView={undefined} />)
}
else if (field instanceof ListField) {
return (<div>
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ba9bd9566..0c0efc437 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -3,28 +3,25 @@ import { baseKeymap } from "prosemirror-commands";
import { history, redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { schema } from "../../util/RichTextSchema";
-import { EditorState, Transaction, } from "prosemirror-state";
+import { EditorState, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Opt, FieldWaiting } from "../../../fields/Field";
import "./FormattedTextBox.scss";
-import React = require("react")
+import React = require("react");
import { RichTextField } from "../../../fields/RichTextField";
import { FieldViewProps, FieldView } from "./FieldView";
-import { Plugin } from 'prosemirror-state'
-import { Decoration, DecorationSet } from 'prosemirror-view'
-import { TooltipTextMenu } from "../../util/TooltipTextMenu"
+import { Plugin } from "prosemirror-state";
+import { Decoration, DecorationSet } from "prosemirror-view";
+import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { ContextMenu } from "../../views/ContextMenu";
import { inpRules } from "../../util/RichTextRules";
const { buildMenuItems } = require("prosemirror-example-setup");
const { menuBar } = require("prosemirror-menu");
-
-
-
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"}
-//
+//
// In Code, the node's HTML is specified in the document's parameterized structure as:
// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
// and the node's binding to the specified document KEYNAME as:
@@ -33,141 +30,161 @@ const { menuBar } = require("prosemirror-menu");
// 'fieldKey' property to the Key stored in LayoutKeys
// and 'doc' property to the document that is being rendered
//
-// When rendered() by React, this extracts the TextController from the Document stored at the
-// specified Key and assigns it to an HTML input node. When changes are made to this node,
+// When rendered() by React, this extracts the TextController from the Document stored at the
+// specified Key and assigns it to an HTML input node. When changes are made to this node,
// this will edit the document and assign the new value to that field.
//]
export class FormattedTextBox extends React.Component<FieldViewProps> {
-
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) }
- private _ref: React.RefObject<HTMLDivElement>;
- private _editorView: Opt<EditorView>;
- private _reactionDisposer: Opt<IReactionDisposer>;
-
- constructor(props: FieldViewProps) {
- super(props);
-
- this._ref = React.createRef();
- this.onChange = this.onChange.bind(this);
- }
-
- dispatchTransaction = (tx: Transaction) => {
- if (this._editorView) {
- const state = this._editorView.state.apply(tx);
- this._editorView.updateState(state);
- const { doc, fieldKey } = this.props;
- doc.SetDataOnPrototype(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
- // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
- }
- }
-
- componentDidMount() {
- let state: EditorState;
- const config = {
- schema,
- inpRules, //these currently don't do anything, but could eventually be helpful
- plugins: [
- history(),
- keymap({ "Mod-z": undo, "Mod-y": redo }),
- keymap(baseKeymap),
- this.tooltipMenuPlugin()
- ]
- };
-
- let field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
- if (field && field != FieldWaiting && field.Data) {
- state = EditorState.fromJSON(config, JSON.parse(field.Data));
- } else {
- state = EditorState.create(config);
- }
- if (this._ref.current) {
- this._editorView = new EditorView(this._ref.current, {
- state,
- dispatchTransaction: this.dispatchTransaction
- });
- }
-
- this._reactionDisposer = reaction(() => {
- const field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
- return field && field != FieldWaiting ? field.Data : undefined;
- }, (field) => {
- if (field && this._editorView) {
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)));
- }
- })
- if (this.props.selectOnLoad) {
- this.props.select();
- this._editorView!.focus();
- }
+ public static LayoutString(fieldStr: string = "DataKey") {
+ return FieldView.LayoutString(FormattedTextBox, fieldStr);
+ }
+ private _ref: React.RefObject<HTMLDivElement>;
+ private _editorView: Opt<EditorView>;
+ private _reactionDisposer: Opt<IReactionDisposer>;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+
+ this._ref = React.createRef();
+ this.onChange = this.onChange.bind(this);
+ }
+
+ dispatchTransaction = (tx: Transaction) => {
+ if (this._editorView) {
+ const state = this._editorView.state.apply(tx);
+ this._editorView.updateState(state);
+ const { doc, fieldKey } = this.props;
+ doc.SetDataOnPrototype(
+ fieldKey,
+ JSON.stringify(state.toJSON()),
+ RichTextField
+ );
+ // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
}
-
- componentWillUnmount() {
- if (this._editorView) {
- this._editorView.destroy();
- }
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
+ };
+
+ componentDidMount() {
+ let state: EditorState;
+ const config = {
+ schema,
+ inpRules, //these currently don't do anything, but could eventually be helpful
+ plugins: [
+ history(),
+ keymap({ "Mod-z": undo, "Mod-y": redo }),
+ keymap(baseKeymap),
+ this.tooltipMenuPlugin()
+ ]
+ };
+
+ let field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
+ if (field && field != FieldWaiting && field.Data) {
+ state = EditorState.fromJSON(config, JSON.parse(field.Data));
+ } else {
+ state = EditorState.create(config);
}
-
- shouldComponentUpdate() {
- return false;
+ if (this._ref.current) {
+ this._editorView = new EditorView(this._ref.current, {
+ state,
+ dispatchTransaction: this.dispatchTransaction
+ });
}
- @action
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- const { fieldKey, doc } = this.props;
- doc.SetOnPrototype(fieldKey, new RichTextField(e.target.value))
- // doc.SetData(fieldKey, e.target.value, RichTextField);
- }
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
- e.stopPropagation();
+ this._reactionDisposer = reaction(
+ () => {
+ const field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
+ return field && field != FieldWaiting ? field.Data : undefined;
+ },
+ field => {
+ if (field && this._editorView) {
+ this._editorView.updateState(
+ EditorState.fromJSON(config, JSON.parse(field))
+ );
}
+ }
+ );
+ if (this.props.selectOnLoad) {
+ this.props.select();
+ this._editorView!.focus();
}
+ }
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => {
+ componentWillUnmount() {
+ if (this._editorView) {
+ this._editorView.destroy();
}
-
- specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability });
- // ContextMenu.Instance.addItem({
- // description: "Submenu",
- // items: [
- // {
- // description: "item 1", event:
- // },
- // {
- // description: "item 2", event:
- // }
- // ]
- // })
- // e.stopPropagation()
-
- }
-
- onPointerWheel = (e: React.WheelEvent): void => {
- e.stopPropagation();
- }
-
- tooltipMenuPlugin() {
- return new Plugin({
- view(_editorView) {
- return new TooltipTextMenu(_editorView)
- }
- })
- }
-
- onKeyPress(e: React.KeyboardEvent) {
- e.stopPropagation();
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
}
- render() {
- return (<div className="formattedTextBox-cont"
- onKeyPress={this.onKeyPress}
- onPointerDown={this.onPointerDown}
- onContextMenu={this.specificContextMenu}
- onWheel={this.onPointerWheel}
- ref={this._ref} />)
+ }
+
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ @action
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ const { fieldKey, doc } = this.props;
+ doc.SetOnPrototype(fieldKey, new RichTextField(e.target.value));
+ // doc.SetData(fieldKey, e.target.value, RichTextField);
+ }
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
+ e.stopPropagation();
}
-} \ No newline at end of file
+ };
+
+ //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
+ textCapability = (e: React.MouseEvent): void => {};
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({
+ description: "Text Capability",
+ event: this.textCapability
+ });
+
+ // ContextMenu.Instance.addItem({
+ // description: "Submenu",
+ // items: [
+ // {
+ // description: "item 1", event:
+ // },
+ // {
+ // description: "item 2", event:
+ // }
+ // ]
+ // })
+ // e.stopPropagation()
+ };
+
+ onPointerWheel = (e: React.WheelEvent): void => {
+ e.stopPropagation();
+ };
+
+ tooltipMenuPlugin() {
+ return new Plugin({
+ view(_editorView) {
+ return new TooltipTextMenu(_editorView);
+ }
+ });
+ }
+ onKeyPress(e: React.KeyboardEvent) {
+ e.stopPropagation();
+ // stop propagation doesn't seem to stop propagation of native keyboard events.
+ // so we set a flag on the native event that marks that the event's been handled.
+ // (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
+ }
+ render() {
+ return (
+ <div
+ className="formattedTextBox-cont"
+ onKeyDown={this.onKeyPress}
+ onKeyPress={this.onKeyPress}
+ onPointerDown={this.onPointerDown}
+ onContextMenu={this.specificContextMenu}
+ // tfs: do we need this event handler
+ onWheel={this.onPointerWheel}
+ ref={this._ref}
+ />
+ );
+ }
+}
diff --git a/src/client/views/nodes/HistogramBox.scss b/src/client/views/nodes/HistogramBox.scss
deleted file mode 100644
index 04bf1d732..000000000
--- a/src/client/views/nodes/HistogramBox.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.histogrambox-container {
- padding: 0vw;
- position: relative;
- text-align: center;
- width: 100%;
- height: 100%;
- }
- \ No newline at end of file
diff --git a/src/client/views/nodes/HistogramBox.tsx b/src/client/views/nodes/HistogramBox.tsx
deleted file mode 100644
index 223fdf0d8..000000000
--- a/src/client/views/nodes/HistogramBox.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React = require("react")
-import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./VideoBox.scss";
-import { observable, reaction } from "mobx";
-import { HistogramOperation } from "../../northstar/operations/HistogramOperation";
-import { Main } from "../Main";
-import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel";
-import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel";
-import { AggregateFunction, HistogramResult, DoubleValueAggregateResult } from "../../northstar/model/idea/idea";
-import { ModelHelpers } from "../../northstar/model/ModelHelpers";
-
-@observer
-export class HistogramBox extends React.Component<FieldViewProps> {
-
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr) }
-
- constructor(props: FieldViewProps) {
- super(props);
- }
-
- @observable _histoResult?: HistogramResult;
- _histoOp?: HistogramOperation;
-
- componentDidMount() {
- Main.Instance.GetAllNorthstarColumnAttributes().map(a => {
- if (a.displayName == this.props.doc.Title) {
- var atmod = new ColumnAttributeModel(a);
- this._histoOp = new HistogramOperation(new AttributeTransformationModel(atmod, AggregateFunction.None),
- new AttributeTransformationModel(atmod, AggregateFunction.Count),
- new AttributeTransformationModel(atmod, AggregateFunction.Count));
- reaction(() => [this._histoOp && this._histoOp.Result],
- () => this._histoResult = this._histoOp ? this._histoOp.Result as HistogramResult : undefined
- );
- this._histoOp.Update();
- }
- })
- }
-
- twoString() {
- let str = "";
- if (this._histoResult && !this._histoResult.isEmpty) {
- for (let key in this._histoResult.bins) {
- if (this._histoResult.bins.hasOwnProperty(key)) {
- let bin = this._histoResult.bins[key];
- str += JSON.stringify(bin.binIndex!.toJSON()) + " = ";
- let valueAggregateKey = ModelHelpers.CreateAggregateKey(this._histoOp!.V, this._histoResult, ModelHelpers.AllBrushIndex(this._histoResult));
- let value = ModelHelpers.GetAggregateResult(bin, valueAggregateKey) as DoubleValueAggregateResult;
- if (value && value.hasResult && value.result) {
- str += value.result;
- }
- }
- }
- }
- return str;
- }
-
- render() {
- if (!this._histoResult)
- return (null);
- return (
- <div className="histogrambox-container">
- `HISTOGRAM RESULT : ${this.twoString()}`
- </div>
- )
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index dd2f71b59..e81f8fec7 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -17,6 +17,8 @@ import { faEye } from '@fortawesome/free-solid-svg-icons';
import { faEdit } from '@fortawesome/free-solid-svg-icons';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { undoBatch } from "../../util/UndoManager";
+import { FieldWaiting } from "../../../fields/Field";
+import { NumberField } from "../../../fields/NumberField";
library.add(faEye);
@@ -39,9 +41,26 @@ export class LinkBox extends React.Component<Props> {
e.stopPropagation();
let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
if (docView) {
- docView.props.focus(this.props.pairedDoc);
+ docView.props.focus(docView.props.Document);
} else {
- CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc)
+ this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => {
+ if (!contextDoc) {
+ CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc.MakeDelegate());
+ } else if (contextDoc instanceof Document) {
+ this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => {
+ contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => {
+ if (pfield != cfield)
+ contextDoc.SetNumber(KeyStore.CurPage, pfield.Data);
+ let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
+ if (contextView) {
+ contextView.props.focus(contextDoc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(contextDoc);
+ }
+ })
+ });
+ }
+ });
}
}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index ad947afd5..830dfe6c6 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,12 +1,16 @@
.react-pdf__Page {
transform-origin: left top;
position: absolute;
+ top: 0;
+ left:0;
}
.react-pdf__Document {
position: absolute;
}
.pdfBox-buttonTray {
position:absolute;
+ top: 0;
+ left:0;
z-index: 25;
}
.pdfBox-contentContainer {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3a0ef2d32..f9f5bc8f8 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,5 +1,5 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, observable, reaction, IReactionDisposer, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
import Measure from "react-measure";
@@ -10,6 +10,7 @@ import { FieldWaiting, Opt } from '../../../fields/Field';
import { ImageField } from '../../../fields/ImageField';
import { KeyStore } from '../../../fields/KeyStore';
import { PDFField } from '../../../fields/PDFField';
+import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
import { Annotation } from './Annotation';
import { FieldView, FieldViewProps } from './FieldView';
@@ -17,7 +18,7 @@ import "./ImageBox.scss";
import "./PDFBox.scss";
import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here
import React = require("react")
-import { RouteStore } from "../../../server/RouteStore";
+import { SelectionManager } from "../../util/SelectionManager";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -57,10 +58,11 @@ export class PDFBox extends React.Component<FieldViewProps> {
private _mainDiv = React.createRef<HTMLDivElement>()
private _pdf = React.createRef<HTMLCanvasElement>();
+ @observable private _renderAsSvg = true;
+
//very useful for keeping track of X and y position throughout the PDF Canvas
private initX: number = 0;
private initY: number = 0;
- private initPage: boolean = false;
//checks if tool is on
private _toolOn: boolean = false; //checks if tool is on
@@ -87,18 +89,16 @@ export class PDFBox extends React.Component<FieldViewProps> {
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
+ @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 1); }
+ @computed private get thumbnailPage() { return this.props.doc.GetNumber(KeyStore.ThumbnailPage, -1); }
componentDidMount() {
this._reactionDisposer = reaction(
- () => this.curPage,
+ () => [SelectionManager.SelectedDocuments().slice()],
() => {
- if (this.curPage && this.initPage) {
+ if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage != this.thumbnailPage && !this.props.isSelected()) {
this.saveThumbnail();
this._interactive = true;
- } else {
- if (this.curPage > 0)
- this.initPage = true;
}
},
{ fireImmediately: true });
@@ -378,17 +378,21 @@ export class PDFBox extends React.Component<FieldViewProps> {
@action
saveThumbnail = () => {
+ this._renderAsSvg = false;
setTimeout(() => {
var me = this;
- htmlToImage.toPng(this._mainDiv.current!,
- { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 })
- .then(function (dataUrl: string) {
+ let nwidth = me.props.doc.GetNumber(KeyStore.NativeWidth, 0);
+ let nheight = me.props.doc.GetNumber(KeyStore.NativeHeight, 0);
+ htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
+ .then(action((dataUrl: string) => {
me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
- })
+ me.props.doc.SetNumber(KeyStore.ThumbnailPage, me.props.doc.GetNumber(KeyStore.CurPage, -1));
+ me._renderAsSvg = true;
+ }))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
- }, 1000);
+ }, 250);
}
@action
@@ -428,21 +432,16 @@ export class PDFBox extends React.Component<FieldViewProps> {
this.props.doc.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.doc.GetNumber(KeyStore.Width, 0));
this.props.doc.SetNumber(KeyStore.NativeHeight, nativeHeight);
}
- if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) {
- this.saveThumbnail();
- }
}
@computed
get pdfContent() {
let page = this.curPage;
- if (page == 0)
- page = 1;
const renderHeight = 2400;
let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight;
return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`}>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : ""}>
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
<div className="pdfBox-page" ref={measureRef}>
@@ -464,19 +463,17 @@ export class PDFBox extends React.Component<FieldViewProps> {
return [
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
- <div key="pdfBox-contentShell">
- {this.pdfContent}
- {proxy}
- </div>
+ this.pdfContent,
+ proxy
];
}
@computed
get imageProxyRenderer() {
- let field = this.props.doc.Get(KeyStore.Thumbnail);
- if (field) {
- let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ let thumbField = this.props.doc.Get(KeyStore.Thumbnail);
+ if (thumbField) {
+ let path = thumbField == FieldWaiting || this.thumbnailPage != this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
+ thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
return <img src={path} width="100%" />;
}
return (null);
diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx
index d57dd5c0b..4a4d69e90 100644
--- a/src/client/views/nodes/Sticky.tsx
+++ b/src/client/views/nodes/Sticky.tsx
@@ -1,83 +1,83 @@
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import React = require("react")
-import { observer } from "mobx-react"
-import 'react-pdf/dist/Page/AnnotationLayer.css'
+import "react-image-lightbox/style.css"; // This only needs to be imported once in your app
+import React = require("react");
+import { observer } from "mobx-react";
+import "react-pdf/dist/Page/AnnotationLayer.css";
interface IProps {
- Height: number;
- Width: number;
- X: number;
- Y: number;
+ Height: number;
+ Width: number;
+ X: number;
+ Y: number;
}
/**
- * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file.
- * Improvements that could be made: maybe store line array and store that somewhere for future rerendering.
- *
- * Written By: Andrew Kim
+ * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file.
+ * Improvements that could be made: maybe store line array and store that somewhere for future rerendering.
+ *
+ * Written By: Andrew Kim
*/
@observer
export class Sticky extends React.Component<IProps> {
+ private initX: number = 0;
+ private initY: number = 0;
- private initX: number = 0;
- private initY: number = 0;
+ private _ref = React.createRef<HTMLCanvasElement>();
+ private ctx: any; //context that keeps track of sticky canvas
- private _ref = React.createRef<HTMLCanvasElement>();
- private ctx: any; //context that keeps track of sticky canvas
-
- /**
- * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas
- */
- drawDown = (e: React.PointerEvent) => {
- if (this._ref.current) {
- this.ctx = this._ref.current.getContext("2d");
- let mouse = e.nativeEvent;
- this.initX = mouse.offsetX;
- this.initY = mouse.offsetY;
- this.ctx.beginPath();
- this.ctx.lineTo(this.initX, this.initY);
- this.ctx.strokeStyle = "black";
- document.addEventListener("pointermove", this.drawMove);
- document.addEventListener("pointerup", this.drawUp);
- }
+ /**
+ * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas
+ */
+ drawDown = (e: React.PointerEvent) => {
+ if (this._ref.current) {
+ this.ctx = this._ref.current.getContext("2d");
+ let mouse = e.nativeEvent;
+ this.initX = mouse.offsetX;
+ this.initY = mouse.offsetY;
+ this.ctx.beginPath();
+ this.ctx.lineTo(this.initX, this.initY);
+ this.ctx.strokeStyle = "black";
+ document.addEventListener("pointermove", this.drawMove);
+ document.addEventListener("pointerup", this.drawUp);
}
+ };
- //when user drags
- drawMove = (e: PointerEvent): void => {
- //x and y mouse movement
- let x = this.initX += e.movementX,
- y = this.initY += e.movementY;
- //connects the point
- this.ctx.lineTo(x, y);
- this.ctx.stroke();
-
- }
+ //when user drags
+ drawMove = (e: PointerEvent): void => {
+ //x and y mouse movement
+ let x = (this.initX += e.movementX),
+ y = (this.initY += e.movementY);
+ //connects the point
+ this.ctx.lineTo(x, y);
+ this.ctx.stroke();
+ };
- /**
- * when user lifts the mouse, the drawing ends
- */
- drawUp = (e: PointerEvent) => {
- this.ctx.closePath();
- console.log(this.ctx);
- document.removeEventListener("pointermove", this.drawMove);
- }
+ /**
+ * when user lifts the mouse, the drawing ends
+ */
+ drawUp = (e: PointerEvent) => {
+ this.ctx.closePath();
+ console.log(this.ctx);
+ document.removeEventListener("pointermove", this.drawMove);
+ };
- render() {
- return (
- <div onPointerDown={this.drawDown}>
- <canvas ref={this._ref} height={this.props.Height} width={this.props.Width}
- style={{
- position: "absolute",
- top: "20px",
- left: "0px",
- zIndex: 1,
- background: "yellow",
- transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
- opacity: 0.4
- }}
- />
-
- </div>
- );
- }
-} \ No newline at end of file
+ render() {
+ return (
+ <div onPointerDown={this.drawDown}>
+ <canvas
+ ref={this._ref}
+ height={this.props.Height}
+ width={this.props.Width}
+ style={{
+ position: "absolute",
+ top: "20px",
+ left: "0px",
+ zIndex: 1,
+ background: "yellow",
+ transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
+ opacity: 0.4
+ }}
+ />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 09ae95183..7c0db83a8 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,23 +1,27 @@
import React = require("react")
import { observer } from "mobx-react";
-import { FieldWaiting } from '../../../fields/Field';
+import { FieldWaiting, Opt } from '../../../fields/Field';
import { VideoField } from '../../../fields/VideoField';
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
import Measure from "react-measure";
-import { action, trace, observable } from "mobx";
+import { action, trace, observable, IReactionDisposer, computed, reaction } from "mobx";
import { KeyStore } from "../../../fields/KeyStore";
import { number } from "prop-types";
@observer
export class VideoBox extends React.Component<FieldViewProps> {
+ private _reactionDisposer: Opt<IReactionDisposer>;
+ private _videoRef = React.createRef<HTMLVideoElement>()
public static LayoutString() { return FieldView.LayoutString(VideoBox) }
constructor(props: FieldViewProps) {
super(props);
}
+ @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
+
_loaded: boolean = false;
@@ -39,7 +43,17 @@ export class VideoBox extends React.Component<FieldViewProps> {
}
}
+ get player(): HTMLVideoElement | undefined {
+ return this._videoRef.current ? this._videoRef.current.getElementsByTagName("video")[0] : undefined;
+ }
+ @action
+ setVideoRef = (vref: HTMLVideoElement | null) => {
+ if (this.curPage >= 0 && vref) {
+ vref!.currentTime = this.curPage;
+ (vref! as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage;
+ }
+ }
render() {
let field = this.props.doc.GetT(this.props.fieldKey, VideoField);
@@ -47,15 +61,16 @@ export class VideoBox extends React.Component<FieldViewProps> {
return <div>Loading</div>
}
let path = field.Data.href;
-
- //setTimeout(action(() => this._loaded = true), 500);
+ trace();
return (
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
- <video className="videobox-cont" onClick={() => { }} ref={measureRef}>
- <source src={path} type="video/mp4" />
- Not supported.
+ <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
+ <video className="videobox-cont" onClick={() => { }} ref={this.setVideoRef}>
+ <source src={path} type="video/mp4" />
+ Not supported.
</video>
+ </div>
}
</Measure>
)
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a535b2638..c73bc0c47 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -2,6 +2,8 @@
.webBox-cont {
padding: 0vw;
position: absolute;
+ top: 0;
+ left:0;
width: 100%;
height: 100%;
overflow: scroll;
diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts
new file mode 100644
index 000000000..7378b30a1
--- /dev/null
+++ b/src/fields/BooleanField.ts
@@ -0,0 +1,25 @@
+import { BasicField } from "./BasicField";
+import { FieldId } from "./Field";
+import { Types } from "../server/Message";
+
+export class BooleanField extends BasicField<boolean> {
+ constructor(data: boolean = false as boolean, id?: FieldId, save: boolean = true as boolean) {
+ super(data, save, id);
+ }
+
+ ToScriptString(): string {
+ return `new BooleanField("${this.Data}")`;
+ }
+
+ Copy() {
+ return new BooleanField(this.Data);
+ }
+
+ ToJson(): { type: Types; data: boolean; _id: string } {
+ return {
+ type: Types.Minimized,
+ data: this.Data,
+ _id: this.Id
+ };
+ }
+}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index b6439364a..92d03d765 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -1,6 +1,6 @@
-import { Key } from "./Key"
+import { Key } from "./Key";
import { KeyStore } from "./KeyStore";
-import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field"
+import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field";
import { NumberField } from "./NumberField";
import { ObservableMap, computed, action, runInAction } from "mobx";
import { TextField } from "./TextField";
@@ -9,17 +9,21 @@ import { Server } from "../client/Server";
import { Types } from "../server/Message";
import { UndoManager } from "../client/util/UndoManager";
import { HtmlField } from "./HtmlField";
+import { BooleanField } from "./BooleanField";
export class Document extends Field {
//TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field
- public fields: ObservableMap<string, { key: Key, field: Field }> = new ObservableMap();
+ public fields: ObservableMap<
+ string,
+ { key: Key; field: Field }
+ > = new ObservableMap();
public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
constructor(id?: string, save: boolean = true) {
- super(id)
+ super(id);
if (save) {
- Server.UpdateField(this)
+ Server.UpdateField(this);
}
}
@@ -30,13 +34,35 @@ export class Document extends Field {
}
}
- public Width = () => { return this.GetNumber(KeyStore.Width, 0) }
- public Height = () => { return this.GetNumber(KeyStore.Height, this.GetNumber(KeyStore.NativeWidth, 0) ? this.GetNumber(KeyStore.NativeHeight, 0) / this.GetNumber(KeyStore.NativeWidth, 0) * this.GetNumber(KeyStore.Width, 0) : 0) }
- public Scale = () => { return this.GetNumber(KeyStore.Scale, 1) }
+ public Width = () => {
+ return this.GetNumber(KeyStore.Width, 0);
+ };
+ public Height = () => {
+ return this.GetNumber(
+ KeyStore.Height,
+ this.GetNumber(KeyStore.NativeWidth, 0)
+ ? (this.GetNumber(KeyStore.NativeHeight, 0) /
+ this.GetNumber(KeyStore.NativeWidth, 0)) *
+ this.GetNumber(KeyStore.Width, 0)
+ : 0
+ );
+ };
+ public Scale = () => {
+ return this.GetNumber(KeyStore.Scale, 1);
+ };
@computed
- public get Title() {
- return this.GetText(KeyStore.Title, "<untitled>");
+ public get Title(): string {
+ let title = this.Get(KeyStore.Title, true);
+ if (title)
+ if (title != FieldWaiting && title instanceof TextField)
+ return title.Data;
+ else return "-waiting-";
+ let parTitle = this.GetT(KeyStore.Title, TextField);
+ if (parTitle)
+ if (parTitle != FieldWaiting) return parTitle.Data + ".alias";
+ else return "-waiting-.alias";
+ return "-untitled-";
}
@computed
@@ -47,7 +73,7 @@ export class Document extends Field {
/**
* Get the field in the document associated with the given key. If the
* associated field has not yet been filled in from the server, a request
- * to the server will automatically be sent, the value will be filled in
+ * to the server will automatically be sent, the value will be filled in
* when the request is completed, and {@link Field.ts#FieldWaiting} will be returned.
* @param key - The key of the value to get
* @param ignoreProto - If true, ignore any prototype this document
@@ -55,7 +81,7 @@ export class Document extends Field {
* If false (default), search up the prototype chain, starting at this document,
* for a document that has a field associated with the given key, and return the first
* one found.
- *
+ *
* @returns If the document does not have a field associated with the given key, returns `undefined`.
* If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}.
* If the document does have an associated field, and the field has not been fetched from the server, returns the associated field.
@@ -68,10 +94,10 @@ export class Document extends Field {
} else if (this._proxies.has(key.Id)) {
Server.GetDocumentField(this, key);
/*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
+ The field might have been instantly filled from the cache
+ Maybe we want to just switch back to returning the value
+ from Server.GetDocumentField if it's in the cache
+ */
if (this.fields.has(key.Id)) {
field = this.fields.get(key.Id)!.field;
} else {
@@ -87,10 +113,10 @@ export class Document extends Field {
if (curProxy) {
Server.GetDocumentField(doc, key);
/*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
+ The field might have been instantly filled from the cache
+ Maybe we want to just switch back to returning the value
+ from Server.GetDocumentField if it's in the cache
+ */
if (this.fields.has(key.Id)) {
field = this.fields.get(key.Id)!.field;
} else {
@@ -98,7 +124,10 @@ export class Document extends Field {
}
break;
}
- if ((doc.fields.has(KeyStore.Prototype.Id) || doc._proxies.has(KeyStore.Prototype.Id))) {
+ if (
+ doc.fields.has(KeyStore.Prototype.Id) ||
+ doc._proxies.has(KeyStore.Prototype.Id)
+ ) {
doc = doc.GetPrototype();
} else {
break;
@@ -108,8 +137,7 @@ export class Document extends Field {
break;
}
}
- if (doc == FieldWaiting)
- field = FieldWaiting;
+ if (doc == FieldWaiting) field = FieldWaiting;
}
return field;
@@ -124,22 +152,47 @@ export class Document extends Field {
* Note: The callback will not be called if there is no associated field.
* @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise
*/
- GetAsync(key: Key, callback: (field: Field) => void): boolean {
+ GetAsync(key: Key, callback: (field: Opt<Field>) => void): void {
//TODO: This currently doesn't deal with prototypes
let field = this.fields.get(key.Id);
if (field && field.field) {
callback(field.field);
} else if (this._proxies.has(key.Id)) {
Server.GetDocumentField(this, key, callback);
- return true;
+ } else if (this._proxies.has(KeyStore.Prototype.Id)) {
+ this.GetTAsync(KeyStore.Prototype, Document, proto => {
+ if (proto) {
+ proto.GetAsync(key, callback);
+ } else {
+ callback(undefined);
+ }
+ });
+ } else {
+ callback(undefined);
}
- return false;
}
- GetTAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: Opt<T>) => void): boolean {
- return this.GetAsync(key, (field) => {
- callback(Cast(field, ctor));
- })
+ GetTAsync<T extends Field>(key: Key, ctor: { new(): T }): Promise<Opt<T>>;
+ GetTAsync<T extends Field>(
+ key: Key,
+ ctor: { new(): T },
+ callback: (field: Opt<T>) => void
+ ): void;
+ GetTAsync<T extends Field>(
+ key: Key,
+ ctor: { new(): T },
+ callback?: (field: Opt<T>) => void
+ ): Promise<Opt<T>> | void {
+ let fn = (cb: (field: Opt<T>) => void) => {
+ return this.GetAsync(key, field => {
+ cb(Cast(field, ctor));
+ });
+ };
+ if (callback) {
+ fn(callback);
+ } else {
+ return new Promise(res => fn(res));
+ }
}
/**
@@ -148,10 +201,14 @@ export class Document extends Field {
* or the field associated with the given key is not of the given type.
* @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc.
*/
- GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void {
+ GetOrCreateAsync<T extends Field>(
+ key: Key,
+ ctor: { new(): T },
+ callback: (field: T) => void
+ ): void {
//This currently doesn't deal with prototypes
if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key, (field) => {
+ Server.GetDocumentField(this, key, field => {
if (field && field instanceof ctor) {
callback(field);
} else {
@@ -174,7 +231,11 @@ export class Document extends Field {
* @returns Same as {@link Document#Get}, except will return `undefined`
* if there is an associated field but it is of the wrong type.
*/
- GetT<T extends Field = Field>(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue<T> {
+ GetT<T extends Field = Field>(
+ key: Key,
+ ctor: { new(...args: any[]): T },
+ ignoreProto: boolean = false
+ ): FieldValue<T> {
var getfield = this.Get(key, ignoreProto);
if (getfield != FieldWaiting) {
return Cast(getfield, ctor);
@@ -182,7 +243,11 @@ export class Document extends Field {
return FieldWaiting;
}
- GetOrCreate<T extends Field>(key: Key, ctor: { new(): T }, ignoreProto: boolean = false): T {
+ GetOrCreate<T extends Field>(
+ key: Key,
+ ctor: { new(): T },
+ ignoreProto: boolean = false
+ ): T {
const field = this.GetT(key, ctor, ignoreProto);
if (field && field != FieldWaiting) {
return field;
@@ -192,9 +257,13 @@ export class Document extends Field {
return newField;
}
- GetData<T, U extends Field & { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T {
+ GetData<T, U extends Field & { Data: T }>(
+ key: Key,
+ ctor: { new(): U },
+ defaultVal: T
+ ): T {
let val = this.Get(key);
- let vval = (val && val instanceof ctor) ? val.Data : defaultVal;
+ let vval = val && val instanceof ctor ? val.Data : defaultVal;
return vval;
}
@@ -202,6 +271,10 @@ export class Document extends Field {
return this.GetData(key, HtmlField, defaultVal);
}
+ GetBoolean(key: Key, defaultVal: boolean): boolean {
+ return this.GetData(key, BooleanField, defaultVal);
+ }
+
GetNumber(key: Key, defaultVal: number): number {
return this.GetData(key, NumberField, defaultVal);
}
@@ -211,14 +284,7 @@ export class Document extends Field {
}
GetList<T extends Field>(key: Key, defaultVal: T[]): T[] {
- return this.GetData<T[], ListField<T>>(key, ListField, defaultVal)
- }
-
- @action
- SetOnPrototype(key: Key, field: Field | undefined): void {
- this.GetAsync(KeyStore.Prototype, (f: Field) => {
- (f as Document).Set(key, field)
- })
+ return this.GetData<T[], ListField<T>>(key, ListField, defaultVal);
}
@action
@@ -226,16 +292,15 @@ export class Document extends Field {
let old = this.fields.get(key.Id);
let oldField = old ? old.field : undefined;
if (setOnPrototype) {
- this.SetOnPrototype(key, field)
- }
- else {
+ this.SetOnPrototype(key, field);
+ } else {
if (field) {
this.fields.set(key.Id, { key, field });
- this._proxies.set(key.Id, field.Id)
+ this._proxies.set(key.Id, field.Id);
// Server.AddDocumentField(this, key, field);
} else {
this.fields.delete(key.Id);
- this._proxies.delete(key.Id)
+ this._proxies.delete(key.Id);
// Server.DeleteDocumentField(this, key);
}
Server.UpdateField(this);
@@ -244,26 +309,42 @@ export class Document extends Field {
UndoManager.AddEvent({
undo: () => this.Set(key, oldField, setOnPrototype),
redo: () => this.Set(key, field, setOnPrototype)
- })
+ });
}
}
@action
- SetDataOnPrototype<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) {
- this.GetAsync(KeyStore.Prototype, (f: Field) => {
- (f as Document).SetData(key, value, ctor)
- })
+ SetOnPrototype(key: Key, field: Field | undefined): void {
+ this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+ f && f.Set(key, field);
+ });
}
@action
- SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) {
+ SetDataOnPrototype<T, U extends Field & { Data: T }>(
+ key: Key,
+ value: T,
+ ctor: { new(): U },
+ replaceWrongType = true
+ ) {
+ this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+ f && f.SetData(key, value, ctor);
+ });
+ }
+ @action
+ SetData<T, U extends Field & { Data: T }>(
+ key: Key,
+ value: T,
+ ctor: { new(data: T): U },
+ replaceWrongType = true
+ ) {
let field = this.Get(key, true);
if (field instanceof ctor) {
field.Data = value;
} else if (!field || replaceWrongType) {
- let newField = new ctor();
- newField.Data = value;
+ let newField = new ctor(value);
+ // newField.Data = value;
this.Set(key, newField);
}
}
@@ -293,12 +374,12 @@ export class Document extends Field {
}
CreateAlias(id?: string): Document {
- let alias = new Document(id)
- this.GetAsync(KeyStore.Prototype, (f: Field) => {
- alias.Set(KeyStore.Prototype, f)
- })
+ let alias = new Document(id);
+ this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+ f && alias.Set(KeyStore.Prototype, f);
+ });
- return alias
+ return alias;
}
MakeDelegate(id?: string): Document {
@@ -318,7 +399,11 @@ export class Document extends Field {
}
GetValue() {
return this.Title;
- var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
+ var title =
+ (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) +
+ "(" +
+ this.Id +
+ ")";
return title;
//throw new Error("Method not implemented.");
}
@@ -326,11 +411,11 @@ export class Document extends Field {
throw new Error("Method not implemented.");
}
- ToJson(): { type: Types, data: [string, string][], _id: string } {
- let fields: [string, string][] = []
+ ToJson(): { type: Types; data: [string, string][]; _id: string } {
+ let fields: [string, string][] = [];
this._proxies.forEach((field, key) => {
if (field) {
- fields.push([key, field as string])
+ fields.push([key, field as string]);
}
});
@@ -338,6 +423,6 @@ export class Document extends Field {
type: Types.Document,
data: fields,
_id: this.Id
- }
+ };
}
-} \ No newline at end of file
+}
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 319702cc4..6ed3b1604 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -1,45 +1,50 @@
import { Key } from "./Key";
+import { KeyTransfer } from "../server/Message";
export namespace KeyStore {
- export const Prototype = new Key("Prototype");
- export const X = new Key("X");
- export const Y = new Key("Y");
- export const Page = new Key("Page");
- export const Title = new Key("Title");
- export const Author = new Key("Author");
- export const PanX = new Key("PanX");
- export const PanY = new Key("PanY");
- export const Scale = new Key("Scale");
- export const NativeWidth = new Key("NativeWidth");
- export const NativeHeight = new Key("NativeHeight");
- export const Width = new Key("Width");
- export const Height = new Key("Height");
- export const ZIndex = new Key("ZIndex");
- export const Data = new Key("Data");
- export const Annotations = new Key("Annotations");
- export const ViewType = new Key("ViewType");
- export const Layout = new Key("Layout");
- export const BackgroundColor = new Key("BackgroundColor");
- export const BackgroundLayout = new Key("BackgroundLayout");
- export const OverlayLayout = new Key("OverlayLayout");
- export const LayoutKeys = new Key("LayoutKeys");
- export const LayoutFields = new Key("LayoutFields");
- export const ColumnsKey = new Key("SchemaColumns");
- export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
- export const Caption = new Key("Caption");
- export const ActiveFrame = new Key("ActiveFrame");
- export const ActiveDB = new Key("ActiveDB");
- export const DocumentText = new Key("DocumentText");
- export const LinkedToDocs = new Key("LinkedToDocs");
- export const LinkedFromDocs = new Key("LinkedFromDocs");
- export const LinkDescription = new Key("LinkDescription");
- export const LinkTags = new Key("LinkTag");
- export const Thumbnail = new Key("Thumbnail");
- export const CurPage = new Key("CurPage");
- export const NumPages = new Key("NumPages");
- export const Ink = new Key("Ink");
- export const Cursors = new Key("Cursors");
- export const OptionalRightCollection = new Key("OptionalRightCollection");
- export const Archives = new Key("Archives");
- export const Updated = new Key("Updated");
+ export const Prototype = new Key("Prototype");
+ export const X = new Key("X");
+ export const Y = new Key("Y");
+ export const Page = new Key("Page");
+ export const Title = new Key("Title");
+ export const Author = new Key("Author");
+ export const PanX = new Key("PanX");
+ export const PanY = new Key("PanY");
+ export const Scale = new Key("Scale");
+ export const NativeWidth = new Key("NativeWidth");
+ export const NativeHeight = new Key("NativeHeight");
+ export const Width = new Key("Width");
+ export const Height = new Key("Height");
+ export const ZIndex = new Key("ZIndex");
+ export const Data = new Key("Data");
+ export const Annotations = new Key("Annotations");
+ export const ViewType = new Key("ViewType");
+ export const Layout = new Key("Layout");
+ export const BackgroundColor = new Key("BackgroundColor");
+ export const BackgroundLayout = new Key("BackgroundLayout");
+ export const OverlayLayout = new Key("OverlayLayout");
+ export const LayoutKeys = new Key("LayoutKeys");
+ export const LayoutFields = new Key("LayoutFields");
+ export const ColumnsKey = new Key("SchemaColumns");
+ export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
+ export const Caption = new Key("Caption");
+ export const ActiveWorkspace = new Key("ActiveWorkspace");
+ export const DocumentText = new Key("DocumentText");
+ export const BrushingDocs = new Key("BrushingDocs");
+ export const LinkedToDocs = new Key("LinkedToDocs");
+ export const LinkedFromDocs = new Key("LinkedFromDocs");
+ export const LinkDescription = new Key("LinkDescription");
+ export const LinkTags = new Key("LinkTag");
+ export const Thumbnail = new Key("Thumbnail");
+ export const ThumbnailPage = new Key("ThumbnailPage");
+ export const CurPage = new Key("CurPage");
+ export const AnnotationOn = new Key("AnnotationOn");
+ export const NumPages = new Key("NumPages");
+ export const Ink = new Key("Ink");
+ export const Cursors = new Key("Cursors");
+ export const OptionalRightCollection = new Key("OptionalRightCollection");
+ export const Archives = new Key("Archives");
+ export const Updated = new Key("Updated");
+ export const Workspaces = new Key("Workspaces");
+ export const Minimized = new Key("Minimized");
}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
index 4527ee548..c4008bd12 100644
--- a/src/fields/ListField.ts
+++ b/src/fields/ListField.ts
@@ -4,6 +4,7 @@ import { UndoManager } from "../client/util/UndoManager";
import { Types } from "../server/Message";
import { BasicField } from "./BasicField";
import { Field, FieldId } from "./Field";
+import { FieldMap } from "../client/SocketStub";
export class ListField<T extends Field> extends BasicField<T[]> {
private _proxies: string[] = []
@@ -71,7 +72,7 @@ export class ListField<T extends Field> extends BasicField<T[]> {
}
init(callback: (field: Field) => any) {
- Server.GetFields(this._proxies, action((fields: { [index: string]: Field }) => {
+ Server.GetFields(this._proxies, action((fields: FieldMap) => {
if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
var dataids = this.data.map(d => d.Id);
var proxies = this._proxies.map(p => p);
@@ -111,7 +112,7 @@ export class ListField<T extends Field> extends BasicField<T[]> {
ToJson(): { type: Types, data: string[], _id: string } {
return {
type: Types.List,
- data: this._proxies,
+ data: this._proxies || [],
_id: this.Id
}
}
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 16808a598..ae48dd2c6 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,15 +1,14 @@
import * as ReactDOM from 'react-dom';
-import React = require('react');
-import "./ImageUpload.scss"
+import * as rp from 'request-promise';
+import { Documents } from '../client/documents/Documents';
+import { Server } from '../client/Server';
import { Document } from '../fields/Document';
import { KeyStore } from '../fields/KeyStore';
-import { Server } from '../client/Server';
-import { Documents } from '../client/documents/Documents';
import { ListField } from '../fields/ListField';
-import { ImageField } from '../fields/ImageField';
-import request = require('request');
-import { ServerUtils } from '../server/ServerUtil';
import { RouteStore } from '../server/RouteStore';
+import { ServerUtils } from '../server/ServerUtil';
+import "./ImageUpload.scss";
+import React = require('react');
@@ -40,28 +39,24 @@ const onFileLoad = (file: any) => {
json.map((file: any) => {
let path = window.location.origin + file
var doc: Document = Documents.ImageDocument(path, { nativeWidth: 200, width: 200 })
- doc.GetTAsync(KeyStore.Data, ImageField, (i) => {
- if (i) {
- document.getElementById("message")!.innerText = i.Data.href;
+
+ rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(res => {
+ if (res) {
+ return Server.GetField(res);
}
- })
- request.get(ServerUtils.prepend(RouteStore.getActiveWorkspace), (error, response, body) => {
- if (body) {
- Server.GetField(body, field => {
- if (field instanceof Document) {
- field.GetTAsync(KeyStore.OptionalRightCollection, Document,
- pending => {
- if (pending) {
- pending.GetOrCreateAsync(KeyStore.Data, ListField, list => {
- list.Data.push(doc);
- })
- }
- })
- }
- }
- );
+ throw new Error("No user id returned");
+ }).then(field => {
+ if (field instanceof Document) {
+ return field.GetTAsync(KeyStore.OptionalRightCollection, Document)
}
- })
+ }).then(pending => {
+ if (pending) {
+ pending.GetOrCreateAsync(KeyStore.Data, ListField, list => {
+ list.Data.push(doc);
+ })
+ }
+ });
+
// console.log(window.location.origin + file[0])
//imgPrev.setAttribute("src", window.location.origin + files[0].name)
diff --git a/src/server/Message.ts b/src/server/Message.ts
index a2d1ab829..29df57419 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,125 +1,145 @@
import { Utils } from "../Utils";
export class Message<T> {
- private name: string;
- private guid: string;
+ private name: string;
+ private guid: string;
- get Name(): string {
- return this.name;
- }
+ get Name(): string {
+ return this.name;
+ }
- get Message(): string {
- return this.guid
- }
+ get Message(): string {
+ return this.guid;
+ }
- constructor(name: string) {
- this.name = name;
- this.guid = Utils.GenerateDeterministicGuid(name)
- }
+ constructor(name: string) {
+ this.name = name;
+ this.guid = Utils.GenerateDeterministicGuid(name);
+ }
- GetValue() {
- return this.Name;
- }
+ GetValue() {
+ return this.Name;
+ }
}
class TestMessageArgs {
- hello: string = "";
+ hello: string = "";
}
export class SetFieldArgs {
- field: string;
- value: any;
+ field: string;
+ value: any;
- constructor(f: string, v: any) {
- this.field = f
- this.value = v
- }
+ constructor(f: string, v: any) {
+ this.field = f;
+ this.value = v;
+ }
}
export class GetFieldArgs {
- field: string;
+ field: string;
- constructor(f: string) {
- this.field = f
- }
+ constructor(f: string) {
+ this.field = f;
+ }
}
export enum Types {
- Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF, Tuple
+ Number,
+ List,
+ Key,
+ Image,
+ Web,
+ Document,
+ Text,
+ RichText,
+ DocumentReference,
+ Html,
+ Video,
+ Audio,
+ Ink,
+ PDF,
+ Tuple,
+ HistogramOp,
+ Minimized
}
export class DocumentTransfer implements Transferable {
- readonly type = Types.Document
- _id: string
-
- constructor(readonly obj: { type: Types, data: [string, string][], _id: string }) {
- this._id = obj._id
- }
+ readonly type = Types.Document;
+ _id: string;
+
+ constructor(
+ readonly obj: { type: Types; data: [string, string][]; _id: string }
+ ) {
+ this._id = obj._id;
+ }
}
export class ImageTransfer implements Transferable {
- readonly type = Types.Image
+ readonly type = Types.Image;
- constructor(readonly _id: string) { }
+ constructor(readonly _id: string) {}
}
export class KeyTransfer implements Transferable {
- name: string
- readonly _id: string
- readonly type = Types.Key
-
- constructor(i: string, n: string) {
- this.name = n
- this._id = i
- }
+ name: string;
+ readonly _id: string;
+ readonly type = Types.Key;
+
+ constructor(i: string, n: string) {
+ this.name = n;
+ this._id = i;
+ }
}
export class ListTransfer implements Transferable {
- type = Types.List;
+ type = Types.List;
- constructor(readonly _id: string) { }
+ constructor(readonly _id: string) {}
}
export class NumberTransfer implements Transferable {
- readonly type = Types.Number
+ readonly type = Types.Number;
- constructor(readonly value: number, readonly _id: string) { }
+ constructor(readonly value: number, readonly _id: string) {}
}
export class TextTransfer implements Transferable {
- value: string
- readonly _id: string
- readonly type = Types.Text
-
- constructor(t: string, i: string) {
- this.value = t
- this._id = i
- }
+ value: string;
+ readonly _id: string;
+ readonly type = Types.Text;
+
+ constructor(t: string, i: string) {
+ this.value = t;
+ this._id = i;
+ }
}
export class RichTextTransfer implements Transferable {
- value: string
- readonly _id: string
- readonly type = Types.Text
-
- constructor(t: string, i: string) {
- this.value = t
- this._id = i
- }
+ value: string;
+ readonly _id: string;
+ readonly type = Types.Text;
+
+ constructor(t: string, i: string) {
+ this.value = t;
+ this._id = i;
+ }
}
export interface Transferable {
- readonly _id: string
- readonly type: Types
+ readonly _id: string;
+ readonly type: Types;
}
export namespace MessageStore {
- export const Foo = new Message<string>("Foo");
- export const Bar = new Message<string>("Bar");
- export const AddDocument = new Message<DocumentTransfer>("Add Document");
- export const SetField = new Message<{ _id: string, data: any, type: Types }>("Set Field")
- export const GetField = new Message<string>("Get Field")
- export const GetFields = new Message<string[]>("Get Fields")
- export const GetDocument = new Message<string>("Get Document");
- export const DeleteAll = new Message<any>("Delete All");
-} \ No newline at end of file
+ export const Foo = new Message<string>("Foo");
+ export const Bar = new Message<string>("Bar");
+ export const AddDocument = new Message<DocumentTransfer>("Add Document");
+ export const SetField = new Message<{ _id: string; data: any; type: Types }>(
+ "Set Field"
+ );
+ export const GetField = new Message<string>("Get Field");
+ export const GetFields = new Message<string[]>("Get Fields");
+ export const GetDocument = new Message<string>("Get Document");
+ export const DeleteAll = new Message<any>("Delete All");
+}
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index fb06b878b..fdf5b6a5c 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -15,10 +15,7 @@ export enum RouteStore {
// USER AND WORKSPACES
getCurrUser = "/getCurrentUser",
- addWorkspace = "/addWorkspaceId",
- getAllWorkspaces = "/getAllWorkspaceIds",
- getActiveWorkspace = "/getActiveWorkspaceId",
- setActiveWorkspace = "/setActiveWorkspaceId",
+ getUserDocumentId = "/getUserDocumentId",
updateCursor = "/updateCursor",
openDocumentWithId = "/doc/:docId",
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index f10f82deb..d3409abf4 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -1,76 +1,84 @@
-
-import { Field } from './../fields/Field';
-import { TextField } from './../fields/TextField';
-import { NumberField } from './../fields/NumberField';
-import { RichTextField } from './../fields/RichTextField';
-import { Key } from './../fields/Key';
-import { ImageField } from './../fields/ImageField';
-import { ListField } from './../fields/ListField';
-import { Document } from './../fields/Document';
-import { Server } from './../client/Server';
-import { Types } from './Message';
-import { Utils } from '../Utils';
-import { HtmlField } from '../fields/HtmlField';
-import { WebField } from '../fields/WebField';
-import { AudioField } from '../fields/AudioField';
-import { VideoField } from '../fields/VideoField';
-import { InkField } from '../fields/InkField';
-import { PDFField } from '../fields/PDFField';
-import { TupleField } from '../fields/TupleField';
-
-
-
+import { Field } from "./../fields/Field";
+import { TextField } from "./../fields/TextField";
+import { NumberField } from "./../fields/NumberField";
+import { RichTextField } from "./../fields/RichTextField";
+import { Key } from "./../fields/Key";
+import { ImageField } from "./../fields/ImageField";
+import { ListField } from "./../fields/ListField";
+import { Document } from "./../fields/Document";
+import { Server } from "./../client/Server";
+import { Types } from "./Message";
+import { Utils } from "../Utils";
+import { HtmlField } from "../fields/HtmlField";
+import { WebField } from "../fields/WebField";
+import { AudioField } from "../fields/AudioField";
+import { VideoField } from "../fields/VideoField";
+import { InkField } from "../fields/InkField";
+import { PDFField } from "../fields/PDFField";
+import { TupleField } from "../fields/TupleField";
+import { BooleanField } from "../fields/BooleanField";
+import { HistogramField } from "../client/northstar/dash-fields/HistogramField";
export class ServerUtils {
- public static prepend(extension: string): string { return window.location.origin + extension; }
+ public static prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
public static FromJson(json: any): Field {
- let obj = json
- let data: any = obj.data
- let id: string = obj._id
- let type: Types = obj.type
+ let obj = json;
+ let data: any = obj.data;
+ let id: string = obj._id;
+ let type: Types = obj.type;
if (!(data !== undefined && id && type !== undefined)) {
- console.log("how did you manage to get an object that doesn't have a data or an id?")
+ console.log(
+ "how did you manage to get an object that doesn't have a data or an id?"
+ );
return new TextField("Something to fill the space", Utils.GenerateGuid());
}
switch (type) {
+ case Types.Minimized:
+ return new BooleanField(data, id, false);
case Types.Number:
- return new NumberField(data, id, false)
+ return new NumberField(data, id, false);
case Types.Text:
- return new TextField(data, id, false)
+ return new TextField(data, id, false);
case Types.Html:
- return new HtmlField(data, id, false)
+ return new HtmlField(data, id, false);
case Types.Web:
- return new WebField(new URL(data), id, false)
+ return new WebField(new URL(data), id, false);
case Types.RichText:
- return new RichTextField(data, id, false)
+ return new RichTextField(data, id, false);
case Types.Key:
- return new Key(data, id, false)
+ return new Key(data, id, false);
case Types.Image:
- return new ImageField(new URL(data), id, false)
+ return new ImageField(new URL(data), id, false);
+ case Types.HistogramOp:
+ return HistogramField.FromJson(id, data);
case Types.PDF:
- return new PDFField(new URL(data), id, false)
+ return new PDFField(new URL(data), id, false);
case Types.List:
- return ListField.FromJson(id, data)
+ return ListField.FromJson(id, data);
case Types.Audio:
- return new AudioField(new URL(data), id, false)
+ return new AudioField(new URL(data), id, false);
case Types.Video:
- return new VideoField(new URL(data), id, false)
+ return new VideoField(new URL(data), id, false);
case Types.Tuple:
return new TupleField(data, id, false);
case Types.Ink:
return InkField.FromJson(id, data);
case Types.Document:
- let doc: Document = new Document(id, false)
- let fields: [string, string][] = data as [string, string][]
+ let doc: Document = new Document(id, false);
+ let fields: [string, string][] = data as [string, string][];
fields.forEach(element => {
doc._proxies.set(element[0], element[1]);
});
- return doc
+ return doc;
default:
- throw Error("Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here");
+ throw Error(
+ "Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here"
+ );
}
}
-} \ No newline at end of file
+}
diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx
index 1533b1e62..8e14cf98e 100644
--- a/src/server/authentication/controllers/WorkspacesMenu.tsx
+++ b/src/server/authentication/controllers/WorkspacesMenu.tsx
@@ -9,30 +9,23 @@ import { KeyStore } from '../../../fields/KeyStore';
export interface WorkspaceMenuProps {
active: Document | undefined;
open: (workspace: Document) => void;
- new: (init: boolean) => void;
+ new: () => void;
allWorkspaces: Document[];
+ isShown: () => boolean;
+ toggle: () => void;
}
@observer
export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> {
- static Instance: WorkspacesMenu;
- @observable private workspacesExposed: boolean = false;
-
constructor(props: WorkspaceMenuProps) {
super(props);
- WorkspacesMenu.Instance = this;
this.addNewWorkspace = this.addNewWorkspace.bind(this);
}
@action
addNewWorkspace() {
- this.props.new(false);
- this.toggle();
- }
-
- @action
- toggle() {
- this.workspacesExposed = !this.workspacesExposed;
+ this.props.new();
+ this.props.toggle();
}
render() {
@@ -45,7 +38,7 @@ export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> {
borderRadius: 5,
position: "absolute",
top: 78,
- left: this.workspacesExposed ? 11 : -500,
+ left: this.props.isShown() ? 11 : -500,
background: "white",
border: "black solid 2px",
transition: "all 1s ease",
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 2cef958e8..e365b8dce 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -11,6 +11,7 @@ import * as async from 'async';
import * as nodemailer from 'nodemailer';
import c = require("crypto");
import { RouteStore } from "../../RouteStore";
+import { Utils } from "../../../Utils";
/**
* GET /signup
@@ -54,7 +55,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
const user = new User({
email,
password,
- userDoc: "document here"
+ userDocumentId: Utils.GenerateGuid()
});
User.findOne({ email }, (err, existingUser) => {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index cc433eb73..0ac85b446 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,29 +1,102 @@
import { DashUserModel } from "./user_model";
-import * as request from 'request'
+import * as rp from 'request-promise';
import { RouteStore } from "../../RouteStore";
import { ServerUtils } from "../../ServerUtil";
+import { Server } from "../../../client/Server";
+import { Document } from "../../../fields/Document";
+import { KeyStore } from "../../../fields/KeyStore";
+import { ListField } from "../../../fields/ListField";
+import { Documents } from "../../../client/documents/Documents";
+import { Schema, Attribute, AttributeGroup, Catalog } from "../../../client/northstar/model/idea/idea";
+import { observable, computed, action } from "mobx";
+import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
export class CurrentUserUtils {
private static curr_email: string;
private static curr_id: string;
+ private static user_document: Document;
+ //TODO tfs: these should be temporary...
+ private static mainDocId: string | undefined;
+ @observable private static catalog?: Catalog;
- public static get email() {
- return CurrentUserUtils.curr_email;
+ public static get email(): string {
+ return this.curr_email;
}
- public static get id() {
- return CurrentUserUtils.curr_id;
+ public static get id(): string {
+ return this.curr_id;
}
- public static loadCurrentUser() {
- request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => {
- if (body) {
- let obj = JSON.parse(body);
+ public static get UserDocument(): Document {
+ return this.user_document;
+ }
+
+ public static get MainDocId(): string | undefined {
+ return this.mainDocId;
+ }
+
+ public static set MainDocId(id: string | undefined) {
+ this.mainDocId = id;
+ }
+
+ @computed public static get NorthstarDBCatalog(): Catalog | undefined {
+ return this.catalog;
+ }
+ public static set NorthstarDBCatalog(ctlog: Catalog | undefined) {
+ this.catalog = ctlog;
+ }
+ public static GetNorthstarSchema(name: string): Schema | undefined {
+ return !this.catalog || !this.catalog.schemas ? undefined :
+ ArrayUtil.FirstOrDefault<Schema>(this.catalog.schemas, (s: Schema) => s.displayName === name);
+ }
+ public static GetAllNorthstarColumnAttributes(schema: Schema) {
+ if (!schema || !schema.rootAttributeGroup) {
+ return [];
+ }
+ const recurs = (attrs: Attribute[], g: AttributeGroup) => {
+ if (g.attributes) {
+ attrs.push.apply(attrs, g.attributes);
+ if (g.attributeGroups) {
+ g.attributeGroups.forEach(ng => recurs(attrs, ng));
+ }
+ }
+ };
+ const allAttributes: Attribute[] = new Array<Attribute>();
+ recurs(allAttributes, schema.rootAttributeGroup);
+ return allAttributes;
+ }
+
+ private static createUserDocument(id: string): Document {
+ let doc = new Document(id);
+
+ doc.Set(KeyStore.Workspaces, new ListField<Document>());
+ doc.Set(KeyStore.OptionalRightCollection, Documents.SchemaDocument([], { title: "Pending documents" }))
+ return doc;
+ }
+
+ public static loadCurrentUser(): Promise<any> {
+ let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then((response) => {
+ if (response) {
+ let obj = JSON.parse(response);
CurrentUserUtils.curr_id = obj.id as string;
CurrentUserUtils.curr_email = obj.email as string;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?")
}
});
+ let userDocPromise = rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(id => {
+ if (id) {
+ return Server.GetField(id).then(field => {
+ if (field instanceof Document) {
+ this.user_document = field;
+ } else {
+ this.user_document = this.createUserDocument(id);
+ }
+ })
+ } else {
+ throw new Error("There should be a user id! Why does Dash think there isn't one?")
+ }
+ });
+ return Promise.all([userPromise, userDocPromise]);
}
} \ No newline at end of file
diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts
index 3d4ed6896..81580aad5 100644
--- a/src/server/authentication/models/user_model.ts
+++ b/src/server/authentication/models/user_model.ts
@@ -21,9 +21,7 @@ export type DashUserModel = mongoose.Document & {
passwordResetToken: string | undefined,
passwordResetExpires: Date | undefined,
- allWorkspaceIds: Array<String>,
- activeWorkspaceId: String,
- activeUsersId: String,
+ userDocumentId: string;
profile: {
name: string,
@@ -49,12 +47,7 @@ const userSchema = new mongoose.Schema({
passwordResetToken: String,
passwordResetExpires: Date,
- allWorkspaceIds: {
- type: Array,
- default: []
- },
- activeWorkspaceId: String,
- activeUsersId: String,
+ userDocumentId: String,
facebook: String,
twitter: String,
diff --git a/src/server/database.ts b/src/server/database.ts
index 99b13805e..616251c72 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -1,8 +1,4 @@
-import { action, configure } from 'mobx';
import * as mongodb from 'mongodb';
-import { ObjectID } from 'mongodb';
-import { Transferable } from './Message';
-import { Utils } from '../Utils';
export class Database {
public static Instance = new Database()
@@ -21,7 +17,16 @@ export class Database {
let collection = this.db.collection('documents');
collection.updateOne({ _id: id }, { $set: value }, {
upsert: true
- }, callback);
+ }, (err, res) => {
+ if (err) {
+ console.log(err.message);
+ console.log(err.errmsg);
+ }
+ // if (res) {
+ // console.log(JSON.stringify(res.result));
+ // }
+ callback()
+ });
}
}
@@ -32,11 +37,13 @@ export class Database {
}
}
- public deleteAll(collectionName: string = 'documents') {
- if (this.db) {
- let collection = this.db.collection(collectionName);
- collection.deleteMany({});
- }
+ public deleteAll(collectionName: string = 'documents'): Promise<any> {
+ return new Promise(res => {
+ if (this.db) {
+ let collection = this.db.collection(collectionName);
+ collection.deleteMany({}, res);
+ }
+ })
}
public insert(kvpairs: any) {
diff --git a/src/server/index.ts b/src/server/index.ts
index d1eb6847d..17d7432e0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -156,16 +156,9 @@ addSecureRoute(
addSecureRoute(
Method.GET,
- (user, res) => res.send(user.activeWorkspaceId || ""),
+ (user, res) => res.send(user.userDocumentId || ""),
undefined,
- RouteStore.getActiveWorkspace,
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => res.send(JSON.stringify(user.allWorkspaceIds)),
- undefined,
- RouteStore.getAllWorkspaces
+ RouteStore.getUserDocumentId,
);
addSecureRoute(
@@ -185,28 +178,6 @@ addSecureRoute(
addSecureRoute(
Method.POST,
(user, res, req) => {
- user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => {
- res.sendStatus(err ? 500 : 200);
- });
- },
- undefined,
- RouteStore.setActiveWorkspace
-);
-
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
- user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => {
- res.sendStatus(err ? 500 : 200);
- });
- },
- undefined,
- RouteStore.addWorkspace
-);
-
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
let form = new formidable.IncomingForm()
form.uploadDir = __dirname + "/public/files/"
form.keepExtensions = true
@@ -252,13 +223,11 @@ app.use(RouteStore.corsProxy, (req, res) => {
});
app.get(RouteStore.delete, (req, res) => {
- deleteFields();
- res.redirect(RouteStore.home);
+ deleteFields().then(() => res.redirect(RouteStore.home));
});
app.get(RouteStore.deleteAll, (req, res) => {
- deleteAll();
- res.redirect(RouteStore.home);
+ deleteAll().then(() => res.redirect(RouteStore.home));
});
app.use(wdm(compiler, {
@@ -291,13 +260,15 @@ server.on("connection", function (socket: Socket) {
})
function deleteFields() {
- Database.Instance.deleteAll();
+ return Database.Instance.deleteAll();
}
function deleteAll() {
- Database.Instance.deleteAll();
- Database.Instance.deleteAll('sessions');
- Database.Instance.deleteAll('users');
+ return Database.Instance.deleteAll().then(() => {
+ return Database.Instance.deleteAll('sessions')
+ }).then(() => {
+ return Database.Instance.deleteAll('users')
+ });
}
function barReceived(guid: String) {
diff --git a/src/server/public/files/upload_a6a70d84ebb65febf7900e29f52cc86d.pdf b/src/server/public/files/upload_a6a70d84ebb65febf7900e29f52cc86d.pdf
new file mode 100644
index 000000000..dfd6ab339
--- /dev/null
+++ b/src/server/public/files/upload_a6a70d84ebb65febf7900e29f52cc86d.pdf
Binary files differ
diff --git a/tsconfig.json b/tsconfig.json
index 2b2c69fe1..41db1d0a7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,7 @@
"lib": [
"dom",
"es2015"
- ]
+ ],
},
// "exclude": [
// "node_modules",