aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.vscode/launch.json2
-rw-r--r--.vscode/settings.json4
-rw-r--r--deploy/index.html5
-rw-r--r--deploy/mobile/image.html15
-rw-r--r--deploy/mobile/ink.html13
-rw-r--r--package.json26
-rw-r--r--src/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--src/client/Server.ts35
-rw-r--r--src/client/SocketStub.ts1
-rw-r--r--src/client/documents/Documents.ts116
-rw-r--r--src/client/util/DragManager.ts54
-rw-r--r--src/client/util/RichTextRules.ts43
-rw-r--r--src/client/util/RichTextSchema.tsx23
-rw-r--r--src/client/util/TooltipTextMenu.scss7
-rw-r--r--src/client/util/TooltipTextMenu.tsx38
-rw-r--r--src/client/util/jsx-decl.d.ts1
-rw-r--r--src/client/views/.DS_Storebin6148 -> 8196 bytes
-rw-r--r--src/client/views/ContextMenu.scss77
-rw-r--r--src/client/views/ContextMenu.tsx16
-rw-r--r--src/client/views/DocumentDecorations.scss85
-rw-r--r--src/client/views/DocumentDecorations.tsx28
-rw-r--r--src/client/views/EditableView.scss6
-rw-r--r--src/client/views/EditableView.tsx6
-rw-r--r--src/client/views/InkingCanvas.scss131
-rw-r--r--src/client/views/InkingCanvas.tsx30
-rw-r--r--src/client/views/InkingControl.tsx75
-rw-r--r--src/client/views/InkingStroke.tsx2
-rw-r--r--src/client/views/Main.scss143
-rw-r--r--src/client/views/Main.tsx364
-rw-r--r--src/client/views/_global_variables.scss17
-rw-r--r--src/client/views/_nodeModuleOverrides.scss23
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx8
-rw-r--r--src/client/views/collections/CollectionFreeFormView.scss108
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx367
-rw-r--r--src/client/views/collections/CollectionPDFView.scss27
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx28
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss108
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx230
-rw-r--r--src/client/views/collections/CollectionTreeView.scss32
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx30
-rw-r--r--src/client/views/collections/CollectionVideoView.scss40
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx118
-rw-r--r--src/client/views/collections/CollectionView.tsx18
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx78
-rw-r--r--src/client/views/collections/MarqueeView.scss8
-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/nodes/AudioBox.tsx12
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx57
-rw-r--r--src/client/views/nodes/DocumentView.scss44
-rw-r--r--src/client/views/nodes/DocumentView.tsx154
-rw-r--r--src/client/views/nodes/FieldTextBox.scss14
-rw-r--r--src/client/views/nodes/FieldView.tsx19
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss64
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx14
-rw-r--r--src/client/views/nodes/ImageBox.scss25
-rw-r--r--src/client/views/nodes/ImageBox.tsx19
-rw-r--r--src/client/views/nodes/KeyValueBox.scss46
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx69
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx31
-rw-r--r--src/client/views/nodes/LinkBox.scss42
-rw-r--r--src/client/views/nodes/LinkBox.tsx22
-rw-r--r--src/client/views/nodes/LinkEditor.scss22
-rw-r--r--src/client/views/nodes/LinkMenu.scss1
-rw-r--r--src/client/views/nodes/LinkMenu.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.scss2
-rw-r--r--src/client/views/nodes/PDFBox.tsx9
-rw-r--r--src/client/views/nodes/VideoBox.scss2
-rw-r--r--src/client/views/nodes/VideoBox.tsx64
-rw-r--r--src/debug/Test.tsx43
-rw-r--r--src/debug/Viewer.tsx4
-rw-r--r--src/fields/AudioField.ts8
-rw-r--r--src/fields/Document.ts62
-rw-r--r--src/fields/HtmlField.ts2
-rw-r--r--src/fields/ImageField.ts4
-rw-r--r--src/fields/KVPField.ts30
-rw-r--r--src/fields/KeyStore.ts6
-rw-r--r--src/fields/ListField.ts50
-rw-r--r--src/fields/PDFField.ts4
-rw-r--r--src/fields/TupleField.ts59
-rw-r--r--src/fields/VideoField.ts4
-rw-r--r--src/fields/WebField.ts4
-rw-r--r--src/mobile/ImageUpload.scss13
-rw-r--r--src/mobile/ImageUpload.tsx82
-rw-r--r--src/mobile/InkControls.tsx (renamed from src/fields/KVPField)0
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/RouteStore.ts33
-rw-r--r--src/server/ServerUtil.ts9
-rw-r--r--src/server/authentication/config/passport.ts7
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.css3
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.tsx96
-rw-r--r--src/server/authentication/controllers/user.ts107
-rw-r--r--src/server/authentication/controllers/user_controller.ts263
-rw-r--r--src/server/authentication/models/current_user_utils.ts29
-rw-r--r--src/server/authentication/models/user_model.ts (renamed from src/server/authentication/models/User.ts)24
-rw-r--r--src/server/database.ts10
-rw-r--r--src/server/index.ts261
-rw-r--r--src/server/public/files/upload_e72669595eae4384a2a32196496f4f05.pdfbin0 -> 548616 bytes
-rw-r--r--src/typings/index.d.ts604
-rw-r--r--views/forgot.pug22
-rw-r--r--views/layout.pug5
-rw-r--r--views/login.pug28
-rw-r--r--views/reset.pug22
-rw-r--r--views/resources/dashlogo.pngbin0 -> 7169 bytes
-rw-r--r--views/signup.pug44
-rw-r--r--views/stylesheets/authentication.css142
-rw-r--r--webpack.config.js2
109 files changed, 4192 insertions, 1486 deletions
diff --git a/.gitignore b/.gitignore
index 7b9483f69..a499c39a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
node_modules
+package-lock.json
dist/
.DS_Store
-src/server/public/files/*
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 074f9ddf0..fb91a1080 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,7 +10,7 @@
"name": "Launch Chrome against localhost",
"sourceMaps": true,
"breakOnLoad": true,
- "url": "http://localhost:1050",
+ "url": "http://localhost:1050/login",
"webRoot": "${workspaceFolder}",
},
{
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 623dae755..081b05b38 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,5 @@
},
"editor.formatOnSave": true,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
- "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
-} \ No newline at end of file
+ "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true
+}
diff --git a/deploy/index.html b/deploy/index.html
index 94699126c..dbfc6dee0 100644
--- a/deploy/index.html
+++ b/deploy/index.html
@@ -2,15 +2,18 @@
<head>
<title>Dash Web</title>
+<<<<<<< HEAD
<link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+=======
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
<script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script>
</head>
<body>
<div id="root"></div>
- <script src="./bundle.js"></script>
+ <script src="/bundle.js"></script>
</body>
</html> \ No newline at end of file
diff --git a/deploy/mobile/image.html b/deploy/mobile/image.html
new file mode 100644
index 000000000..6424d2a60
--- /dev/null
+++ b/deploy/mobile/image.html
@@ -0,0 +1,15 @@
+<html>
+
+<head>
+ <title>Test view</title>
+ <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+</head>
+
+<body>
+ <div id="root">
+ <p>Capture Image: <input type="file" accept="image/*" id="capture">
+ </div>
+ <script src="../imageUpload.js"></script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/deploy/mobile/ink.html b/deploy/mobile/ink.html
new file mode 100644
index 000000000..938d85794
--- /dev/null
+++ b/deploy/mobile/ink.html
@@ -0,0 +1,13 @@
+<html>
+
+<head>
+ <title>Test view</title>
+ <link href="https://fonts.googleapis.com/css?family=Fjalla+One|Hind+Siliguri:300" rel="stylesheet">
+</head>
+
+<body>
+ <div id="root"></div>
+ <script src="../inkControls.js"></script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/package.json b/package.json
index 3a1928632..640f16437 100644
--- a/package.json
+++ b/package.json
@@ -38,10 +38,19 @@
"@fortawesome/free-solid-svg-icons": "^5.7.2",
"@fortawesome/react-fontawesome": "^0.1.4",
"@hig/flyout": "^1.0.3",
+<<<<<<< HEAD
"@trendmicro/react-dropdown": "^1.3.0",
+=======
+ "@types/async": "^2.4.1",
+ "@hig/theme-context": "^2.1.3",
+ "@hig/theme-data": "^2.3.3",
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
"@types/bcrypt-nodejs": "0.0.30",
"@types/bluebird": "^3.5.25",
"@types/body-parser": "^1.17.0",
+ "@types/connect-flash": "0.0.34",
+ "@types/cookie-parser": "^1.4.1",
+ "@types/cookie-session": "^2.0.36",
"@types/express": "^4.16.1",
"@types/express-flash": "0.0.0",
"@types/express-session": "^1.15.12",
@@ -50,13 +59,16 @@
"@types/jquery": "^3.3.29",
"@types/jsonwebtoken": "^8.3.2",
"@types/lodash": "^4.14.121",
+ "@types/mobile-detect": "^1.3.4",
"@types/mongodb": "^3.1.22",
"@types/mongoose": "^5.3.21",
"@types/node": "^10.12.30",
+ "@types/nodemailer": "^4.6.6",
"@types/passport": "^1.0.0",
"@types/passport-local": "^1.0.33",
"@types/prosemirror-commands": "^1.0.1",
"@types/prosemirror-history": "^1.0.1",
+ "@types/prosemirror-inputrules": "^1.0.2",
"@types/prosemirror-keymap": "^1.0.1",
"@types/prosemirror-model": "^1.7.0",
"@types/prosemirror-schema-basic": "^1.0.1",
@@ -75,12 +87,20 @@
"@types/typescript": "^2.0.0",
"@types/uuid": "^3.4.4",
"@types/webpack": "^4.4.25",
+ "async": "^2.6.2",
"babel-runtime": "^6.26.0",
"bcrypt-nodejs": "0.0.3",
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
+<<<<<<< HEAD
"bootstrap": "^4.3.1",
+=======
+ "connect-flash": "^0.1.1",
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
"connect-mongo": "^2.0.3",
+ "cookie-parser": "^1.4.4",
+ "cookie-session": "^2.0.0-beta.3",
+ "crypto-browserify": "^3.11.0",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.15.6",
@@ -94,12 +114,14 @@
"jsonwebtoken": "^8.5.0",
"jsx-to-string": "^1.4.0",
"lodash": "^4.17.11",
+ "mobile-detect": "^1.4.3",
"mobx": "^5.9.0",
"mobx-react": "^5.3.5",
"mobx-react-devtools": "^6.1.1",
"mongodb": "^3.1.13",
"mongoose": "^5.4.18",
"node-sass": "^4.11.0",
+ "nodemailer": "^5.1.1",
"nodemon": "^1.18.10",
"normalize.css": "^8.0.1",
"npm": "^6.9.0",
@@ -137,7 +159,9 @@
"request": "^2.88.0",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
+ "socketio": "^1.0.0",
"url-loader": "^1.1.2",
- "uuid": "^3.3.2"
+ "uuid": "^3.3.2",
+ "xoauth2": "^1.2.0"
}
}
diff --git a/src/.DS_Store b/src/.DS_Store
index f20f36d63..90213270f 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/client/Server.ts b/src/client/Server.ts
index f0cf0bb9b..3fb1ae878 100644
--- a/src/client/Server.ts
+++ b/src/client/Server.ts
@@ -40,8 +40,8 @@ export class Server {
return this.ClientFieldsCached.get(fieldid);
}, (field, reaction) => {
if (field !== "<Waiting>") {
- callback(field)
reaction.dispose()
+ callback(field)
}
})
}
@@ -49,14 +49,39 @@ export class Server {
}
public static GetFields(fieldIds: FieldId[], callback: (fields: { [id: string]: Field }) => any) {
- SocketStub.SEND_FIELDS_REQUEST(fieldIds, (fields) => {
+ 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.has(field.Id)) {
+ if (!(this.ClientFieldsCached.get(field.Id) instanceof Field)) {
this.ClientFieldsCached.set(field.Id, field)
}
}
- callback(fields)
+ 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;
+ }
+ callback({ ...fields, ...existingFields })
+ }
+ }, { fireImmediately: true })
});
}
@@ -121,4 +146,4 @@ export class Server {
}
Server.Socket.on(MessageStore.Foo.Message, Server.connected);
-Server.Socket.on(MessageStore.SetField.Message, Server.updateField); \ No newline at end of file
+Server.Socket.on(MessageStore.SetField.Message, Server.updateField);
diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts
index 18df4ca0a..a0b89b7c9 100644
--- a/src/client/SocketStub.ts
+++ b/src/client/SocketStub.ts
@@ -7,6 +7,7 @@ import { Utils } from "../Utils";
import { Server } from "./Server";
import { ServerUtils } from "../server/ServerUtil";
+//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 {
static FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3d9f4a0cb..fabdaad17 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,32 +1,32 @@
+import { AudioField } from "../../fields/AudioField";
import { Document } from "../../fields/Document";
-import { Server } from "../Server";
+import { Field } from "../../fields/Field";
+import { HtmlField } from "../../fields/HtmlField";
+import { ImageField } from "../../fields/ImageField";
+import { InkField, StrokeData } from "../../fields/InkField";
+import { Key } from "../../fields/Key";
import { KeyStore } from "../../fields/KeyStore";
-import { TextField } from "../../fields/TextField";
-import { NumberField } from "../../fields/NumberField";
import { ListField } from "../../fields/ListField";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { ImageField } from "../../fields/ImageField";
-import { ImageBox } from "../views/nodes/ImageBox";
+import { PDFField } from "../../fields/PDFField";
+import { TextField } from "../../fields/TextField";
+import { VideoField } from "../../fields/VideoField";
import { WebField } from "../../fields/WebField";
-import { WebBox } from "../views/nodes/WebBox";
+import { Server } from "../Server";
+import { CollectionPDFView } from "../views/collections/CollectionPDFView";
+import { CollectionVideoView } from "../views/collections/CollectionVideoView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
-import { HtmlField } from "../../fields/HtmlField";
-import { Key } from "../../fields/Key"
-import { Field } from "../../fields/Field";
-import { KeyValueBox } from "../views/nodes/KeyValueBox"
-import { KVPField } from "../../fields/KVPField";
-import { VideoField } from "../../fields/VideoField"
-import { VideoBox } from "../views/nodes/VideoBox";
-import { AudioField } from "../../fields/AudioField";
import { AudioBox } from "../views/nodes/AudioBox";
-import { PDFField } from "../../fields/PDFField";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { ImageBox } from "../views/nodes/ImageBox";
+import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { PDFBox } from "../views/nodes/PDFBox";
-import { CollectionPDFView } from "../views/collections/CollectionPDFView";
-import { RichTextField } from "../../fields/RichTextField";
+import { VideoBox } from "../views/nodes/VideoBox";
+import { WebBox } from "../views/nodes/WebBox";
export interface DocumentOptions {
x?: number;
y?: number;
+ ink?: Map<string, StrokeData>;
width?: number;
height?: number;
nativeWidth?: number;
@@ -39,6 +39,7 @@ export interface DocumentOptions {
layout?: string;
layoutKeys?: Key[];
viewType?: number;
+ backgroundColor?: string;
}
export namespace Documents {
@@ -59,33 +60,40 @@ export namespace Documents {
const videoProtoId = "videoProto"
const audioProtoId = "audioProto";
- export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) {
- Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => {
+ export function initProtos(callback: () => void) {
+ Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => {
collProto = fields[collProtoId] as Document;
imageProto = fields[imageProtoId] as Document;
textProto = fields[textProtoId] as Document;
webProto = fields[webProtoId] as Document;
kvpProto = fields[kvpProtoId] as Document;
- callback(fields[mainDocId] as Document)
+ callback();
});
}
function assignOptions(doc: Document, options: DocumentOptions): Document {
- if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); }
- if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); }
- if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); }
- if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }
if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); }
if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); }
if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); }
- if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); }
- if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); }
if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); }
if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); }
if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); }
+ if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); }
+ if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); }
if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); }
if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); }
return doc;
}
+
+ function assignToDelegate(doc: Document, options: DocumentOptions): Document {
+ if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); }
+ if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); }
+ if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); }
+ if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }
+ if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); }
+ if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); }
+ return doc
+ }
+
function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document {
return assignOptions(new Document(protoId), { ...options, title: title, layout: layout });
}
@@ -128,7 +136,7 @@ export namespace Documents {
function GetCollectionPrototype(): Document {
return collProto ? collProto :
collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"),
- { panx: 0, pany: 0, scale: 1, layoutKeys: [KeyStore.Data] });
+ { panx: 0, pany: 0, scale: 1, width: 500, height: 500, layoutKeys: [KeyStore.Data] });
}
function GetKVPPrototype(): Document {
@@ -137,9 +145,13 @@ export namespace Documents {
{ x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] })
}
function GetVideoPrototype(): Document {
- return videoProto ? videoProto :
- videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", VideoBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] })
+ if (!videoProto) {
+ videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("AnnotationsKey"),
+ { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
+ videoProto.SetNumber(KeyStore.CurPage, 0);
+ videoProto.SetText(KeyStore.BackgroundLayout, VideoBox.LayoutString());
+ }
+ return videoProto;
}
function GetAudioPrototype(): Document {
return audioProto ? audioProto :
@@ -149,8 +161,7 @@ export namespace Documents {
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
- [new URL(url), ImageField]);
+ return assignToDelegate(SetInstanceOptions(GetImagePrototype(), options, [new URL(url), ImageField]).MakeDelegate(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
// let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
// [new URL(url), ImageField]);
// doc.SetText(KeyStore.Caption, "my caption...");
@@ -159,34 +170,38 @@ export namespace Documents {
// return doc;
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetVideoPrototype(), options, [new URL(url), VideoField]);
+ return assignToDelegate(SetInstanceOptions(GetVideoPrototype(), options, [new URL(url), VideoField]), options);
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]);
+ return assignToDelegate(SetInstanceOptions(GetAudioPrototype(), options, [new URL(url), AudioField]), options);
}
+
export function TextDocument(options: DocumentOptions = {}) {
- return SetInstanceOptions(GetTextPrototype(), options, ["", RichTextField]);
+ return assignToDelegate(SetInstanceOptions(GetTextPrototype(), options, ["", TextField]).MakeDelegate(), options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetPdfPrototype(), options, [new URL(url), PDFField]);
+ return assignToDelegate(SetInstanceOptions(GetPdfPrototype(), options, [new URL(url), PDFField]).MakeDelegate(), options);
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetWebPrototype(), options, [new URL(url), WebField]);
+ return assignToDelegate(SetInstanceOptions(GetWebPrototype(), options, [new URL(url), WebField]).MakeDelegate(), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
- return SetInstanceOptions(GetWebPrototype(), options, [html, HtmlField]);
+ return assignToDelegate(SetInstanceOptions(GetWebPrototype(), options, [html, HtmlField]).MakeDelegate(), options);
}
export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) {
- return SetInstanceOptions(GetKVPPrototype(), options, document, id)
+ return assignToDelegate(SetInstanceOptions(GetKVPPrototype(), options, document, id), options)
}
- export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id)
+ export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string, makePrototype: boolean = true) {
+ if (!makePrototype) {
+ return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id)
+ }
+ return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id).MakeDelegate(), options)
}
export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id)
+ return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options)
}
export function DockDocument(config: string, options: DocumentOptions, id?: string) {
- return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id)
+ return assignToDelegate(SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options)
}
// example of custom display string for an image that shows a caption.
@@ -205,4 +220,17 @@ export namespace Documents {
+ FormattedTextBox.LayoutString(fieldName + "Key") +
`</div>
</div>` };
+
+ function Caption() {
+ return (`
+<div>
+ <div style="margin:auto; height:85%; width:85%;">
+ {layout}
+ </div>
+ <div style="height:15%; width:100%; position:absolute">
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ </div>
+</div>
+ `)
+ }
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4a61220a5..753115f76 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -2,18 +2,21 @@ import { DocumentDecorations } from "../views/DocumentDecorations";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { Document } from "../../fields/Document"
import { action } from "mobx";
-import { DocumentView } from "../views/nodes/DocumentView";
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) {
+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);
- DragManager.StartDrag(_reference.current!, { document: docFunc() });
+ var dragData = new DragManager.DocumentDragData(docFunc());
+ dragData.removeDocument = removeFunc;
+ DragManager.StartDocumentDrag(_reference.current!, dragData);
});
let onRowUp = action((e: PointerEvent): void => {
document.removeEventListener("pointermove", onRowMove);
@@ -70,11 +73,12 @@ export namespace DragManager {
export interface DropOptions {
handlers: DropHandlers;
}
-
export class DropEvent {
constructor(readonly x: number, readonly y: number, readonly data: { [id: string]: any }) { }
}
+
+
export interface DropHandlers {
drop: (e: Event, de: DropEvent) => void;
}
@@ -96,7 +100,34 @@ export namespace DragManager {
};
}
- export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions) {
+ export class DocumentDragData {
+ constructor(dragDoc: Document) {
+ this.draggedDocument = dragDoc;
+ this.droppedDocument = dragDoc;
+ }
+ draggedDocument: Document;
+ droppedDocument: 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 class LinkDragData {
+ constructor(linkSourceDoc: DocumentView) {
+ this.linkSourceDocumentView = linkSourceDoc;
+ }
+ linkSourceDocumentView: DocumentView;
+ [id: string]: any;
+ }
+ 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) {
if (!dragDiv) {
dragDiv = document.createElement("div");
dragDiv.className = "dragManager-dragDiv"
@@ -122,9 +153,7 @@ export namespace DragManager {
// 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 docView: DocumentView = dragData["documentView"];
- const doc: Document = docView ? docView.props.Document : dragData["document"];
-
+ const doc: Document = dragData["draggedDocument"];
if (doc) {
var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField);
@@ -138,7 +167,6 @@ export namespace DragManager {
}
}
-
dragDiv.appendChild(dragElement);
let hideSource = false;
@@ -175,13 +203,13 @@ export namespace DragManager {
}
const upHandler = (e: PointerEvent) => {
abortDrag();
- FinishDrag(ele, e, dragData, options);
+ FinishDrag(ele, 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) {
+ 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);
@@ -191,6 +219,9 @@ export namespace DragManager {
if (!target) {
return;
}
+ if (finishDrag)
+ finishDrag(dragData);
+
target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
bubbles: true,
detail: {
@@ -199,6 +230,7 @@ export namespace DragManager {
data: dragData
}
}));
+
if (options) {
options.handlers.dragComplete({});
}
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
new file mode 100644
index 000000000..3b8396510
--- /dev/null
+++ b/src/client/util/RichTextRules.ts
@@ -0,0 +1,43 @@
+import {
+ inputRules,
+ wrappingInputRule,
+ textblockTypeInputRule,
+ smartQuotes,
+ emDash,
+ ellipsis
+} from "prosemirror-inputrules";
+import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model";
+
+import { schema } from "./RichTextSchema";
+
+export const inpRules = {
+ rules: [
+ ...smartQuotes,
+ ellipsis,
+ emDash,
+
+ // > blockquote
+ wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+
+ // 1. ordered list
+ wrappingInputRule(
+ /^(\d+)\.\s$/,
+ schema.nodes.ordered_list,
+ match => ({ order: +match[1] }),
+ (match, node) => node.childCount + node.attrs.order === +match[1]
+ ),
+
+ // * bullet list
+ wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
+
+ // ``` code block
+ textblockTypeInputRule(/^```$/, schema.nodes.code_block),
+
+ // # heading
+ textblockTypeInputRule(
+ new RegExp("^(#{1,6})\\s$"),
+ schema.nodes.heading,
+ match => ({ level: match[1].length })
+ )
+ ]
+};
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index abf448c9f..2a3c1da6e 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,12 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray } from "prosemirror-model"
+import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model"
import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands'
import { redo, undo } from 'prosemirror-history'
-import { orderedList, bulletList, listItem } from 'prosemirror-schema-list'
+import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list'
+import { EditorState, Transaction, NodeSelection, } from "prosemirror-state";
+import { EditorView, } from "prosemirror-view";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]
+
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes: { [index: string]: NodeSpec } = {
@@ -113,12 +116,22 @@ export const nodes: { [index: string]: NodeSpec } = {
content: 'list_item+',
group: 'block'
},
+ //this doesn't currently work for some reason
bullet_list: {
+ ...bulletList,
content: 'list_item+',
group: 'block',
- parseDOM: [{ tag: "ul" }, { style: "list-style-type=disc;" }],
- toDOM() { return ulDOM }
- },
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ // toDOM() { return ulDOM }
+ },
+ //bullet_list: {
+ // content: 'list_item+',
+ // group: 'block',
+ //active: blockActive(schema.nodes.bullet_list),
+ //enable: wrapInList(schema.nodes.bullet_list),
+ //run: wrapInList(schema.nodes.bullet_list),
+ //select: state => true,
+ // },
list_item: {
...listItem,
content: 'paragraph block*'
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
index fa43f5326..ea580d104 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/util/TooltipTextMenu.scss
@@ -1,8 +1,9 @@
+@import "../views/global_variables";
.tooltipMenu {
position: absolute;
z-index: 20;
- background: rgb(19, 18, 18);
+ background: $dark-color;
border: 1px solid silver;
border-radius: 4px;
padding: 2px 10px;
@@ -31,14 +32,14 @@
bottom: -4.5px;
border: 5px solid transparent;
border-bottom-width: 0;
- border-top-color: black;
+ border-top-color: $dark-color;
}
.menuicon {
display: inline-block;
border-right: 1px solid rgba(0, 0, 0, 0.2);
//color: rgb(19, 18, 18);
- color: white;
+ color: $light-color;
line-height: 1;
padding: 0px 2px;
margin: 1px;
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 3b87fe9de..2a613ba8b 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -2,10 +2,10 @@ import { action, IReactionDisposer, reaction } from "mobx";
import { baseKeymap } from "prosemirror-commands";
import { history, redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-const { exampleSetup } = require("prosemirror-example-setup")
-import { EditorState, Transaction, } from "prosemirror-state";
+import { EditorState, Transaction, NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "./RichTextSchema";
+import { Schema, NodeType } from "prosemirror-model"
import React = require("react")
import "./TooltipTextMenu.scss";
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
@@ -16,7 +16,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
-
+//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
private tooltip: HTMLElement;
@@ -39,7 +39,8 @@ export class TooltipTextMenu {
{ command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") },
{ command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") },
{ command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") },
- { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }
+ //this doesn't work currently - look into notion of active block
+ { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
]
items.forEach(({ dom }) => this.tooltip.appendChild(dom));
@@ -49,7 +50,9 @@ export class TooltipTextMenu {
view.focus();
items.forEach(({ command, dom }) => {
if (dom.contains(e.srcElement)) {
- command(view.state, view.dispatch, view)
+ let active = command(view.state, view.dispatch, view);
+ //uncomment this if we want the bullet button to disappear if current selection is bulleted
+ // dom.style.display = active ? "" : "none"
}
})
})
@@ -66,13 +69,25 @@ export class TooltipTextMenu {
return span;
}
- blockActive(view: EditorView) {
- const { $from, to } = view.state.selection
+ //adapted this method - use it to check if block has a tag (ie bulleting)
+ blockActive(type: NodeType<Schema<string, string>>, state: EditorState) {
+ let attrs = {};
+
+ if (state.selection instanceof NodeSelection) {
+ const sel: NodeSelection = state.selection;
+ let $from = sel.$from;
+ let to = sel.to;
+ let node = sel.node;
+
+ if (node) {
+ return node.hasMarkup(type, attrs);
+ }
- return to <= $from.end() && $from.parent.hasMarkup(schema.nodes.bulletList);
+ return to <= $from.end() && $from.parent.hasMarkup(type, attrs);
+ }
}
- //this doesn't currently work but hopefully will soon
+ //this doesn't currently work but could be used to use icons for buttons
unorderedListIcon(): HTMLSpanElement {
let span = document.createElement("span");
let icon = document.createElement("FontAwesomeIcon");
@@ -105,8 +120,6 @@ export class TooltipTextMenu {
// Otherwise, reposition it and update its content
this.tooltip.style.display = ""
let { from, to } = state.selection
- // These are in screen coordinates
- //check this - tranform
let start = view.coordsAtPos(from), end = view.coordsAtPos(to)
// The box in which the tooltip is positioned, to use as base
let box = this.tooltip.offsetParent!.getBoundingClientRect()
@@ -116,8 +129,9 @@ export class TooltipTextMenu {
this.tooltip.style.left = (left - box.left) + "px"
let width = Math.abs(start.left - end.left) / 2;
let mid = Math.min(start.left, end.left) + width;
+
//THIS WIDTH IS 15 * NUMBER OF ICONS + 15
- this.tooltip.style.width = 120 + "px";
+ this.tooltip.style.width = 122 + "px";
this.tooltip.style.bottom = (box.bottom - start.top) + "px";
}
diff --git a/src/client/util/jsx-decl.d.ts b/src/client/util/jsx-decl.d.ts
new file mode 100644
index 000000000..532f06178
--- /dev/null
+++ b/src/client/util/jsx-decl.d.ts
@@ -0,0 +1 @@
+declare module 'react-jsx-parser';
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 6bd614c8b..0964d5ff3 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 41994ef79..f6830d9cd 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -1,42 +1,55 @@
+@import "global_variables";
.contextMenu-cont {
- position: absolute;
- display: flex;
- z-index: 1000;
- box-shadow: #AAAAAA .2vw .2vw .4vw;
- flex-direction: column;
+ position: absolute;
+ display: flex;
+ z-index: 1000;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+}
+
+.contextMenu-item:first-child {
+ background: $intermediate-color;
+ color: $light-color;
+}
+
+.contextMenu-item:first-child::placeholder {
+ color: $light-color;
+}
+
+.contextMenu-item:first-child:hover {
+ background: $intermediate-color;
+ color: $light-color;
}
.contextMenu-item {
- width: auto;
- height: auto;
- background: #F0F8FF;
- display: flex;
- justify-content: left;
- align-items: center;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- transition: all .1s;
- border-width: .11px;
- border-style: none;
- border-color: rgb(187, 186, 186);
- border-bottom-style: solid;
- padding: 10px;
- white-space: nowrap;
- // font-size: 1.5vw;
- font-size: 12px;
+ width: auto;
+ height: auto;
+ background: $light-color-secondary;
+ display: flex;
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all 0.1s;
+ border-width: 0.11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ white-space: nowrap;
+ font-size: 13px;
}
.contextMenu-item:hover {
- transition: all .1s;
- background: #B0E0E6;
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
}
.contextMenu-description {
- // font-size: 1.5vw;
- text-align: left;
- // width: 8vw;
-} \ No newline at end of file
+ text-align: left;
+ width: 8vw;
+}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 12352c667..9109b56bb 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -13,6 +13,8 @@ export class ContextMenu extends React.Component {
@observable private _pageY: number = 0;
@observable private _display: string = "none";
@observable private _searchString: string = "";
+ // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be
+ @observable private _yRelativeToTop: boolean = true;
private ref: React.RefObject<HTMLDivElement>;
@@ -59,8 +61,13 @@ export class ContextMenu extends React.Component {
intersects = (x: number, y: number): boolean => {
if (this.ref.current && this._display !== "none") {
- if (x >= this._pageX && x <= this._pageX + this.ref.current.getBoundingClientRect().width) {
- if (y >= this._pageY && y <= this._pageY + this.ref.current.getBoundingClientRect().height) {
+ let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height };
+
+ let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) };
+ let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY };
+
+ if (x >= upperLeft.x && x <= bottomRight.x) {
+ if (y >= upperLeft.y && y <= bottomRight.y) {
return true;
}
}
@@ -69,9 +76,12 @@ export class ContextMenu extends React.Component {
}
render() {
+ let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
+ { left: this._pageX, bottom: this._pageY, display: this._display };
+
return (
- <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}>
+ <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;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index fb9091dfc..11595aa01 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,17 +1,18 @@
+@import "global_variables";
#documentDecorations-container {
position: absolute;
display: grid;
z-index: 1000;
- grid-template-rows: 20px 1fr 20px 0px;
- grid-template-columns: 20px 1fr 20px;
+ grid-template-rows: 8px 1fr 8px 30px;
+ grid-template-columns: 8px 1fr 8px;
pointer-events: none;
#documentDecorations-centerCont {
background: none;
}
.documentDecorations-resizer {
pointer-events: auto;
- background: lightblue;
- opacity: 0.4;
+ background: $alt-accent;
+ opacity: 0.8;
}
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
@@ -29,23 +30,89 @@
#documentDecorations-rightResizer {
cursor: ew-resize;
}
-
}
+
+// position: absolute;
+// display: grid;
+// z-index: 1000;
+// grid-template-rows: 20px 1fr 20px 0px;
+// grid-template-columns: 20px 1fr 20px;
+// pointer-events: none;
+// #documentDecorations-centerCont {
+// background: none;
+// }
+// .documentDecorations-resizer {
+// pointer-events: auto;
+// background: lightblue;
+// opacity: 0.4;
+// }
+// #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;
+// }
+// }
+.linkFlyout {
+ grid-column: 1/4
+}
+
+.linkButton-empty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.linkButton-nonempty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
.linkButton-empty {
height: 20px;
width: 20px;
margin-top: 10px;
border-radius: 50%;
- opacity: 0.6;
+ opacity: 0.9;
pointer-events: auto;
- background-color: #2B6091;
+ background-color: $dark-color;
+ color: $light-color;
+ 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-nonempty {
height: 20px;
width: 20px;
margin-top: 10px;
border-radius: 50%;
- opacity: 0.6;
+ opacity: 0.9;
pointer-events: auto;
- background-color: rgb(35, 165, 42);
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 1aa6d1936..47098c3b5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -18,6 +18,8 @@ export class DocumentDecorations extends React.Component {
static Instance: DocumentDecorations
private _resizer = ""
private _isPointerDown = false;
+
+ private _resizeBorderWidth = 16;
private _linkButton = React.createRef<HTMLDivElement>();
@observable private _hidden = false;
@@ -75,7 +77,6 @@ export class DocumentDecorations extends React.Component {
}
onLinkButtonUp = (e: PointerEvent): void => {
- console.log("up");
document.removeEventListener("pointermove", this.onLinkButtonMoved)
document.removeEventListener("pointerup", this.onLinkButtonUp)
e.stopPropagation();
@@ -83,19 +84,17 @@ export class DocumentDecorations extends React.Component {
onLinkButtonMoved = (e: PointerEvent): void => {
- console.log("moved");
- let dragData: { [id: string]: any } = {};
- dragData["linkSourceDoc"] = SelectionManager.SelectedDocuments()[0];
if (this._linkButton.current != null) {
- DragManager.StartDrag(this._linkButton.current, dragData, {
+ 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("pointermove", this.onLinkButtonMoved)
- document.removeEventListener("pointerup", this.onLinkButtonUp)
e.stopPropagation();
}
@@ -206,21 +205,24 @@ export class DocumentDecorations extends React.Component {
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
+ let linkToSize = selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length;
+ let linkFromSize = selFirst.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []).length;
+ let linkCount = linkToSize + linkFromSize;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
content={
<LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}>
</LinkMenu>
}>
- <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} ref={this._linkButton} />
+ <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={{
- width: (bounds.r - bounds.x + 40) + "px",
- height: (bounds.b - bounds.y + 40) + "px",
- left: bounds.x - 20,
- top: bounds.y - 20,
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2,
}}>
<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>
@@ -232,7 +234,7 @@ 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>
- {linkButton}
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
</div >
)
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
new file mode 100644
index 000000000..be3c5069a
--- /dev/null
+++ b/src/client/views/EditableView.scss
@@ -0,0 +1,6 @@
+.editableView-container-editing {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ hyphens: auto;
+ max-width: 300px;
+} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 84b1b91c3..98a6ed1ba 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,6 +1,7 @@
import React = require('react')
import { observer } from 'mobx-react';
import { observable, action } from 'mobx';
+import "./EditableView.scss"
export interface EditableProps {
/**
@@ -20,6 +21,7 @@ export interface EditableProps {
*/
contents: any;
height: number
+ display?: string;
}
/**
@@ -46,10 +48,10 @@ export class EditableView extends React.Component<EditableProps> {
render() {
if (this.editing) {
return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
- style={{ display: "inline" }}></input>
+ style={{ display: this.props.display }}></input>
} else {
return (
- <div className="editableView-container-editing" style={{ display: "inline", height: "100%", maxHeight: `${this.props.height}` }}
+ <div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={action(() => this.editing = true)}>
{this.props.contents}
</div>
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index f654b194b..e79b146b9 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,8 +1,10 @@
+@import "global_variables";
.inking-canvas {
- position: fixed;
+ position: absolute;
top: -50000px;
left: -50000px; // z-index: 99; //overlays ink on top of everything
svg {
+ position:absolute;
width: 100000px;
height: 100000px;
.highlight {
@@ -13,20 +15,135 @@
.inking-control {
position: absolute;
- right: 0;
- bottom: 75px;
- text-align: right;
+ 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-top: 12px;
+ 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 {
- width: 85%;
+ 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 0d87c1239..d7b8bf3c3 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,4 +1,5 @@
import { observer } from "mobx-react";
+import { observable } from "mobx";
import { action, computed } from "mobx";
import { InkingControl } from "./InkingControl";
import React = require("react");
@@ -6,14 +7,10 @@ import { Transform } from "../util/Transform";
import { Document } from "../../fields/Document";
import { KeyStore } from "../../fields/KeyStore";
import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { JsxArgs } from "./nodes/DocumentView";
import { InkingStroke } from "./InkingStroke";
import "./InkingCanvas.scss"
-import { CollectionDockingView } from "./collections/CollectionDockingView";
import { Utils } from "../../Utils";
import { FieldWaiting } from "../../fields/Field";
-import { getMapLikeKeys } from "mobx/lib/internal";
-
interface InkCanvasProps {
getScreenTransform: () => Transform;
@@ -22,7 +19,16 @@ interface InkCanvasProps {
@observer
export class InkingCanvas extends React.Component<InkCanvasProps> {
-
+ static InkOffset: number = 50000;
+ 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
+ }
private _isDrawing: boolean = false;
private _idGenerator: string = "";
@@ -40,7 +46,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
set inkData(value: StrokeMap) {
- this.props.Document.SetData(KeyStore.Ink, value, InkField);
+ this.props.Document.SetOnPrototype(KeyStore.Ink, new InkField(value));
}
componentDidMount() {
@@ -51,7 +57,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
document.removeEventListener("mouseup", this.handleMouseUp);
}
-
@action
handleMouseDown = (e: React.PointerEvent): void => {
if (e.button != 0 ||
@@ -62,7 +67,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
return
}
- e.stopPropagation()
const point = this.relativeCoordinatesForEvent(e);
// start the new line, saves a uuid to represent the field of the stroke
@@ -74,7 +78,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: this.props.Document.GetNumber(KeyStore.CurPage, 0)
+ page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
});
this.inkData = data;
this._isDrawing = true;
@@ -110,8 +114,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => {
let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY);
- x += 50000
- y += 50000
+ x += InkingCanvas.InkOffset;
+ y += InkingCanvas.InkOffset;
return { x, y };
}
@@ -145,11 +149,11 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
// parse data from server
let paths: Array<JSX.Element> = []
- let curPage = this.props.Document.GetNumber(KeyStore.CurPage, 0)
+ let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1)
Array.from(lines).map(item => {
let id = item[0];
let strokeData = item[1];
- if (strokeData.page == 0 || strokeData.page == curPage)
+ if (strokeData.page == -1 || strokeData.page == curPage)
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
color={strokeData.color}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 955831fb6..ad6bbd476 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,16 +1,28 @@
import { observable, action, computed } from "mobx";
-import { CirclePicker, ColorResult } from 'react-color';
+
+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 { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
+import { SelectionManager } from "../util/SelectionManager";
+import { KeyStore } from "../../fields/KeyStore";
+import { TextField } from "../../fields/TextField";
+
+library.add(faPen, faHighlighter, faEraser, faBan);
@observer
export class InkingControl extends React.Component {
static Instance: InkingControl = new InkingControl({});
@observable private _selectedTool: InkTool = InkTool.None;
- @observable private _selectedColor: string = "#f44336";
+ @observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "25";
+ @observable private _open: boolean = false;
+ @observable private _colorPickerDisplay: boolean = false;
constructor(props: Readonly<{}>) {
super(props);
@@ -25,6 +37,12 @@ export class InkingControl extends React.Component {
@action
switchColor = (color: ColorResult): void => {
this._selectedColor = color.hex;
+ if (SelectionManager.SelectedDocuments().length == 1) {
+ var sdoc = SelectionManager.SelectedDocuments()[0];
+ if (sdoc.props.ContainingCollectionView && sdoc.props.ContainingCollectionView) {
+ sdoc.props.Document.SetOnPrototype(KeyStore.BackgroundColor, new TextField(color.hex));
+ }
+ }
}
@action
@@ -49,29 +67,50 @@ export class InkingControl extends React.Component {
selected = (tool: InkTool) => {
if (this._selectedTool === tool) {
- return { backgroundColor: "black", color: "white" }
+ return { color: "#61aaa3" }
}
return {}
}
+ @action
+ toggleDisplay = () => {
+ this._open = !this._open;
+ }
+
+ @action
+ toggleColorPicker = () => {
+ this._colorPickerDisplay = !this._colorPickerDisplay;
+ }
+
render() {
return (
- <div className="inking-control">
- <div className="ink-tools ink-panel">
- <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button>
- <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button>
- <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button>
- <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button>
- </div>
- <div className="ink-size ink-panel">
- <label htmlFor="stroke-width">Size</label>
- <input type="range" min="1" max="100" defaultValue="25" name="stroke-width"
+ <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <li className="ink-tools ink-panel">
+ <div className="ink-tool-buttons">
+ <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button>
+ <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Highlighter" /></button>
+ <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Eraser" /></button>
+ <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}><FontAwesomeIcon icon="ban" size="lg" title="Pointer" /></button>
+ </div>
+ </li>
+ <li className="ink-color ink-panel">
+ <label>COLOR: </label>
+ <div className="ink-color-display" style={{ backgroundColor: this._selectedColor }}
+ onClick={() => this.toggleColorPicker()}>
+ {/* {this._colorPickerDisplay ? <span>&#9660;</span> : <span>&#9650;</span>} */}
+ </div>
+ <div className="ink-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}>
+ <CirclePicker onChange={this.switchColor} circleSize={22} width={"220"} />
+ </div>
+ </li>
+ <li className="ink-size ink-panel">
+ <label htmlFor="stroke-width">SIZE: </label>
+ <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
+ <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- </div>
- <div className="ink-color ink-panel">
- <CirclePicker onChange={this.switchColor} />
- </div>
- </div>
+ </li>
+ </ul >
)
}
} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index d724421d3..87b5c43d8 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -21,8 +21,6 @@ export class InkingStroke extends React.Component<StrokeProps> {
@observable private _strokeColor: string = this.props.color;
@observable private _strokeWidth: string = this.props.width;
- private _canvasColor: string = "#cdcdcd";
-
deleteStroke = (e: React.MouseEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 4334ed299..bb42db202 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -1,22 +1,35 @@
+@import "global_variables";
+@import "nodeModuleOverrides";
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
- font-family: 'Hind Siliguri', sans-serif;
+ font-family: $sans-serif;
margin: 0;
}
+#dash-title {
+ position: absolute;
+ right: 46.5%;
+ letter-spacing: 3px;
+ top: 9px;
+ font-size: 12px;
+ color: $alt-accent;
+ z-index: 9999;
+}
+
h1 {
font-size: 50px;
position: fixed;
top: 30px;
left: 50%;
transform: translateX(-50%);
- color: black;
+ color: $dark-color;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
z-index: 9999;
- font-family: 'Fjalla One', sans-serif;
+ font-family: $sans-serif;
+ font-weight: 700;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@@ -25,27 +38,139 @@ h1 {
user-select: none;
}
+.jsx-parser {
+ width:100%
+}
+
p {
margin: 0px;
padding: 0px;
}
+
::-webkit-scrollbar {
-webkit-appearance: none;
- height:5px;
- width:5px;
+ height: 5px;
+ width: 5px;
}
+
::-webkit-scrollbar-thumb {
border-radius: 2px;
- background-color: rgba(0,0,0,.5);
+ background-color: rgba(0, 0, 0, 0.5);
}
-.main-buttonDiv {
+// button stuff
+button {
+ background: $dark-color;
+ outline: none;
+ border: 0px;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ padding: 10px;
+ transition: transform 0.2s;
+}
+
+button:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.clear-db-button {
position: absolute;
- width: 150px;
- left: 0px;
+ right: 45%;
+ bottom: 3%;
+ font-size: 50%;
+}
+
+.round-button {
+ width: 36px;
+ height: 36px;
+ border-radius: 18px;
+ font-size: 15px;
}
+
+.round-button:hover {
+ transform: scale(1.15);
+}
+
+.add-button {
+ position: relative;
+ margin-right: 10px;
+}
+
.main-undoButtons {
position: absolute;
width: 150px;
right: 0px;
}
+
+//toolbar stuff
+#toolbar {
+ position: absolute;
+ bottom: 62px;
+ left: 24px;
+ .toolbar-button {
+ display: block;
+ margin-bottom: 10px;
+ }
+}
+
+// add nodes menu. Note that the + button is actually an input label, not an actual button.
+#add-nodes-menu {
+ position: absolute;
+ bottom: 24px;
+ left: 24px;
+ label {
+ background: $dark-color;
+ color: $light-color;
+ display: inline-block;
+ border-radius: 18px;
+ font-size: 25px;
+ width: 36px;
+ height: 36px;
+ margin-right: 10px;
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+ label p {
+ padding-left: 10.5px;
+ padding-top: 3px;
+ }
+ label:hover {
+ background: $main-accent;
+ transform: scale(1.15);
+ }
+ input {
+ display: none;
+ }
+ input:not(:checked)~#add-options-content {
+ display: none;
+ }
+ input:checked~label {
+ transform: rotate(45deg);
+ transition: transform 0.5s;
+ cursor: pointer;
+ }
+}
+
+#add-options-content {
+ display: table;
+ opacity: 1;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ float: right;
+ bottom: 0.3em;
+ margin-bottom: -1.68em;
+}
+
+ul#add-options-list {
+ list-style: none;
+ padding: 0;
+ li {
+ display: inline-block;
+ padding: 0;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b78f59681..26a07fdfe 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,4 +1,4 @@
-import { action, configure } from 'mobx';
+import { action, configure, observable, runInAction } from 'mobx';
import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@@ -7,111 +7,303 @@ import { KeyStore } from '../../fields/KeyStore';
import "./Main.scss";
import { MessageStore } from '../../server/Message';
import { Utils } from '../../Utils';
+import * as request from 'request'
import { Documents } from '../documents/Documents';
import { Server } from '../Server';
import { setupDrag } from '../util/DragManager';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
+import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
import { DocumentView } from './nodes/DocumentView';
import "./Main.scss";
+import { observer } from 'mobx-react';
import { InkingControl } from './InkingControl';
+import { RouteStore } from '../../server/RouteStore';
+import { json } from 'body-parser';
+import { library } 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';
+import { faFilePdf } from '@fortawesome/free-solid-svg-icons';
+import { faObjectGroup } from '@fortawesome/free-solid-svg-icons';
+import { faTable } from '@fortawesome/free-solid-svg-icons';
+import { faGlobeAsia } from '@fortawesome/free-solid-svg-icons';
+import { faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+import { faRedoAlt } from '@fortawesome/free-solid-svg-icons';
+import { faPenNib } from '@fortawesome/free-solid-svg-icons';
+import { faFilm } from '@fortawesome/free-solid-svg-icons';
+import { faMusic } from '@fortawesome/free-solid-svg-icons';
+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 { ListField } from '../../fields/ListField';
+@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;
-configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action
-window.addEventListener("drop", (e) => e.preventDefault(), false)
-window.addEventListener("dragover", (e) => e.preventDefault(), false)
-document.addEventListener("pointerdown", action(function (e: PointerEvent) {
- if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
- ContextMenu.Instance.clearItems()
+ private mainDocId: string | undefined;
+ private currentUser?: DashUserModel;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ // causes errors to be generated when modifying an observable outside of an action
+ configure({ enforceActions: "observed" });
+ if (window.location.pathname !== RouteStore.home) {
+ let pathname = window.location.pathname.split("/");
+ this.mainDocId = pathname[pathname.length - 1];
+ }
+
+ CurrentUserUtils.loadCurrentUser();
+
+ library.add(faFont);
+ library.add(faImage);
+ library.add(faFilePdf);
+ library.add(faObjectGroup);
+ library.add(faTable);
+ library.add(faGlobeAsia);
+ library.add(faUndoAlt);
+ library.add(faRedoAlt);
+ library.add(faPenNib);
+ library.add(faFilm);
+ library.add(faMusic);
+
+ this.initEventListeners();
+ Documents.initProtos(() => {
+ this.initAuthenticationRouters();
+ });
+ }
+
+ 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>) => {
+ if (field instanceof Document) {
+ this.openWorkspace(field, true);
+ }
+ }));
+ }
+ }
+
+ componentDidMount() {
+ window.onpopstate = this.onHistory;
}
-}), true)
+ componentWillUnmount() {
+ window.onpopstate = null;
+ }
+
+ initEventListeners = () => {
+ // window.addEventListener("pointermove", (e) => this.reportLocation(e))
+ window.addEventListener("drop", (e) => e.preventDefault(), false) // drop event handler
+ window.addEventListener("dragover", (e) => e.preventDefault(), false) // drag event handler
+ // click interactions for the context menu
+ document.addEventListener("pointerdown", action(function (e: PointerEvent) {
+ if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
+ ContextMenu.Instance.clearItems();
+ }
+ }), true);
+ }
-const mainDocId = "mainDoc";
-let mainContainer: Document;
-let mainfreeform: Document;
-Documents.initProtos(mainDocId, (res?: Document) => {
- if (res instanceof Document) {
- mainContainer = res;
- mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document);
+ 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);
+ }
+ });
}
- else {
- mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, 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(() => {
- mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
-
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] };
- mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout));
- mainContainer.Set(KeyStore.ActiveFrame, mainfreeform);
+ 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);
+ }
+
+ @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)));
+ });
+ }
+
+ @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 => {
+ // 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) {
+ col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => {
+ if (f && f.Data.length > 0) {
+ CollectionDockingView.Instance.AddRightSplit(col);
+ }
+ })
+ }
+ }, 100);
+ });
+ }
+
+ toggleWorkspaces = () => {
+ if (WorkspacesMenu.Instance) {
+ WorkspacesMenu.Instance.toggle()
+ }
+ }
+
+ render() {
+ let imgRef = React.createRef<HTMLDivElement>();
+ let pdfRef = React.createRef<HTMLDivElement>();
+ let webRef = React.createRef<HTMLDivElement>();
+ let textRef = React.createRef<HTMLDivElement>();
+ let schemaRef = React.createRef<HTMLDivElement>();
+ let videoRef = React.createRef<HTMLDivElement>();
+ let audioRef = React.createRef<HTMLDivElement>();
+ let colRef = React.createRef<HTMLDivElement>();
+ let workspacesRef = React.createRef<HTMLDivElement>();
+ let logoutRef = React.createRef<HTMLDivElement>();
+
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"
+ let weburl = "https://cs.brown.edu/courses/cs166/";
+ let audiourl = "http://techslides.com/demos/samples/sample.mp3";
+ let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
+
+ let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}))
+ 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([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" }));
+ 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 addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator()));
+
+ return (
+ <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <Measure onResize={(r: any) => runInAction(() => {
+ this.pwidth = r.entry.width;
+ this.pheight = r.entry.height;
+ })}>
+ {({ measureRef }) => {
+ if (!this.mainContainer) {
+ return <div></div>
+ }
+ return <div ref={measureRef} style={{ position: "absolute", width: "100%", height: "100%" }}>
+ <DocumentView Document={this.mainContainer}
+ AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity}
+ ContentScaling={() => 1}
+ PanelWidth={() => this.pwidth}
+ PanelHeight={() => this.pheight}
+ isTopMost={true}
+ SelectOnLoad={false}
+ focus={() => { }}
+ ContainingCollectionView={undefined} />
+ </div>
+ }}
+ </Measure>
+ <DocumentDecorations />
+ <ContextMenu />
+
+ <button className="clear-db-button" onClick={clearDatabase}>Clear Database</button>
+
+ {/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */}
+ < div id="toolbar" >
+ <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
+ </div >
+
+ <div className="main-buttonDiv" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}>
+ <button onClick={this.toggleWorkspaces}>Workspaces</button></div>
+ <div className="main-buttonDiv" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+ <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div>
+
+ <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} />
+ {/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */}
+ < div id="add-nodes-menu" >
+ <input type="checkbox" id="add-menu-toggle" />
+ <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+
+ <div id="add-options-content">
+ <ul id="add-options-list">
+ <li><div ref={textRef}><button className="round-button add-button" title="Add Textbox" onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>
+ <FontAwesomeIcon icon="font" size="sm" />
+ </button></div></li>
+ <li><div ref={imgRef}><button className="round-button add-button" title="Add Image" onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>
+ <FontAwesomeIcon icon="image" size="sm" />
+ </button></div></li>
+ <li><div ref={pdfRef}><button className="round-button add-button" title="Add PDF" onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>
+ <FontAwesomeIcon icon="file-pdf" size="sm" />
+ </button></div></li>
+ <li><div ref={videoRef}><button className="round-button add-button" title="Add Video" onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>
+ <FontAwesomeIcon icon="film" size="sm" />
+ </button></div></li>
+ <li><div ref={audioRef}><button className="round-button add-button" title="Add Audio" onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>
+ <FontAwesomeIcon icon="music" size="sm" />
+ </button></div></li>
+ <li><div ref={webRef}><button className="round-button add-button" title="Add Web Clipping" onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>
+ <FontAwesomeIcon icon="globe-asia" size="sm" />
+ </button></div></li>
+ <li><div ref={colRef}><button className="round-button add-button" title="Add Collection" onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>
+ <FontAwesomeIcon icon="object-group" size="sm" />
+ </button></div></li>
+ <li><div ref={schemaRef}><button className="round-button add-button" title="Add Schema" onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>
+ <FontAwesomeIcon icon="table" size="sm" />
+ </button></div></li>
+ </ul>
+ </div>
+ </div >
+
+ <InkingControl />
+ </div>
+ );
}
+}
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
- let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}))
- 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([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" }));
- 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 addClick = (creator: () => Document) => action(() =>
- mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())
- );
-
- let imgRef = React.createRef<HTMLDivElement>();
- let pdfRef = React.createRef<HTMLDivElement>();
- let webRef = React.createRef<HTMLDivElement>();
- let textRef = React.createRef<HTMLDivElement>();
- let schemaRef = React.createRef<HTMLDivElement>();
- let videoRef = React.createRef<HTMLDivElement>();
- let audioRef = React.createRef<HTMLDivElement>();
- let colRef = React.createRef<HTMLDivElement>();
-
- ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <DocumentView Document={mainContainer}
- AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity}
- ContentScaling={() => 1}
- PanelWidth={() => 0}
- PanelHeight={() => 0}
- isTopMost={true}
- SelectOnLoad={false}
- focus={() => { }}
- ContainingCollectionView={undefined} />
- <DocumentDecorations />
- <ContextMenu />
- <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} >
- <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div>
- <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} >
- <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div>
- <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}>
- <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div>
- <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}>
- <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div>
- <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}>
- <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div>
- <div className="main-buttonDiv" style={{ bottom: '125px' }} >
- <button onClick={clearDatabase}>Clear Database</button></div>
- <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}>
- <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div>
- <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}>
- <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div>
- <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}>
- <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div>
- <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button>
- <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button>
- <InkingControl />
- </div>),
- document.getElementById('root'));
-})
+ReactDOM.render(<Main />, document.getElementById('root'));
diff --git a/src/client/views/_global_variables.scss b/src/client/views/_global_variables.scss
new file mode 100644
index 000000000..44a819b79
--- /dev/null
+++ b/src/client/views/_global_variables.scss
@@ -0,0 +1,17 @@
+@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
+// colors
+$light-color: #fcfbf7;
+$light-color-secondary: rgb(241, 239, 235);
+$main-accent: #61aaa3;
+// $alt-accent: #cdd5ec;
+// $alt-accent: #cdeceb;
+$alt-accent: #59dff7;
+$lighter-alt-accent: rgb(207, 220, 240);
+$intermediate-color: #9c9396;
+$dark-color: #121721;
+// fonts
+$sans-serif: "Noto Sans", sans-serif;
+// $sans-serif: "Roboto Slab", sans-serif;
+$serif: "Crimson Text", serif;
+// misc values
+$border-radius: 0.3em;
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
new file mode 100644
index 000000000..6f97e60f8
--- /dev/null
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -0,0 +1,23 @@
+// this file is for overriding all the css from installed node modules
+
+// goldenlayout stuff
+div .lm_header {
+ background: $dark-color;
+ min-height: 2em;
+}
+
+.lm_tab {
+ margin-top: 0.6em !important;
+ padding-top: 0.5em !important;
+ min-height: 1.35em;
+ padding-bottom: 0px;
+ border-radius: 5px;
+ font-family: $sans-serif !important;
+}
+
+.lm_header .lm_controls {
+ right: 1em !important;
+}
+
+// @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out
+// why. Low priority for now but it's bugging me. --Julie
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 94005a4c0..19788447e 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -144,7 +144,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (this._containerRef.current) {
reaction(
() => this.props.Document.GetText(KeyStore.Data, ""),
- () => this.setupGoldenLayout(), { fireImmediately: true });
+ () => setTimeout(() => this.setupGoldenLayout(), 1), { fireImmediately: true });
window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
}
@@ -212,6 +212,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stack.remove();
//}
}));
+ stack.header.controlsContainer.find('.lm_popout') //get the close icon
+ .off('click') //unbind the current click handler
+ .click(action(function () {
+ var url = "http://localhost:1050/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId;
+ let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
+ }));
}
render() {
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
index d487cd7ce..11addc5a1 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/CollectionFreeFormView.scss
@@ -1,36 +1,56 @@
+@import "../global_variables";
+
.collectionfreeformview-container {
+ .collectionfreeformview > .jsx-parser {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ }
- .collectionfreeformview > .jsx-parser{
- position:absolute;
- height: 100%;
- width: 100%;
- }
-
- border-style: solid;
- box-sizing: border-box;
- position: relative;
+ //nested freeform views
+ // .collectionfreeformview-container {
+ // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
+ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
+ // background-size: 30px 30px;
+ // }
+
+ border: 0px solid $light-color-secondary;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: relative;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ .collectionfreeformview {
+ position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
- overflow: hidden;
- .collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width:100%;
- height: 100%;
- }
-}
-.collectionfreeformview-marquee{
- border-style: dashed;
- box-sizing: border-box;
- position: absolute;
- border-width: 1px;
- border-color: black;
+ }
}
.collectionfreeformview-overlay {
+ .collectionfreeformview > .jsx-parser {
+ position: absolute;
+ height: 100%;
+ }
+ .formattedTextBox-cont {
+ background: $light-color-secondary;
+ }
+ position:absolute;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ overflow: hidden;
+ box-sizing: border-box;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ .collectionfreeformview {
.collectionfreeformview > .jsx-parser{
position:absolute;
height: 100%;
@@ -39,37 +59,39 @@
background:yellow;
}
- border-style: solid;
- box-sizing: border-box;
+ // overflow: hidden;
+ // border-style: solid;
+ // box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
- overflow: hidden;
- .collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width:100%;
- height: 100%;
- }
+ }
}
+// selection border...?
.border {
- border-style: solid;
- box-sizing: border-box;
- width: 100%;
- height: 100%;
+ border-style: solid;
+ box-sizing: border-box;
+ width: 98%;
+ height: 98%;
+ border-radius: $border-radius;
}
//this is an animation for the blinking cursor!
@keyframes blink {
- 0% {opacity: 0}
- 49%{opacity: 0}
- 50% {opacity: 1}
+ 0% {
+ opacity: 0;
+ }
+ 49% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
}
#prevCursor {
- animation: blink 1s infinite;
-} \ No newline at end of file
+ animation: blink 1s infinite;
+}
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index ab41d1378..8bf4a7539 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -5,46 +5,63 @@ import { FieldWaiting } from "../../../fields/Field";
import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
import { TextField } from "../../../fields/TextField";
-import { Documents } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionView } from "../collections/CollectionView";
import { InkingCanvas } from "../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DocumentView } from "../nodes/DocumentView";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
-import { ImageBox } from "../nodes/ImageBox";
-import { KeyValueBox } from "../nodes/KeyValueBox";
-import { PDFBox } from "../nodes/PDFBox";
-import { WebBox } from "../nodes/WebBox";
+import { DocumentContentsView } from "../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../nodes/DocumentView";
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 { SelectionManager } from "../../util/SelectionManager";
-const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
+import v5 = require("uuid/v5");
@observer
export class CollectionFreeFormView extends CollectionViewBase {
- private _canvasRef = React.createRef<HTMLDivElement>();
- @observable
- private _lastX: number = 0;
- @observable
- private _lastY: number = 0;
+ public _canvasRef = React.createRef<HTMLDivElement>();
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
- @observable
- private _downX: number = 0;
- @observable
- private _downY: number = 0;
+ 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
+ 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;
+ }
+
+ public selectDocuments = (docs: Document[]) => {
+ this.props.CollectionView.SelectedDocs.length = 0;
+ docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id));
+ }
+
+ 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;
+ }
//determines whether the blinking cursor for indicating whether a text will be made on key down is visible
- @observable
- private _previewCursorVisible: boolean = false;
+ @observable public PreviewCursorVisible: boolean = false;
+ @observable public MarqueeVisible = false;
+ @observable public DownX: number = 0;
+ @observable public DownY: number = 0;
+ @observable private _lastX: number = 0;
+ @observable private _lastY: number = 0;
@computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
@computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
@@ -60,52 +77,44 @@ export class CollectionFreeFormView extends CollectionViewBase {
@action
drop = (e: Event, de: DragManager.DropEvent) => {
super.drop(e, de);
- const docView: DocumentView = de.data["documentView"];
- let doc: Document = docView ? docView.props.Document : de.data["document"];
- if (doc) {
- let screenX = de.x - (de.data["xOffset"] as number || 0);
- let screenY = de.y - (de.data["yOffset"] as number || 0);
+ 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);
- doc.SetNumber(KeyStore.X, x);
- doc.SetNumber(KeyStore.Y, y);
- this.bringToFront(doc);
+ de.data.droppedDocument.SetNumber(KeyStore.X, x);
+ de.data.droppedDocument.SetNumber(KeyStore.Y, y);
+ this.bringToFront(de.data.droppedDocument);
}
}
- @observable
- _marquee = false;
+
+ @action
+ 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) {
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
- this._lastX = e.pageX;
- this._lastY = e.pageY;
- this._downX = e.pageX;
- this._downY = e.pageY;
+ this._lastX = this.DownX = e.pageX;
+ this._lastY = this.DownY = e.pageY;
}
}
@action
onPointerUp = (e: PointerEvent): void => {
- if (this._marquee) {
- document.removeEventListener("keydown", this.marqueeCommand);
- }
e.stopPropagation();
- if (this._marquee) {
- if (!e.shiftKey) {
- SelectionManager.DeselectAll();
- }
- var selectedDocs = this.marqueeSelect();
- selectedDocs.map(s => this.props.CollectionView.SelectedDocs.push(s.Id));
- }
- else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) {
+ if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) {
//show preview text cursor on tap
- this._previewCursorVisible = true;
+ this.PreviewCursorVisible = true;
//select is not already selected
if (!this.props.isSelected()) {
this.props.select(false);
@@ -115,98 +124,26 @@ export class CollectionFreeFormView extends CollectionViewBase {
}
@action
- cleanupInteractions = () => {
- document.removeEventListener("keydown", this.marqueeCommand);
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- this._marquee = false;
- }
-
- intersectRect(r1: { left: number, right: number, top: number, bottom: number },
- r2: { left: number, right: number, top: number, bottom: number }) {
- return !(r2.left > r1.right ||
- r2.right < r1.left ||
- r2.top > r1.bottom ||
- r2.bottom < r1.top);
- }
-
- marqueeSelect() {
- this.props.CollectionView.SelectedDocs.length = 0;
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
- let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
- let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- let selRect = { left: p[0], top: p[1], right: p[0] + Math.abs(v[0]), bottom: p[1] + Math.abs(v[1]) }
-
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
- const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
- let selection: Document[] = [];
- if (lvalue && lvalue != FieldWaiting) {
- lvalue.Data.map(doc => {
- var page = doc.GetNumber(KeyStore.Page, 0);
- if (page == curPage || page == 0) {
- 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, right: x + w, bottom: y + h }, selRect))
- selection.push(doc)
- }
- })
- }
- return selection;
- }
-
- @action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.props.active()) {
- let wasMarquee = this._marquee;
- this._marquee = e.buttons != 2 && !e.altKey && !e.metaKey;
- if (this._marquee && !wasMarquee) {
- this._previewCursorVisible = false;
- document.addEventListener("keydown", this.marqueeCommand);
+ if (e.buttons == 1 && !e.altKey && !e.metaKey) {
+ this.MarqueeVisible = true;
}
- if (this._marquee) {
+ if (this.MarqueeVisible) {
e.stopPropagation();
e.preventDefault();
}
-
- if (!this._marquee && (!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
+ else 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);
- this._previewCursorVisible = false;
this.SetPan(x - dx, y - dy);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
e.stopPropagation();
e.preventDefault();
}
}
- this._lastX = e.pageX;
- this._lastY = e.pageY;
- }
-
- @action
- marqueeCommand = (e: KeyboardEvent) => {
- if (e.key == "Backspace") {
- this.marqueeSelect().map(d => this.props.removeDocument(d));
- this.cleanupInteractions();
- }
- if (e.key == "c") {
- let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
- let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
-
- let selected = this.marqueeSelect().map(m => m);
- this.marqueeSelect().map(d => this.props.removeDocument(d));
- //setTimeout(() => {
- this.props.CollectionView.addDocument(Documents.FreeformDocument(selected.map(d => {
- d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - p[0] - v[0] / 2);
- d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - p[1] - v[1] / 2);
- d.SetNumber(KeyStore.Page, this.props.Document.GetNumber(KeyStore.Page, 0));
- d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
- return d;
- }), { x: p[0], y: p[1], panx: 0, pany: 0, width: v[0], height: v[1], title: "a nested collection" }));
- // }, 100);
- this.cleanupInteractions();
- }
}
@action
@@ -247,7 +184,6 @@ export class CollectionFreeFormView extends CollectionViewBase {
@action
private SetPan(panX: number, panY: number) {
var x1 = this.getLocalTransform().inverse().Scale;
- var x2 = this.getTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
@@ -264,24 +200,6 @@ export class CollectionFreeFormView extends CollectionViewBase {
}
@action
- onKeyDown = (e: React.KeyboardEvent<Element>) => {
- //if not these keys, make a textbox if preview cursor is active!
- if (!e.ctrlKey && !e.altKey) {
- if (this._previewCursorVisible) {
- //make textbox and add it to this collection
- let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY);
- let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" });
- // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself
- this._selectOnLoaded = newBox.Id;
- //set text to be the typed key and get focus on text box
- this.props.CollectionView.addDocument(newBox);
- //remove cursor from screen
- this._previewCursorVisible = false;
- }
- }
- }
-
- @action
bringToFront(doc: Document) {
const { fieldKey: fieldKey, Document: Document } = this.props;
@@ -319,27 +237,31 @@ export class CollectionFreeFormView extends CollectionViewBase {
this.props.focus(this.props.Document);
}
+ getDocumentViewProps(document: Document): DocumentViewProps {
+ return {
+ Document: document,
+ AddDocument: this.props.addDocument,
+ RemoveDocument: this.props.removeDocument,
+ ScreenToLocalTransform: this.getTransform,
+ isTopMost: false,
+ SelectOnLoad: document.Id == this._selectOnLoaded,
+ PanelWidth: document.Width,
+ PanelHeight: document.Height,
+ ContentScaling: this.noScaling,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument
+ }
+ }
@computed
get views() {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
+ 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} Document={doc}
- AddDocument={this.props.addDocument}
- RemoveDocument={this.props.removeDocument}
- ScreenToLocalTransform={this.getTransform}
- isTopMost={false}
- SelectOnLoad={doc.Id === this._selectOnLoaded}
- ContentScaling={this.noScaling}
- PanelWidth={doc.Width}
- PanelHeight={doc.Height}
- ContainingCollectionView={this.props.CollectionView}
- focus={this.focusDocument}
- />);
+ (<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
})
}
return null;
@@ -348,63 +270,79 @@ export class CollectionFreeFormView extends CollectionViewBase {
@computed
get backgroundView() {
return !this.backgroundLayout ? (null) :
- (<JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
- bindings={this.props.bindings}
- jsx={this.backgroundLayout}
- showWarnings={true}
- onError={(test: any) => console.log(test)}
- />);
+ (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+ layoutKey={KeyStore.BackgroundLayout} isSelected={() => false} select={() => { }} />);
}
@computed
get overlayView() {
return !this.overlayLayout ? (null) :
- (<JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
- bindings={this.props.bindings}
- jsx={this.overlayLayout}
- showWarnings={true}
- onError={(test: any) => console.log(test)}
- />);
+ (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+ layoutKey={KeyStore.OverlayLayout} 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)
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 = (e: React.FocusEvent<HTMLDivElement>): void => {
- this._previewCursorVisible = false;
+ onBlur = (): void => {
+ this.PreviewCursorVisible = false;
}
- render() {
- //determines whether preview text cursor should be visible (ie when user taps this collection it should)
- let cursor = null;
- if (this._previewCursorVisible) {
- //get local position and place cursor there!
- let [x, y] = this.getTransform().transformPoint(this._downX, this._downY);
- cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div>
- }
+ 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);
- let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
- let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }}></div> : (null);
+ 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);
+ }
+ }
+ }
+
+ 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}
- onKeyPress={this.onKeyDown}
+ 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`, }}
+ style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }}
tabIndex={0}
ref={this.createDropTarget}>
<div className="collectionfreeformview"
@@ -412,12 +350,57 @@ export class CollectionFreeFormView extends CollectionViewBase {
ref={this._canvasRef}>
{this.backgroundView}
<InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
- {cursor}
+ <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} />
{this.views}
- {marquee}
+ {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>
+ <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}
+
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
new file mode 100644
index 000000000..0144625c1
--- /dev/null
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -0,0 +1,27 @@
+.collectionPdfView-buttonTray {
+ top : 25px;
+ left : 20px;
+ position: relative;
+ transform-origin: left top;
+ position: absolute;
+}
+.collectionPdfView-cont{
+ width: 100%;
+ height: 100%;
+ position: absolute;
+
+}
+.collectionPdfView-backward {
+ color : white;
+ top :0px;
+ left : 0px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+}
+.collectionPdfView-forward {
+ color : white;
+ top :0px;
+ left : 35px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index f22c07060..e64b4c945 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,10 +1,11 @@
-import { action, computed } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
import { CollectionView, CollectionViewType } from "./CollectionView";
import { CollectionViewProps } from "./CollectionViewBase";
+import "./CollectionPDFView.scss"
import React = require("react");
import { FieldId } from "../../../fields/Field";
@@ -18,25 +19,26 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> {
isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
}
- public SelectedDocs: FieldId[] = []
- @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0;
- @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0;
+ private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); }
+ private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
+ @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : -1;
+ @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : -1;
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); }
- @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
- @computed private get uIButtons() {
+ private get uIButtons() {
+ let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().transformDirection(1, 1)[0]);
return (
- <div className="pdfBox-buttonTray" key="tray">
- <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button>
- <button className="pdfButton" onClick={this.onPageForward}>{">"}</button>
+ <div className="collectionPdfView-buttonTray" key="tray" style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button>
+ <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button>
</div>);
}
// "inherited" CollectionView API starts here...
-
+ @observable
+ public SelectedDocs: FieldId[] = []
public active: () => boolean = () => CollectionView.Active(this);
- addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
+ 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 => {
@@ -49,7 +51,7 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> {
get subView(): any { return CollectionView.SubView(this); }
render() {
- return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}>
+ return (<div className="collectionPdfView-cont" onContextMenu={this.specificContextMenu}>
{this.subView}
{this.props.isSelected() ? this.uIButtons : (null)}
</div>)
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index ab906cf4b..d16f0edf9 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -60,16 +60,25 @@
padding:0px;
}
+@import "../global_variables";
.collectionSchemaView-container {
- border-style: solid;
+ border: 1px solid $intermediate-color;
+ border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
+ overflow: hidden;
+ .collectionSchemaView-content {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+ }
.collectionSchemaView-previewRegion {
- position: relative;
- background: black;
- float: left;
+ position: relative;
+ background: $light-color;
+ float: left;
height: 100%;
}
.collectionSchemaView-previewHandle {
@@ -85,30 +94,57 @@
position: relative;
background: black;
float: left;
+ height: 37px;
+ width: 20px;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ background: $main-accent;
+ }
+ .collectionSchemaView-columnsHandle {
+ position: absolute;
+ height: 37px;
+ width: 20px;
+ z-index: 20;
+ left: 0;
+ bottom: 0;
+ background: $main-accent;
+ }
+ .collectionSchemaView-colDividerDragger {
+ position: relative;
+ box-sizing: border-box;
+ border-top: 1px solid $intermediate-color;
+ border-bottom: 1px solid $intermediate-color;
+ float: top;
+ width: 100%;
+ }
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ box-sizing: border-box;
+ border-left: 1px solid $intermediate-color;
+ border-right: 1px solid $intermediate-color;
+ float: left;
height: 100%;
}
.collectionSchemaView-tableContainer {
position: relative;
float: left;
- height: 100%;
+ height: 100%;
}
-
.ReactTable {
- position: absolute;
- // display: inline-block;
- // overflow: auto;
+ // position: absolute; // display: inline-block;
+ // overflow: auto;
width: 100%;
height: 100%;
- background: white;
+ background: $light-color;
box-sizing: border-box;
+ border: none !important;
.rt-table {
overflow-y: auto;
overflow-x: auto;
height: 100%;
-
display: -webkit-inline-box;
- direction: ltr;
- // direction:rtl;
+ direction: ltr; // direction:rtl;
// display:block;
}
.rt-tbody {
@@ -120,37 +156,47 @@
max-height: 44px;
}
.rt-td {
- border-width: 1;
- border-right-color: #aaa;
+ border-width: 1px;
+ border-right-color: $intermediate-color;
.imageBox-cont {
- position:relative;
- max-height:100%;
+ position: relative;
+ max-height: 100%;
}
.imageBox-cont img {
object-fit: contain;
max-width: 100%;
- height: 100%
+ height: 100%;
+ }
+ .videobox-cont {
+ object-fit: contain;
+ width: auto;
+ height: 100%;
}
- }
- .rt-tr-group {
- border-width: 1;
- border-bottom-color: #aaa
}
}
.ReactTable .rt-thead.-header {
- background:grey;
- }
- .ReactTable .rt-th, .ReactTable .rt-td {
+ background: $intermediate-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 12px;
+ height: 30px;
+ padding-top: 4px;
+ }
+ .ReactTable .rt-th,
+ .ReactTable .rt-td {
max-height: 44;
padding: 3px 7px;
+ font-size: 13px;
+ text-align: center;
}
.ReactTable .rt-tbody .rt-tr-group:last-child {
- border-bottom: grey;
+ border-bottom: $intermediate-color;
border-bottom-style: solid;
border-bottom-width: 1;
}
.documentView-node:first-child {
- background: grey;
+ background: $light-color;
.imageBox-cont img {
object-fit: contain;
}
@@ -264,4 +310,12 @@
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
+}
+
+.-even {
+ background: $light-color !important;
+}
+
+.-odd {
+ background: $light-color-secondary !important;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 7c87ef744..2598dd3a9 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,11 +1,16 @@
+<<<<<<< HEAD
import React = require("react");
import { action, observable } from "mobx";
+=======
+import React = require("react")
+import { action, observable, ObservableMap, computed } from "mobx";
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
import { observer } from "mobx-react";
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 } from "../../../fields/Field";
+import { Field, Opt } from "../../../fields/Field";
import { KeyStore } from "../../../fields/KeyStore";
import { CompileScript, ToField } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
@@ -14,9 +19,10 @@ import { EditableView } from "../EditableView";
import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { COLLECTION_BORDER_WIDTH, CollectionView } from "./CollectionView";
import { CollectionViewBase } from "./CollectionViewBase";
import { setupDrag } from "../../util/DragManager";
+<<<<<<< HEAD
import '../DocumentDecorations.scss';
import { Flyout, anchorPoints } from "../DocumentDecorations";
import { DropdownButton, Dropdown } from 'react-bootstrap';
@@ -27,21 +33,53 @@ import { Key } from "../../../fields/Key";
import { Server } from "../../Server";
//import { MenuItem } from 'react-bootstrap';
+=======
+import { Key } from "./../../../fields/Key";
+import { Server } from "../../Server";
+import { ListField } from "../../../fields/ListField";
+
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@observer
+class KeyToggle extends React.Component<{ keyId: string, checked: boolean, toggle: (key: Key) => void }> {
+ @observable
+ key: Key | undefined;
+
+ componentWillReceiveProps() {
+ Server.GetField(this.props.keyId, action((field: Opt<Field>) => {
+ if (field instanceof Key) {
+ this.key = field;
+ }
+ }))
+ }
+
+ render() {
+ if (this.key) {
+ return (<div key={this.key.Id}>
+ <input type="checkbox" checked={this.props.checked} onChange={() => this.key && this.props.toggle(this.key)} />{this.key.Name}
+ </div>)
+ } else {
+ return <div></div>
+ }
+ }
+}
+
+@observer
export class CollectionSchemaView extends CollectionViewBase {
private _mainCont = React.createRef<HTMLDivElement>();
- private DIVIDER_WIDTH = 5;
+ private DIVIDER_WIDTH = 4;
+ @observable _columns: Array<Key> = [KeyStore.Title, KeyStore.Data, KeyStore.Author];
@observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView
@observable _dividerX = 0;
@observable _panelWidth = 0;
@observable _panelHeight = 0;
@observable _selectedIndex = 0;
- @observable _splitPercentage: number = 50;
+ @observable _columnsPercentage = 0;
+ @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
@@ -57,10 +95,12 @@ export class CollectionSchemaView extends CollectionViewBase {
<FieldView {...props} />
)
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = setupDrag(reference, () => props.doc);
+ let onItemDown = setupDrag(reference, () => props.doc, (containingCollection: CollectionView) => this.props.removeDocument(props.doc));
return (
- <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}>
- <EditableView contents={contents}
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "36px" }} key={props.doc.Id} ref={reference}>
+ <EditableView
+ display={"inline"}
+ contents={contents}
height={36} GetValue={() => {
let field = props.doc.Get(props.fieldKey);
if (field && field instanceof Field) {
@@ -69,7 +109,7 @@ export class CollectionSchemaView extends CollectionViewBase {
return field || "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, undefined, true);
+ let script = CompileScript(value);
if (!script.compiled) {
return false;
}
@@ -99,7 +139,8 @@ export class CollectionSchemaView extends CollectionViewBase {
return {
onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
that._selectedIndex = rowInfo.index;
- this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize
+ // bcz - ugh - needed to force Measure to do its thing and call onResize
+ this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage - 0.05)
if (handleOriginal) {
handleOriginal()
@@ -112,27 +153,83 @@ export class CollectionSchemaView extends CollectionViewBase {
};
}
+ get columns() {
+ return this.props.Document.GetList<Key>(KeyStore.ColumnsKey, []);
+ }
+
+ @action
+ toggleKey = (key: Key) => {
+ this.props.Document.GetOrCreateAsync<ListField<Key>>(KeyStore.ColumnsKey, ListField,
+ (field) => {
+ const index = field.Data.indexOf(key);
+ if (index === -1) {
+ this.columns.push(key);
+ } else {
+ this.columns.splice(index, 1);
+ }
+
+ })
+ }
+
+ @observable keys: Key[] = [];
+
+ findAllDocumentKeys = (): { [id: string]: boolean } => {
+ const docs = this.props.Document.GetList<Document>(this.props.fieldKey, []);
+ let keys: { [id: string]: boolean } = {}
+ docs.forEach(doc => {
+ let protos = doc.GetAllPrototypes();
+ for (const proto of protos) {
+ proto._proxies.forEach((val: any, key: string) => {
+ keys[key] = false
+ })
+ }
+ })
+ this.columns.forEach(key => {
+ keys[key.Id] = true;
+ })
+ return keys;
+ }
+
_startSplitPercent = 0;
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100);
+ this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 1 ? 66 : 100;
+ if (this._startSplitPercent == this.splitPercentage) {
+ this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage == 0 ? 33 : 0);
}
}
onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
+ this._startSplitPercent = this.splitPercentage;
e.stopPropagation();
e.preventDefault();
document.addEventListener("pointermove", this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
}
+
+
+ @action
+ onColDividerMove = (e: PointerEvent): void => {
+ let nativeWidth = this._mainCont.current!.getBoundingClientRect();
+ this._columnsPercentage = 100 - (e.clientY - nativeWidth.top) / nativeWidth.height * 100;
+ }
+ @action
+ onColDividerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onColDividerMove);
+ document.removeEventListener('pointerup', this.onColDividerUp);
+ }
+ onColDividerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onColDividerMove);
+ document.addEventListener('pointerup', this.onColDividerUp);
+ }
+
@action
onExpanderMove = (e: PointerEvent): void => {
e.stopPropagation();
@@ -144,12 +241,12 @@ export class CollectionSchemaView extends CollectionViewBase {
e.preventDefault();
document.removeEventListener("pointermove", this.onExpanderMove);
document.removeEventListener('pointerup', this.onExpanderUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 100 ? 66 : 100;
+ if (this._startSplitPercent == this.splitPercentage) {
+ this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage == 0 ? 33 : 0);
}
}
onExpanderDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
+ this._startSplitPercent = this.splitPercentage;
e.stopPropagation();
e.preventDefault();
document.addEventListener("pointermove", this.onExpanderMove);
@@ -162,15 +259,36 @@ export class CollectionSchemaView extends CollectionViewBase {
// e.preventDefault();
// } else
{
- if (e.buttons === 1) {
- if (this.props.isSelected()) {
- e.stopPropagation();
- }
- }
+ // if (e.buttons === 1) {
+ // if (this.props.isSelected()) {
+ // e.stopPropagation();
+ // }
+ // }
}
}
@action
+ onColumnsMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ @action
+ onColumnsUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onColumnsMove);
+ document.removeEventListener('pointerup', this.onColumnsUp);
+ this._columnsPercentage = this._columnsPercentage ? 0 : 50;
+ }
+ onColumnsDown = (e: React.PointerEvent) => {
+ this._startSplitPercent = this.splitPercentage;
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onColumnsMove);
+ document.addEventListener('pointerup', this.onColumnsUp);
+ }
+
+ @action
setScaling = (r: any) => {
const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
@@ -202,9 +320,10 @@ export class CollectionSchemaView extends CollectionViewBase {
}
render() {
- const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author])
+ const columns = this.columns;
const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
+ const allKeys = this.findAllDocumentKeys();
let content = this._selectedIndex == -1 || !selected ? (null) : (
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
@@ -226,8 +345,11 @@ export class CollectionSchemaView extends CollectionViewBase {
)
let previewHandle = !this.props.active() ? (null) : (
<div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />);
- let dividerDragger = this._splitPercentage == 100 ? (null) :
+ let columnsHandle = !this.props.active() ? (null) : (
+ <div className="collectionSchemaView-columnsHandle" onPointerDown={this.onColumnsDown} />);
+ let dividerDragger = this.splitPercentage == 0 ? (null) :
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />
+<<<<<<< HEAD
//get the union of all childrens' keys
let addFields: { id: string, name: string }[] = [];
@@ -268,6 +390,10 @@ export class CollectionSchemaView extends CollectionViewBase {
}
+=======
+ let colDividerDragger = this._columnsPercentage == 0 ? (null) :
+ <div className="collectionSchemaView-colDividerDragger" onPointerDown={this.onColDividerDown} style={{ height: `${this.DIVIDER_WIDTH}px` }} />
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
@@ -277,33 +403,53 @@ export class CollectionSchemaView extends CollectionViewBase {
this._panelHeight = r.entry.height;
})}>
{({ measureRef }) =>
- <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}>
- <ReactTable
- data={children}
- pageSize={children.length}
- page={0}
- showPagination={false}
- columns={columns.map(col => ({
- Header: col.Name,
- accessor: (doc: Document) => [doc, col],
- id: col.Id
- }))}
- column={{
- ...ReactTableDefaults.column,
- Cell: this.renderCell,
-
- }}
- getTrProps={this.getTrProps}
- />
+ <div ref={measureRef} className="collectionSchemaView-tableContainer"
+ style={{ width: `calc(100% - ${this.splitPercentage}%)` }}>
+ <div className="collectionSchemaView-reactContainer" style={{ height: `calc(100% - ${this._columnsPercentage}%)` }}>
+ <ReactTable
+ data={children}
+ pageSize={children.length}
+ page={0}
+ showPagination={false}
+ columns={columns.map(col => ({
+ Header: col.Name,
+ accessor: (doc: Document) => [doc, col],
+ id: col.Id
+ }))}
+ column={{
+ ...ReactTableDefaults.column,
+ Cell: this.renderCell,
+
+ }}
+ getTrProps={this.getTrProps}
+ />
+ </div>
+ {colDividerDragger}
+ <div className="collectionSchemaView-addColumn" style={{ height: `${this._columnsPercentage}%` }} >
+ {/* <input type="checkbox" id="addColumn-toggle" />
+ <label htmlFor="addColumn-toggle" title="Add Column"><p>+</p></label> */}
+
+ <div className="addColumn-options">
+ <ul style={{ overflow: "scroll" }}>
+ {Array.from(Object.keys(allKeys)).map(item => {
+ return (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />)
+ })}
+ </ul>
+ </div>
+ </div>
</div>
}
</Measure>
{dividerDragger}
- <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}>
+ <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}>
{content}
</div>
{previewHandle}
+<<<<<<< HEAD
{optionsMenu}
+=======
+ {columnsHandle}
+>>>>>>> f70ad315167b714f11f7d68f35a46abe9e525a4d
</div>
</div >
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index f8d580a7b..fa0f1c761 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -1,16 +1,25 @@
+@import "../global_variables";
#body {
padding: 20px;
- background: #bbbbbb;
+ background: $light-color-secondary;
+ font-size: 13px;
+ overflow: scroll;
}
ul {
list-style: none;
+ padding-left: 20px;
}
li {
margin: 5px 0;
}
+.collection-child {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
.no-indent {
padding-left: 0;
}
@@ -18,10 +27,17 @@ li {
.bullet {
width: 1.5em;
display: inline-block;
+ color: $intermediate-color;
+}
+
+.coll-title {
+ font-size: 24px;
+ margin-bottom: 20px;
}
.collectionTreeView-dropTarget {
- border-style: solid;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
box-sizing: border-box;
height: 100%;
}
@@ -30,8 +46,16 @@ li {
display: inline-table;
}
+.docContainer:hover {
+ .delete-button {
+ display: inline;
+ }
+}
+
.delete-button {
- color: #999999;
+ color: $intermediate-color;
float: right;
- margin-left: 1em;
+ margin-left: 15px;
+ margin-top: 3px;
+ display: none;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 8b06d9ac4..f9da759fd 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -12,6 +12,10 @@ import { setupDrag } from "../../util/DragManager";
import { FieldWaiting } from "../../../fields/Field";
import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTrashAlt, faCaretRight, faCaretDown } from '@fortawesome/free-solid-svg-icons';
+
export interface TreeViewProps {
document: Document;
deleteDoc: (doc: Document) => void;
@@ -23,6 +27,10 @@ export enum BulletType {
List
}
+library.add(faTrashAlt);
+library.add(faCaretDown);
+library.add(faCaretRight);
+
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
@@ -50,11 +58,11 @@ class TreeView extends React.Component<TreeViewProps> {
switch (type) {
case BulletType.Collapsed:
- return <div className="bullet" onClick={onClicked}>&#9654;</div>
+ return <div className="bullet" onClick={onClicked}><FontAwesomeIcon icon="caret-right" /></div>
case BulletType.Collapsible:
- return <div className="bullet" onClick={onClicked}>&#9660;</div>
+ return <div className="bullet" onClick={onClicked}><FontAwesomeIcon icon="caret-down" /></div>
case BulletType.List:
- return <div className="bullet">&mdash;</div>
+ return <div className="bullet"></div>
}
}
@@ -69,7 +77,9 @@ class TreeView extends React.Component<TreeViewProps> {
return <div key={this.props.document.Id}></div>;
}
- return <div className="docContainer"> <EditableView contents={title.Data}
+ return <div className="docContainer"> <EditableView
+ display={"inline"}
+ contents={title.Data}
height={36} GetValue={() => {
let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField);
if (title && title !== "<Waiting>")
@@ -79,7 +89,7 @@ class TreeView extends React.Component<TreeViewProps> {
this.props.document.SetData(KeyStore.Title, value, TextField);
return true;
}} />
- <div className="delete-button" onClick={this.delete}>x</div>
+ <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>
</div >
}
@@ -101,7 +111,7 @@ class TreeView extends React.Component<TreeViewProps> {
<TreeView document={value} deleteDoc={this.remove} />)
)
subView =
- <li key={this.props.document.Id} >
+ <li className="collection-child" key={this.props.document.Id} >
{this.renderBullet(BulletType.Collapsible)}
{titleElement}
<ul key={this.props.document.Id}>
@@ -109,7 +119,7 @@ class TreeView extends React.Component<TreeViewProps> {
</ul>
</li>
} else {
- subView = <li key={this.props.document.Id}>
+ subView = <li className="collection-child" key={this.props.document.Id}>
{this.renderBullet(BulletType.Collapsed)}
{titleElement}
</li>
@@ -157,15 +167,17 @@ 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` }}>
- <h3>
+ <div className="coll-title">
<EditableView contents={titleStr}
+ display={"inline"}
height={72} GetValue={() => {
return this.props.Document.Title;
}} SetValue={(value: string) => {
this.props.Document.SetData(KeyStore.Title, value, TextField);
return true;
}} />
- </h3>
+ </div>
+ <hr />
<ul className="no-indent">
{childrenElement}
</ul>
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
new file mode 100644
index 000000000..cbb981b13
--- /dev/null
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -0,0 +1,40 @@
+
+.collectionVideoView-cont{
+ width: 100%;
+ height: 100%;
+ position: absolute;
+
+}
+.collectionVideoView-time{
+ color : white;
+ top :25px;
+ left : 25px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+}
+.collectionVideoView-play {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ left : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: left bottom;
+}
+.collectionVideoView-full {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ right : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: right bottom;
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
new file mode 100644
index 000000000..05f759967
--- /dev/null
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -0,0 +1,118 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../fields/Document";
+import { KeyStore } from "../../../fields/KeyStore";
+import { ContextMenu } from "../ContextMenu";
+import { CollectionView, CollectionViewType } from "./CollectionView";
+import { CollectionViewProps } from "./CollectionViewBase";
+import React = require("react");
+import { FieldId } from "../../../fields/Field";
+import "./CollectionVideoView.scss"
+
+
+@observer
+export class CollectionVideoView extends React.Component<CollectionViewProps> {
+
+ 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>
+ </div>,
+ <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ {this.playing ? "\"" : ">"}
+ </div>,
+ <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ F
+ </div>
+ ]);
+ }
+
+
+ // "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: () => { } });
+ }
+ }
+
+ get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
+ get subView(): any { return CollectionView.SubView(this); }
+
+ componentDidMount() {
+ this.updateTimecode();
+ }
+
+ get player(): HTMLVideoElement | undefined {
+ return this._mainCont.current ? this._mainCont.current.getElementsByTagName("video")[0] : undefined;
+ }
+
+ @action
+ updateTimecode = () => {
+ if (this.player) {
+ this.ctime = this.player.currentTime;
+ this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this.ctime));
+ }
+ 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;
+ } else {
+ this.player.pause();
+ this.playing = false;
+ }
+ }
+ }
+ @action
+ onFullDown = (e: React.PointerEvent) => {
+ if (this.player) {
+ this.player.requestFullscreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onResetDown = () => {
+ if (this.player) {
+ this.player.pause();
+ this.player.currentTime = 0;
+ }
+
+ }
+
+ render() {
+ return (<div className="collectionVideoView-cont" ref={this._mainCont} onContextMenu={this.specificContextMenu}>
+ {this.subView}
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </div>)
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 17a0fbd23..303099c16 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -22,22 +22,21 @@ export enum CollectionViewType {
Tree
}
-export const COLLECTION_BORDER_WIDTH = 2;
+export const COLLECTION_BORDER_WIDTH = 1;
@observer
export class CollectionView extends React.Component<CollectionViewProps> {
- @observable
- public SelectedDocs: FieldId[] = [];
-
public static LayoutString(fieldKey: string = "DataKey") {
return `<${CollectionView.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}/>`;
}
+ @observable
+ public SelectedDocs: FieldId[] = [];
public active: () => boolean = () => CollectionView.Active(this);
- addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
+ addDocument = (doc: Document, allowDuplicates: boolean): void => { CollectionView.AddDocument(this.props, doc, allowDuplicates); }
removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
get subView() { return CollectionView.SubView(this); }
@@ -49,14 +48,15 @@ export class CollectionView extends React.Component<CollectionViewProps> {
}
@action
- public static AddDocument(props: CollectionViewProps, doc: Document) {
- doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0));
+ public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean) {
+ doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, -1));
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>())
- value.push(doc);
+ if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
+ value.push(doc);
} else {
- props.Document.SetData(props.fieldKey, [doc], ListField);
+ props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
}
}
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index b581d3060..7175e2846 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -3,14 +3,18 @@ import { Document } from "../../../fields/Document";
import { ListField } from "../../../fields/ListField";
import React = require("react");
import { KeyStore } from "../../../fields/KeyStore";
-import { FieldWaiting } from "../../../fields/Field";
+import { FieldWaiting, Opt } from "../../../fields/Field";
import { undoBatch } from "../../util/UndoManager";
import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
import { Documents, DocumentOptions } from "../../documents/Documents";
import { Key } from "../../../fields/Key";
import { Transform } from "../../util/Transform";
import { CollectionView } from "./CollectionView";
+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";
export interface CollectionViewProps {
fieldKey: Key;
@@ -24,13 +28,16 @@ export interface CollectionViewProps {
panelHeight: () => number;
focus: (doc: Document) => void;
}
+
export interface SubCollectionViewProps extends CollectionViewProps {
active: () => boolean;
- addDocument: (doc: Document) => void;
+ addDocument: (doc: Document, allowDuplicates: boolean) => void;
removeDocument: (doc: Document) => boolean;
CollectionView: CollectionView;
}
+export type CursorEntry = TupleField<[string, string], [number, number]>;
+
export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
private dropDisposer?: DragManager.DragDropDisposer;
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -42,52 +49,83 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
}
}
+ @action
+ protected setCursorPosition(position: [number, number]) {
+ let ind;
+ let doc = this.props.Document;
+ let id = CurrentUserUtils.id;
+ 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);
+ }
+ })
+
+
+ }
+ }
+
+ 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) {
- const docView: DocumentView = de.data["documentView"];
- const doc: Document = de.data["document"];
- if (docView && (!docView.props.ContainingCollectionView || docView.props.ContainingCollectionView !== this.props.CollectionView)) {
- if (docView.props.RemoveDocument) {
- docView.props.RemoveDocument(docView.props.Document);
+ 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.removeDocument(this.props.CollectionView);
}
- this.props.addDocument(docView.props.Document);
- e.stopPropagation();
- } else if (doc) {
- this.props.removeDocument(doc);
- this.props.addDocument(doc);
+ this.props.addDocument(de.data.droppedDocument, false);
e.stopPropagation();
}
}
@action
protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
- e.stopPropagation()
- e.preventDefault()
let that = this;
let html = e.dataTransfer.getData("text/html");
let text = e.dataTransfer.getData("text/plain");
+
+ if (text && text.startsWith("<div")) {
+ return;
+ }
+ e.stopPropagation()
+ e.preventDefault()
+
if (html && html.indexOf("<img") != 0) {
console.log("not good");
let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 });
htmlDoc.SetText(KeyStore.DocumentText, text);
- this.props.addDocument(htmlDoc);
+ this.props.addDocument(htmlDoc, false);
return;
}
console.log(e.dataTransfer.items.length);
for (let i = 0; i < e.dataTransfer.items.length; i++) {
- const upload = window.location.origin + "/upload";
+ const upload = window.location.origin + RouteStore.upload;
let item = e.dataTransfer.items[i];
if (item.kind === "string" && item.type.indexOf("uri") != -1) {
- e.dataTransfer.items[i].getAsString(action((s: string) => this.props.addDocument(Documents.WebDocument(s, options))))
+ e.dataTransfer.items[i].getAsString(action((s: string) => this.props.addDocument(Documents.WebDocument(s, options), false)))
}
let type = item.type
console.log(type)
if (item.kind == "file") {
- let fReader = new FileReader()
let file = item.getAsFile();
let formData = new FormData()
@@ -109,7 +147,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
var doc: any;
if (type.indexOf("image") !== -1) {
- doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, })
+ doc = Documents.ImageDocument(path, { ...options, nativeWidth: 200, width: 200, })
}
if (type.indexOf("video") !== -1) {
doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, })
diff --git a/src/client/views/collections/MarqueeView.scss b/src/client/views/collections/MarqueeView.scss
new file mode 100644
index 000000000..6d9a79344
--- /dev/null
+++ b/src/client/views/collections/MarqueeView.scss
@@ -0,0 +1,8 @@
+
+.marqueeView {
+ border-style: dashed;
+ box-sizing: border-box;
+ position: absolute;
+ border-width: 1px;
+ border-color: black;
+} \ No newline at end of file
diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx
new file mode 100644
index 000000000..8c2f3443c
--- /dev/null
+++ b/src/client/views/collections/MarqueeView.tsx
@@ -0,0 +1,175 @@
+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
new file mode 100644
index 000000000..a797411f6
--- /dev/null
+++ b/src/client/views/collections/PreviewCursor.scss
@@ -0,0 +1,18 @@
+
+.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
new file mode 100644
index 000000000..cbf36cf9e
--- /dev/null
+++ b/src/client/views/collections/PreviewCursor.tsx
@@ -0,0 +1,73 @@
+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/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index f7d89843d..6daf15f5f 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -18,7 +18,7 @@ export class AudioBox extends React.Component<FieldViewProps> {
super(props);
}
-
+
componentDidMount() {
}
@@ -26,16 +26,16 @@ export class AudioBox extends React.Component<FieldViewProps> {
componentWillUnmount() {
}
-
+
render() {
let field = this.props.doc.Get(this.props.fieldKey)
- let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3":
+ let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3" :
field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3";
-
+
return (
<div>
- <audio controls className = "audiobox-cont">
- <source src = {path} type="audio/mpeg"/>
+ <audio controls className="audiobox-cont">
+ <source src={path} type="audio/mpeg" />
Not supported.
</audio>
</div>
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
new file mode 100644
index 000000000..ce72ab64b
--- /dev/null
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -0,0 +1,57 @@
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+import { FieldWaiting } from "../../../fields/Field";
+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 { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionSchemaView } from "../collections/CollectionSchemaView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import { AudioBox } from "./AudioBox";
+import { DocumentViewProps, JsxBindings } from "./DocumentView";
+import "./DocumentView.scss";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { ImageBox } from "./ImageBox";
+import { KeyValueBox } from "./KeyValueBox";
+import { PDFBox } from "./PDFBox";
+import { VideoBox } from "./VideoBox";
+import { WebBox } from "./WebBox";
+import React = require("react");
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+
+
+@observer
+export class DocumentContentsView extends React.Component<DocumentViewProps & {
+ isSelected: () => boolean,
+ select: (ctrl: boolean) => void,
+ layoutKey: Key
+}> {
+ @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "<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>()); }
+
+ CreateBindings(): JsxBindings {
+ let bindings: JsxBindings = { ...this.props, };
+ for (const key of this.layoutKeys) {
+ bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
+ }
+ for (const key of this.layoutFields) {
+ let field = this.props.Document.Get(key);
+ bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
+ }
+ return bindings;
+ }
+
+ render() {
+ return <JsxParser
+ components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }}
+ bindings={this.CreateBindings()}
+ jsx={this.layout}
+ showWarnings={true}
+ onError={(test: any) => { console.log(test) }}
+ />
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index ab913897b..85a115f1c 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,23 +1,23 @@
+@import "../global_variables";
.documentView-node {
- position: absolute;
- background: #cdcdcd;
- //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);
- }
-} \ No newline at end of file
+ position: absolute;
+ 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);
+ }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ea39e2ac0..9e34b2b60 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,38 +1,29 @@
-import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
-import { Field, FieldWaiting, Opt } from "../../../fields/Field";
+import { Field, Opt } from "../../../fields/Field";
import { Key } from "../../../fields/Key";
import { KeyStore } from "../../../fields/KeyStore";
import { ListField } from "../../../fields/ListField";
+import { TextField } from "../../../fields/TextField";
+import { Utils } from "../../../Utils";
+import { Documents } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
-import { CollectionSchemaView } from "../collections/CollectionSchemaView";
import { CollectionView, CollectionViewType } from "../collections/CollectionView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
import { ContextMenu } from "../ContextMenu";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
-import { ImageBox } from "../nodes/ImageBox";
-import { VideoBox } from "../nodes/VideoBox";
-import { AudioBox } from "../nodes/AudioBox";
-import { Documents } from "../../documents/Documents"
-import { KeyValueBox } from "./KeyValueBox"
-import { WebBox } from "../nodes/WebBox";
-import { PDFBox } from "../nodes/PDFBox";
+import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { TextField } from "../../../fields/TextField";
-import { DocumentManager } from "../../util/DocumentManager";
-const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView>;
Document: Document;
- AddDocument?: (doc: Document) => void;
+ AddDocument?: (doc: Document, allowDuplicates: boolean) => void;
RemoveDocument?: (doc: Document) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
@@ -84,10 +75,20 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
return args;
}
+export interface JsxBindings {
+ Document: Document;
+ isSelected: () => boolean;
+ select: (isCtrlPressed: boolean) => void;
+ isTopMost: boolean;
+ SelectOnLoad: boolean;
+ [prop: string]: any;
+}
+
+
+
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
- private _documentBindings: any = null;
private _downX: number = 0;
private _downY: number = 0;
private _reactionDisposer: Opt<IReactionDisposer>;
@@ -102,7 +103,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
this._downY = e.clientY;
if (e.shiftKey && e.buttons === 2) {
if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY);
+ this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey);
}
else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e);
e.stopPropagation();
@@ -159,18 +160,23 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
}
- startDragging(x: number, y: number) {
+ startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
if (this._mainCont.current) {
const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- let dragData: { [id: string]: any } = {};
- dragData["documentView"] = this;
- dragData["xOffset"] = x - left;
- dragData["yOffset"] = y - top;
- DragManager.StartDrag(this._mainCont.current, dragData, {
+ 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) {
+ this.props.RemoveDocument(this.props.Document);
+ }
+ }
+ DragManager.StartDocumentDrag(this._mainCont.current, dragData, {
handlers: {
dragComplete: action(() => { }),
},
- hideSource: true
+ hideSource: !dropAliasOfDraggedDoc
})
}
}
@@ -183,7 +189,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
document.removeEventListener("pointermove", this.onPointerMove)
document.removeEventListener("pointerup", this.onPointerUp);
if (!this.topMost || e.buttons == 2 || e.altKey) {
- this.startDragging(e.x, e.y);
+ this.startDragging(e.x, e.y, e.ctrlKey || e.altKey);
}
}
e.stopPropagation();
@@ -197,6 +203,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
SelectionManager.SelectDoc(this, e.ctrlKey);
}
}
+ stopPropogation = (e: React.SyntheticEvent) => {
+ e.stopPropagation();
+ }
deleteClicked = (): void => {
if (this.props.RemoveDocument) {
@@ -206,7 +215,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
fieldsClicked = (e: React.MouseEvent): void => {
if (this.props.AddDocument) {
- this.props.AddDocument(Documents.KVPDocument(this.props.Document));
+ this.props.AddDocument(Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }), false);
}
}
fullScreenClicked = (e: React.MouseEvent): void => {
@@ -225,28 +234,39 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- console.log("drop");
- const sourceDocView: DocumentView = de.data["linkSourceDoc"];
- if (!sourceDocView) {
- return;
- }
- let sourceDoc: Document = sourceDocView.props.Document;
- let destDoc: Document = this.props.Document;
- if (this.props.isTopMost) {
- return;
- }
- let linkDoc: Document = new Document();
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc: Document = de.data.linkSourceDocumentView.props.Document;
+ let destDoc: Document = this.props.Document;
+ if (this.props.isTopMost) {
+ return;
+ }
+ 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"));
+ 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);
+ 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);
- e.stopPropagation();
+ e.stopPropagation();
+ }
+ }
+
+ onDrop = (e: React.DragEvent) => {
+ if (e.isDefaultPrevented()) {
+ return;
+ }
+ let text = e.dataTransfer.getData("text/plain");
+ if (text && text.startsWith("<div")) {
+ let oldLayout = this.props.Document.GetText(KeyStore.Layout, "");
+ let layout = text.replace("{layout}", oldLayout);
+ this.props.Document.SetText(KeyStore.Layout, layout);
+ e.stopPropagation();
+ e.preventDefault();
+ }
}
@action
@@ -263,6 +283,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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 ID",
+ event: () => {
+ Utils.CopyText(this.props.Document.Id);
+ }
+ });
//ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) })
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
if (!this.topMost) {
@@ -275,15 +301,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
SelectionManager.SelectDoc(this, e.ctrlKey);
}
- get mainContent() {
- return <JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }}
- bindings={this._documentBindings}
- jsx={this.layout}
- showWarnings={true}
- onError={(test: any) => { console.log(test) }}
- />
- }
isSelected = () => {
return SelectionManager.IsSelected(this);
@@ -294,40 +311,31 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
render() {
- if (!this.props.Document) return <div></div>
+ if (!this.props.Document) {
+ return (null);
+ }
let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
if (!lkeys || lkeys === "<Waiting>") {
return <p>Error loading layout keys</p>;
}
- this._documentBindings = {
- ...this.props,
- isSelected: this.isSelected,
- select: this.select,
- focus: this.props.focus
- };
- for (const key of this.layoutKeys) {
- this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
- }
- for (const key of this.layoutFields) {
- let field = this.props.Document.Get(key);
- this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
- }
- this._documentBindings.bindings = this._documentBindings;
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} >
- {this.mainContent}
- </div>
+ <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/FieldTextBox.scss b/src/client/views/nodes/FieldTextBox.scss
index b6ce2fabc..d2cd61b0d 100644
--- a/src/client/views/nodes/FieldTextBox.scss
+++ b/src/client/views/nodes/FieldTextBox.scss
@@ -1,14 +1,14 @@
.ProseMirror {
- margin-top: -1em;
- width: 100%;
- height: 100%;
+ margin-top: -1em;
+ width: 100%;
+ height: 100%;
}
.ProseMirror:focus {
- outline: none !important
+ outline: none !important;
}
.fieldTextBox-cont {
- background: white;
- padding: 1vw;
-} \ No newline at end of file
+ background: white;
+ padding: 1vw;
+}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 49f4cefce..e84c5f933 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -16,6 +16,7 @@ import { WebBox } from "./WebBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
import { AudioField } from "../../../fields/AudioField";
+import { ListField } from "../../../fields/ListField";
//
@@ -60,12 +61,20 @@ export class FieldView extends React.Component<FieldViewProps> {
}
else if (field instanceof WebField) {
return <WebBox {...this.props} />
- }
- else if (field instanceof VideoField){
- return <VideoBox {...this.props}/>
}
- else if (field instanceof AudioField){
- return <AudioBox {...this.props}/>
+ else if (field instanceof VideoField) {
+ return <VideoBox {...this.props} />
+ }
+ else if (field instanceof AudioField) {
+ return <AudioBox {...this.props} />
+ } else if (field instanceof Document) {
+ return <div>{field.Title}</div>
+ } else if (field instanceof ListField) {
+ return (<div>
+ {(field as ListField<Field>).Data.map(f => {
+ return f instanceof Document ? f.Title : f.GetValue().toString();
+ }).join(", ")}
+ </div>)
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
// else if (field instanceof HtmlField) {
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index ab5849f09..32da2632e 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,38 +1,46 @@
+@import "../global_variables";
.ProseMirror {
- width: 100%;
- height: auto;
- min-height: 100%
+ width: 100%;
+ height: auto;
+ min-height: 100%;
+ font-family: $serif;
}
.ProseMirror:focus {
- outline: none !important
+ outline: none !important;
}
.formattedTextBox-cont {
- background: white;
- padding: 1;
- border-width: 1px;
- border-radius: 2px;
- border-color:black;
- box-sizing: border-box;
- background: white;
- border-style:solid;
- overflow-y: scroll;
- overflow-x: hidden;
- color: initial;
- height: 100%;
+ background: $light-color-secondary;
+ padding: 0.9em;
+ border-width: 0px;
+ border-radius: $border-radius;
+ border-color: $intermediate-color;
+ box-sizing: border-box;
+ border-style: solid;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ color: initial;
+ height: 100%;
}
.menuicon {
- display: inline-block;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
- color: #888;
- line-height: 1;
- padding: 0 7px;
- margin: 1px;
- cursor: pointer;
- text-align: center;
- min-width: 1.4em;
- }
- .strong, .heading { font-weight: bold; }
- .em { font-style: italic; } \ No newline at end of file
+ display: inline-block;
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+ color: #888;
+ line-height: 1;
+ padding: 0 7px;
+ margin: 1px;
+ cursor: pointer;
+ text-align: center;
+ min-width: 1.4em;
+}
+
+.strong,
+.heading {
+ font-weight: bold;
+}
+
+.em {
+ font-style: italic;
+}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5c7aaf9fe..4bd5726f4 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -14,6 +14,9 @@ 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");
@@ -31,7 +34,7 @@ import { ContextMenu } from "../../views/ContextMenu";
// 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 tot his node,
+// 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> {
@@ -52,7 +55,9 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField);
+ const { doc, fieldKey } = this.props;
+ doc.SetOnPrototype(fieldKey, new RichTextField(JSON.stringify(state.toJSON())))
+ // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
}
}
@@ -60,6 +65,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
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 }),
@@ -110,7 +116,9 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
@action
onChange(e: React.ChangeEvent<HTMLInputElement>) {
- this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField);
+ 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) {
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 31452bc85..487038841 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,12 +1,11 @@
-
.imageBox-cont {
- padding: 0vw;
- position: relative;
- text-align: center;
- width: 100%;
- height: auto;
- max-width: 100%;
- max-height: 100%
+ padding: 0vw;
+ position: relative;
+ text-align: center;
+ width: 100%;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%;
}
.imageBox-cont img {
@@ -15,8 +14,8 @@
}
.imageBox-button {
- padding : 0vw;
- border: none;
- width : 100%;
- height: 100%;
-} \ No newline at end of file
+ padding: 0vw;
+ border: none;
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 30910fb1f..2db0cc4e2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,5 @@
-import { action, observable } from 'mobx';
+import { action, observable, trace } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
@@ -10,6 +10,7 @@ import { ContextMenu } from "../../views/ContextMenu";
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react")
+import { Utils } from '../../../Utils';
@observer
export class ImageBox extends React.Component<FieldViewProps> {
@@ -70,7 +71,7 @@ export class ImageBox extends React.Component<FieldViewProps> {
}
lightbox = (path: string) => {
- const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"];
+ const images = [path];
if (this._isOpen && this.props.isSelected()) {
return (<Lightbox
mainSrc={images[this._photoIndex]}
@@ -89,12 +90,16 @@ export class ImageBox extends React.Component<FieldViewProps> {
}
}
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- imageCapability = (e: React.MouseEvent): void => {
- }
-
specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability });
+ let field = this.props.doc.GetT(this.props.fieldKey, ImageField);
+ if (field && field !== FieldWaiting) {
+ let url = field.Data.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => {
+ Utils.CopyText(url)
+ }
+ });
+ }
}
render() {
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 1295266e5..63ae75424 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -1,31 +1,57 @@
+@import "../global_variables";
.keyValueBox-cont {
- overflow-y:scroll;
+ overflow-y: scroll;
height: 100%;
- border: black;
- border-width: 1px;
- border-style: solid;
+ background-color: $light-color;
+ border: 1px solid $intermediate-color;
+ border-radius: $border-radius;
box-sizing: border-box;
display: inline-block;
.imageBox-cont img {
- max-height:45px;
+ max-height: 45px;
height: auto;
}
+ td {
+ padding: 6px 8px;
+ border-right: 1px solid $intermediate-color;
+ border-top: 1px solid $intermediate-color;
+ &:last-child {
+ border-right: none;
+ }
+ }
}
+
.keyValueBox-table {
position: relative;
+ border-collapse: collapse;
}
+
.keyValueBox-header {
- background:gray;
+ background: $intermediate-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 12px;
+ height: 30px;
+ padding-top: 4px;
+ th {
+ font-weight: normal;
+ &:first-child {
+ border-right: 1px solid $light-color;
+ }
+ }
}
+
.keyValueBox-evenRow {
- background: white;
+ background: $light-color;
.formattedTextBox-cont {
- background: white;
+ background: $light-color;
}
}
+
.keyValueBox-oddRow {
- background: lightGray;
+ background: $light-color-secondary;
.formattedTextBox-cont {
- background: lightgray;
+ background: $light-color-secondary;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index ac8c949a9..283c1f732 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,17 +2,62 @@
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Document } from '../../../fields/Document';
-import { FieldWaiting } from '../../../fields/Field';
+import { FieldWaiting, Field } from '../../../fields/Field';
import { KeyStore } from '../../../fields/KeyStore';
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react")
+import { CompileScript, ToField } from "../../util/Scripting";
+import { Key } from '../../../fields/Key';
+import { observable, action } from "mobx";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) }
+ @observable private _keyInput: string = "";
+ @observable private _valueInput: string = "";
+
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ }
+
+
+
+ shouldComponentUpdate() {
+ return false;
+ }
+
+ @action
+ onEnterKey = (e: React.KeyboardEvent): void => {
+ if (e.key == 'Enter') {
+ if (this._keyInput && this._valueInput) {
+ let doc = this.props.doc.GetT(KeyStore.Data, Document);
+ if (!doc || doc == FieldWaiting) {
+ return
+ }
+ let realDoc = doc;
+
+ let script = CompileScript(this._valueInput, undefined, true);
+ if (!script.compiled) {
+ return;
+ }
+ let field = script();
+ if (field instanceof Field) {
+ realDoc.Set(new Key(this._keyInput), field);
+ } else {
+ let dataField = ToField(field);
+ if (dataField) {
+ realDoc.Set(new Key(this._keyInput), dataField);
+ }
+ }
+ this._keyInput = ""
+ this._valueInput = ""
+ }
+ }
+ }
onPointerDown = (e: React.PointerEvent): void => {
if (e.buttons === 1 && this.props.isSelected()) {
@@ -33,7 +78,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let ids: { [key: string]: string } = {};
let protos = doc.GetAllPrototypes();
for (const proto of protos) {
- proto._proxies.forEach((val, key) => {
+ proto._proxies.forEach((val: any, key: string) => {
if (!(key in ids)) {
ids[key] = key;
}
@@ -48,9 +93,26 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return rows;
}
+ @action
+ keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyInput = e.currentTarget.value;
+ }
+
+ @action
+ valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueInput = e.currentTarget.value;
+ }
- render() {
+ newKeyValue = () => {
+ return (
+ <tr>
+ <td><input type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /></td>
+ <td><input type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /></td>
+ </tr>
+ )
+ }
+ render() {
return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}>
<table className="keyValueBox-table">
<tbody>
@@ -59,6 +121,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<th>Fields</th>
</tr>
{this.createTable()}
+ {this.newKeyValue()}
</tbody>
</table>
</div>)
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index a97e98313..111f85a05 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -8,6 +8,8 @@ import { observable, action } from 'mobx';
import { Document } from '../../../fields/Document';
import { Key } from '../../../fields/Key';
import { Server } from "../../Server"
+import { EditableView } from "../EditableView";
+import { CompileScript, ToField } from "../../util/Scripting";
// Represents one row in a key value plane
@@ -48,10 +50,37 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
bindings: {},
selectOnLoad: false,
}
+ let contents = (
+ <FieldView {...props} />
+ );
return (
<tr className={this.props.rowStyle}>
<td>{this.key.Name}</td>
- <td><FieldView {...props} /></td>
+ <td><EditableView contents={contents} height={36} GetValue={() => {
+ let field = props.doc.Get(props.fieldKey);
+ if (field && field instanceof Field) {
+ return field.ToScriptString();
+ }
+ return field || "";
+ }}
+ SetValue={(value: string) => {
+ let script = CompileScript(value, undefined, true);
+ 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 false;
+ }}></EditableView></td>
</tr>
)
}
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 00e5ebb3d..5d5f782d2 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,13 +1,14 @@
+@import "../global_variables";
.link-container {
width: 100%;
- height: 30px;
+ height: 35px;
display: flex;
flex-direction: row;
border-top: 0.5px solid #bababa;
}
.info-container {
- width: 60%;
+ width: 55%;
padding-top: 5px;
padding-left: 5px;
display: flex;
@@ -23,17 +24,42 @@
}
.button-container {
- width: 40%;
+ width: 45%;
display: flex;
flex-direction: row;
}
.button {
- height: 15px;
- width: 15px;
- margin: 8px 5px;
+ height: 20px;
+ width: 20px;
+ margin: 8px 4px;
border-radius: 50%;
- opacity: 0.6;
+ opacity: 0.9;
pointer-events: auto;
- background-color: #2B6091;
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 60%;
+ transition: transform 0.2s;
+}
+
+.button:hover {
+ background: $main-accent;
+ cursor: pointer;
+}
+
+.fa-icon-view {
+ margin-left: 3px;
+ margin-top: 5px;
+}
+
+.fa-icon-edit {
+ margin-left: 5px;
+ margin-top: 5px;
+}
+
+.fa-icon-delete {
+ margin-left: 6px;
+ margin-top: 5px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 69df676ff..dd2f71b59 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -11,6 +11,17 @@ import { ListField } from "../../../fields/ListField";
import { DocumentManager } from "../../util/DocumentManager";
import { LinkEditor } from "./LinkEditor";
import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+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";
+
+
+library.add(faEye);
+library.add(faEdit);
+library.add(faTimes);
interface Props {
linkDoc: Document;
@@ -23,8 +34,8 @@ interface Props {
@observer
export class LinkBox extends React.Component<Props> {
+ @undoBatch
onViewButtonPressed = (e: React.PointerEvent): void => {
- console.log("view down");
e.stopPropagation();
let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
if (docView) {
@@ -79,9 +90,12 @@ export class LinkBox extends React.Component<Props> {
</div>
<div className="button-container">
- <div className="button" onPointerDown={this.onViewButtonPressed}></div>
- <div className="button" onPointerDown={this.onEditButtonPressed}></div>
- <div className="button" onPointerDown={this.onDeleteButtonPressed}></div>
+ <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div>
+ <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
+ <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
</div>
</div>
)
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index cb191dc8c..fb0c69cff 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,3 +1,4 @@
+@import "../global_variables";
.edit-container {
width: 100%;
height: auto;
@@ -9,21 +10,34 @@
margin-bottom: 10px;
padding: 5px;
font-size: 12px;
+ border: 1px solid #bababa;
}
.description-input {
- font-size: 12px;
+ font-size: 11px;
padding: 5px;
margin-bottom: 10px;
+ border: 1px solid #bababa;
}
.save-button {
width: 50px;
height: 20px;
- background-color: #2B6091;
+ pointer-events: auto;
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ padding: 2px;
+ font-size: 10px;
margin: 0 auto;
- color: white;
+ transition: transform 0.2s;
text-align: center;
line-height: 20px;
- font-size: 12px;
+}
+
+.save-button:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index a120ab2a7..dedcce6ef 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -10,6 +10,7 @@
padding: 5px;
margin-bottom: 10px;
font-size: 12px;
+ border: 1px solid #bababa;
}
#linkMenu-list {
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 5c6b06d00..5eeb40772 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -39,8 +39,8 @@ export class LinkMenu extends React.Component<Props> {
<div id="linkMenu-container">
<input id="linkMenu-searchBar" type="text" placeholder="Search..."></input>
<div id="linkMenu-list">
- {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Source: ")}
- {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Destination: ")}
+ {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Destination: ")}
+ {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Source: ")}
</div>
</div>
)
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 9f92410d4..ad947afd5 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -11,5 +11,5 @@
}
.pdfBox-contentContainer {
position: absolute;
- transform-origin: "left top";
+ transform-origin: left top;
} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 368a80b8e..3a0ef2d32 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 } from 'mobx';
+import { action, computed, observable, reaction, IReactionDisposer, trace } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
import Measure from "react-measure";
@@ -17,6 +17,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";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -86,7 +87,7 @@ 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, 0); }
+ @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
componentDidMount() {
this._reactionDisposer = reaction(
@@ -96,7 +97,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
this.saveThumbnail();
this._interactive = true;
} else {
- if (this.curPage)
+ if (this.curPage > 0)
this.initPage = true;
}
},
@@ -441,7 +442,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
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 + "/corsProxy/" + `${pdfUrl}`}>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`}>
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
<div className="pdfBox-page" ref={measureRef}>
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 7306450d9..76bbeb37c 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,4 @@
.videobox-cont{
width: 100%;
- height: 100%;
+ height: Auto;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 22ff5c5ad..09ae95183 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,12 +1,13 @@
import React = require("react")
-import { FieldViewProps, FieldView } from './FieldView';
+import { observer } from "mobx-react";
import { FieldWaiting } from '../../../fields/Field';
-import { observer } from "mobx-react"
-import { VideoField } from '../../../fields/VideoField';
-import "./VideoBox.scss"
-import { ContextMenu } from "../../views/ContextMenu";
-import { observable, action } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
+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 { KeyStore } from "../../../fields/KeyStore";
+import { number } from "prop-types";
@observer
export class VideoBox extends React.Component<FieldViewProps> {
@@ -17,27 +18,46 @@ export class VideoBox extends React.Component<FieldViewProps> {
super(props);
}
-
- componentDidMount() {
- }
+ _loaded: boolean = false;
- componentWillUnmount() {
+ @action
+ setScaling = (r: any) => {
+ if (this._loaded) {
+ // bcz: the nativeHeight should really be set when the document is imported.
+ // also, the native dimensions could be different for different pages of the PDF
+ // so this design is flawed.
+ var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0);
+ var nativeHeight = this.props.doc.GetNumber(KeyStore.NativeHeight, 0);
+ var newNativeHeight = nativeWidth * r.entry.height / r.entry.width;
+ if (!nativeHeight && newNativeHeight != nativeHeight && !isNaN(newNativeHeight)) {
+ this.props.doc.SetNumber(KeyStore.Height, newNativeHeight / nativeWidth * this.props.doc.GetNumber(KeyStore.Width, 0));
+ this.props.doc.SetNumber(KeyStore.NativeHeight, newNativeHeight);
+ }
+ } else {
+ this._loaded = true;
+ }
}
-
+
+
render() {
- let field = this.props.doc.Get(this.props.fieldKey)
- let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4":
- field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4";
-
+ let field = this.props.doc.GetT(this.props.fieldKey, VideoField);
+ if (!field || field === FieldWaiting) {
+ return <div>Loading</div>
+ }
+ let path = field.Data.href;
+
+ //setTimeout(action(() => this._loaded = true), 500);
return (
- <div>
- <video width = {200} height = {200} controls className = "videobox-cont">
- <source src = {path} type = "video/mp4"/>
- Not supported.
- </video>
- </div>
+ <Measure onResize={this.setScaling}>
+ {({ measureRef }) =>
+ <video className="videobox-cont" onClick={() => { }} ref={measureRef}>
+ <source src={path} type="video/mp4" />
+ Not supported.
+ </video>
+ }
+ </Measure>
)
}
} \ No newline at end of file
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 7bc70615f..c8de33f41 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,46 +1,29 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
+import JsxParser from 'react-jsx-parser'
-class TestInternal extends React.Component {
- onContextMenu = (e: React.MouseEvent) => {
- console.log("Internal");
- e.stopPropagation();
- }
-
- onPointerDown = (e: React.MouseEvent) => {
- console.log("pointer down")
- e.preventDefault();
- }
-
- render() {
- return <div onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown}
- onPointerUp={this.onPointerDown}>Hello world</div>
- }
-}
-
-class TestChild extends React.Component {
- onContextMenu = () => {
- console.log("Child");
- }
-
+class Hello extends React.Component<{ firstName: string, lastName: string }> {
render() {
- return <div onContextMenu={this.onContextMenu}><TestInternal /></div>
+ return <div>Hello {this.props.firstName} {this.props.lastName}</div>
}
}
-class TestParent extends React.Component {
- onContextMenu = () => {
- console.log("Parent");
- }
-
+class Test extends React.Component {
render() {
- return <div onContextMenu={this.onContextMenu}><TestChild /></div>
+ let jsx = "<Hello {...props}/>";
+ let bindings = {
+ props: {
+ firstName: "First",
+ lastName: "Last"
+ }
+ }
+ return <JsxParser jsx={jsx} bindings={bindings} components={{ Hello }}></JsxParser>
}
}
ReactDOM.render((
<div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <TestParent />
+ <Test />
</div>),
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 780e9f8f2..7fdd77bf3 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -137,11 +137,13 @@ class DebugViewer extends React.Component<{ fieldId: string }> {
content = (<FieldViewer field={this.field} />)
} else if (this.field instanceof Key) {
content = (<KeyViewer field={this.field} />)
+ } else {
+ content = (<span>Unrecognized field type</span>)
}
} else if (this.error) {
content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>
} else {
- content = <>Field loading</>
+ content = <span>Field loading: {this.props.fieldId}</span>
}
return content;
}
diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts
index aefcc15c1..8864471ae 100644
--- a/src/fields/AudioField.ts
+++ b/src/fields/AudioField.ts
@@ -10,8 +10,8 @@ export class AudioField extends BasicField<URL> {
toString(): string {
return this.Data.href;
}
-
-
+
+
ToScriptString(): string {
return `new AudioField("${this.Data}")`;
}
@@ -20,10 +20,10 @@ export class AudioField extends BasicField<URL> {
return new AudioField(this.Data);
}
- ToJson(): { type: Types, data: URL, _id: string } {
+ ToJson(): { type: Types, data: string, _id: string } {
return {
type: Types.Audio,
- data: this.Data,
+ data: this.Data.href,
_id: this.Id
}
}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index 25e239417..be0137128 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -2,7 +2,7 @@ import { Key } from "./Key"
import { KeyStore } from "./KeyStore";
import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field"
import { NumberField } from "./NumberField";
-import { ObservableMap, computed, action } from "mobx";
+import { ObservableMap, computed, action, runInAction } from "mobx";
import { TextField } from "./TextField";
import { ListField } from "./ListField";
import { Server } from "../client/Server";
@@ -11,6 +11,7 @@ import { UndoManager } from "../client/util/UndoManager";
import { HtmlField } from "./HtmlField";
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 _proxies: ObservableMap<string, FieldId> = new ObservableMap();
@@ -38,6 +39,11 @@ export class Document extends Field {
return this.GetText(KeyStore.Title, "<untitled>");
}
+ @computed
+ public get Fields() {
+ return this.fields;
+ }
+
/**
* 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
@@ -119,9 +125,11 @@ export class Document extends 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 {
- //TODO: This should probably check if this.fields contains the key before calling Server.GetDocumentField
- //This currently doesn't deal with prototypes
- if (this._proxies.has(key.Id)) {
+ //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;
}
@@ -207,25 +215,37 @@ export class Document extends Field {
}
@action
- Set(key: Key, field: Field | undefined): void {
+ SetOnPrototype(key: Key, field: Field | undefined): void {
+ this.GetAsync(KeyStore.Prototype, (f: Field) => {
+ (f as Document).Set(key, field)
+ })
+ }
+
+ @action
+ Set(key: Key, field: Field | undefined, setOnPrototype = false): void {
let old = this.fields.get(key.Id);
let oldField = old ? old.field : undefined;
- if (field) {
- this.fields.set(key.Id, { key, field });
- this._proxies.set(key.Id, field.Id)
- // Server.AddDocumentField(this, key, field);
- } else {
- this.fields.delete(key.Id);
- this._proxies.delete(key.Id)
- // Server.DeleteDocumentField(this, key);
+ if (setOnPrototype) {
+ this.SetOnPrototype(key, field)
+ }
+ else {
+ if (field) {
+ this.fields.set(key.Id, { key, field });
+ this._proxies.set(key.Id, field.Id)
+ // Server.AddDocumentField(this, key, field);
+ } else {
+ this.fields.delete(key.Id);
+ this._proxies.delete(key.Id)
+ // Server.DeleteDocumentField(this, key);
+ }
+ Server.UpdateField(this);
}
if (oldField || field) {
UndoManager.AddEvent({
- undo: () => this.Set(key, oldField),
- redo: () => this.Set(key, field)
+ undo: () => this.Set(key, oldField, setOnPrototype),
+ redo: () => this.Set(key, field, setOnPrototype)
})
}
- Server.UpdateField(this);
}
@action
@@ -265,6 +285,15 @@ export class Document extends Field {
return protos;
}
+ CreateAlias(id?: string): Document {
+ let alias = new Document(id)
+ this.GetAsync(KeyStore.Prototype, (f: Field) => {
+ alias.Set(KeyStore.Prototype, f)
+ })
+
+ return alias
+ }
+
MakeDelegate(id?: string): Document {
let delegate = new Document(id);
@@ -281,6 +310,7 @@ export class Document extends Field {
throw new Error("Method not implemented.");
}
GetValue() {
+ return this.Title;
var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
return title;
//throw new Error("Method not implemented.");
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
index a07326095..7cbdf7e58 100644
--- a/src/fields/HtmlField.ts
+++ b/src/fields/HtmlField.ts
@@ -15,7 +15,7 @@ export class HtmlField extends BasicField<string> {
return new HtmlField(this.Data);
}
- ToJson(): { _id: string; type: Types; data: any; } {
+ ToJson(): { _id: string; type: Types; data: string; } {
return {
type: Types.Html,
data: this.Data,
diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts
index be8d73e68..a9ece7d7b 100644
--- a/src/fields/ImageField.ts
+++ b/src/fields/ImageField.ts
@@ -19,10 +19,10 @@ export class ImageField extends BasicField<URL> {
return new ImageField(this.Data);
}
- ToJson(): { type: Types, data: URL, _id: string } {
+ ToJson(): { type: Types, data: string, _id: string } {
return {
type: Types.Image,
- data: this.Data,
+ data: this.Data.href,
_id: this.Id
}
}
diff --git a/src/fields/KVPField.ts b/src/fields/KVPField.ts
deleted file mode 100644
index a7ecc0768..000000000
--- a/src/fields/KVPField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField"
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-import { Document } from "./Document"
-
-export class KVPField extends BasicField<Document> {
- constructor(data: Document | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data == undefined ? new Document() : data, save, id);
- }
-
- toString(): string {
- return this.Data.Title;
- }
-
- ToScriptString(): string {
- return `new KVPField("${this.Data}")`;
- }
-
- Copy() {
- return new KVPField(this.Data);
- }
-
- ToJson(): { type: Types, data: Document, _id: string } {
- return {
- type: Types.Text,
- data: this.Data,
- _id: this.Id
- }
- }
-} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index f93a68c85..68883d6f1 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -19,11 +19,13 @@ export namespace KeyStore {
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 DocumentText = new Key("DocumentText");
@@ -35,4 +37,8 @@ export namespace KeyStore {
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");
}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
index ce32da0a6..4527ee548 100644
--- a/src/fields/ListField.ts
+++ b/src/fields/ListField.ts
@@ -16,8 +16,13 @@ export class ListField<T extends Field> extends BasicField<T[]> {
this.observeList();
}
+ private _processingServerUpdate: boolean = false;
+
private observeDisposer: Lambda | undefined;
private observeList(): void {
+ if (this.observeDisposer) {
+ this.observeDisposer()
+ }
this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
this.updateProxies()
if (change.type == "splice") {
@@ -31,14 +36,12 @@ export class ListField<T extends Field> extends BasicField<T[]> {
redo: () => this.Data[change.index] = change.newValue
})
}
- Server.UpdateField(this);
+ if (!this._processingServerUpdate)
+ Server.UpdateField(this);
});
}
protected setData(value: T[]) {
- if (this.observeDisposer) {
- this.observeDisposer()
- }
this.data = observable(value);
this.updateProxies();
this.observeList();
@@ -69,30 +72,29 @@ export class ListField<T extends Field> extends BasicField<T[]> {
init(callback: (field: Field) => any) {
Server.GetFields(this._proxies, action((fields: { [index: string]: Field }) => {
- if (!this.arraysEqual(this._proxies, this.Data.map(field => field.Id))) {
+ if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
var dataids = this.data.map(d => d.Id);
- var added = this.data.length == this._proxies.length - 1;
+ var proxies = this._proxies.map(p => p);
+ var added = this.data.length < this._proxies.length;
var deleted = this.data.length > this._proxies.length;
for (let i = 0; i < dataids.length && added; i++)
- added = this._proxies.indexOf(dataids[i]) != -1;
+ added = proxies.indexOf(dataids[i]) != -1;
for (let i = 0; i < this._proxies.length && deleted; i++)
- deleted = dataids.indexOf(this._proxies[i]) != -1;
- if (added) { // if only 1 items was added
- for (let i = 0; i < this._proxies.length; i++)
- if (dataids.indexOf(this._proxies[i]) === -1)
- this.Data.splice(i, 0, fields[this._proxies[i]] as T);
- } else if (deleted) { // if only items were deleted
- for (let i = this.data.length - 1; i >= 0; i--) {
- if (this._proxies.indexOf(this.data[i].Id) === -1) {
- this.Data.splice(i, 1);
- }
- }
- } else // otherwise, just rebuild the whole list
- this.data = this._proxies.map(id => fields[id] as T)
- observe(this.Data, () => {
- this.updateProxies()
- Server.UpdateField(this);
- })
+ deleted = dataids.indexOf(proxies[i]) != -1;
+
+ this._processingServerUpdate = true;
+ for (let i = 0; i < proxies.length && added; i++) {
+ if (dataids.indexOf(proxies[i]) === -1)
+ this.Data.splice(i, 0, fields[proxies[i]] as T);
+ }
+ for (let i = dataids.length - 1; i >= 0 && deleted; i--) {
+ if (proxies.indexOf(dataids[i]) === -1)
+ this.Data.splice(i, 1);
+ }
+ if (!added && !deleted) {// otherwise, just rebuild the whole list
+ this.setData(proxies.map(id => fields[id] as T));
+ }
+ this._processingServerUpdate = false;
}
callback(this);
}))
diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts
index f3a009001..b6625387e 100644
--- a/src/fields/PDFField.ts
+++ b/src/fields/PDFField.ts
@@ -22,10 +22,10 @@ export class PDFField extends BasicField<URL> {
return `new PDFField("${this.Data}")`;
}
- ToJson(): { type: Types, data: URL, _id: string } {
+ ToJson(): { type: Types, data: string, _id: string } {
return {
type: Types.PDF,
- data: this.Data,
+ data: this.Data.href,
_id: this.Id
}
}
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
new file mode 100644
index 000000000..e2162c751
--- /dev/null
+++ b/src/fields/TupleField.ts
@@ -0,0 +1,59 @@
+import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
+import { Server } from "../client/Server";
+import { UndoManager } from "../client/util/UndoManager";
+import { Types } from "../server/Message";
+import { BasicField } from "./BasicField";
+import { Field, FieldId } from "./Field";
+
+export class TupleField<T, U> extends BasicField<[T, U]> {
+ constructor(data: [T, U], id?: FieldId, save: boolean = true) {
+ super(data, save, id);
+ if (save) {
+ Server.UpdateField(this);
+ }
+ this.observeTuple();
+ }
+
+ private observeDisposer: Lambda | undefined;
+ private observeTuple(): void {
+ this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
+ if (change.type === "update") {
+ UndoManager.AddEvent({
+ undo: () => this.Data[change.index] = change.oldValue,
+ redo: () => this.Data[change.index] = change.newValue
+ })
+ Server.UpdateField(this);
+ } else {
+ throw new Error("Why are you messing with the length of a tuple, huh?");
+ }
+ });
+ }
+
+ protected setData(value: [T, U]) {
+ if (this.observeDisposer) {
+ this.observeDisposer()
+ }
+ this.data = observable(value) as (T | U)[] as [T, U];
+ this.observeTuple();
+ }
+
+ UpdateFromServer(values: [T, U]) {
+ this.setData(values);
+ }
+
+ ToScriptString(): string {
+ return `new TupleField([${this.Data[0], this.Data[1]}])`;
+ }
+
+ Copy(): Field {
+ return new TupleField<T, U>(this.Data);
+ }
+
+ ToJson(): { type: Types, data: [T, U], _id: string } {
+ return {
+ type: Types.Tuple,
+ data: this.Data,
+ _id: this.Id
+ }
+ }
+} \ No newline at end of file
diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts
index 5f4ae19bf..626e4ec83 100644
--- a/src/fields/VideoField.ts
+++ b/src/fields/VideoField.ts
@@ -19,10 +19,10 @@ export class VideoField extends BasicField<URL> {
return new VideoField(this.Data);
}
- ToJson(): { type: Types, data: URL, _id: string } {
+ ToJson(): { type: Types, data: string, _id: string } {
return {
type: Types.Video,
- data: this.Data,
+ data: this.Data.href,
_id: this.Id
}
}
diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts
index 8f945d686..6c4de5000 100644
--- a/src/fields/WebField.ts
+++ b/src/fields/WebField.ts
@@ -19,10 +19,10 @@ export class WebField extends BasicField<URL> {
return new WebField(this.Data);
}
- ToJson(): { type: Types, data: URL, _id: string } {
+ ToJson(): { type: Types, data: string, _id: string } {
return {
type: Types.Web,
- data: this.Data,
+ data: this.Data.href,
_id: this.Id
}
}
diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss
new file mode 100644
index 000000000..d0b7d4e41
--- /dev/null
+++ b/src/mobile/ImageUpload.scss
@@ -0,0 +1,13 @@
+.imgupload_cont {
+ height: 100vh;
+ width: 100vw;
+ align-content: center;
+ .button_file {
+ text-align: center;
+ height: 50%;
+ width: 50%;
+ background-color: paleturquoise;
+ color: grey;
+ font-size: 3em;
+ }
+} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
new file mode 100644
index 000000000..16808a598
--- /dev/null
+++ b/src/mobile/ImageUpload.tsx
@@ -0,0 +1,82 @@
+import * as ReactDOM from 'react-dom';
+import React = require('react');
+import "./ImageUpload.scss"
+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';
+
+
+
+
+// const onPointerDown = (e: React.TouchEvent) => {
+// let imgInput = document.getElementById("input_image_file");
+// if (imgInput) {
+// imgInput.click();
+// }
+// }
+
+const onFileLoad = (file: any) => {
+ let imgPrev = document.getElementById("img_preview")
+ if (imgPrev) {
+ let files: File[] = file.target.files;
+ if (files.length != 0) {
+ console.log(files[0]);
+ let formData = new FormData();
+ formData.append("file", files[0]);
+
+ const upload = window.location.origin + "/upload"
+ fetch(upload, {
+ method: 'POST',
+ body: formData
+ }).then((res: Response) => {
+ return res.json()
+ }).then(json => {
+ 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;
+ }
+ })
+ 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);
+ })
+ }
+ })
+ }
+ }
+ );
+ }
+ })
+ // console.log(window.location.origin + file[0])
+
+ //imgPrev.setAttribute("src", window.location.origin + files[0].name)
+ })
+ })
+ }
+ }
+}
+
+ReactDOM.render((
+ <div className="imgupload_cont">
+ {/* <button className = "button_file" = {onPointerDown}> Open Image </button> */}
+ <input type="file" accept="image/*" onChange={onFileLoad} className="input_file" id="input_image_file"></input>
+ <img id="img_preview" src=""></img>
+ <div id="message" />
+ </div>),
+ document.getElementById('root')
+); \ No newline at end of file
diff --git a/src/fields/KVPField b/src/mobile/InkControls.tsx
index e69de29bb..e69de29bb 100644
--- a/src/fields/KVPField
+++ b/src/mobile/InkControls.tsx
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 8a00f6b59..a2d1ab829 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -45,7 +45,7 @@ export class GetFieldArgs {
}
export enum Types {
- Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF
+ Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF, Tuple
}
export class DocumentTransfer implements Transferable {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
new file mode 100644
index 000000000..fb06b878b
--- /dev/null
+++ b/src/server/RouteStore.ts
@@ -0,0 +1,33 @@
+// PREPEND ALL ROUTES WITH FORWARD SLASHES!
+
+export enum RouteStore {
+ // GENERAL
+ root = "/",
+ home = "/home",
+ corsProxy = "/corsProxy",
+ delete = "/delete",
+ deleteAll = "/deleteAll",
+
+ // UPLOAD AND STATIC FILE SERVING
+ public = "/public",
+ upload = "/upload",
+ images = "/images",
+
+ // USER AND WORKSPACES
+ getCurrUser = "/getCurrentUser",
+ addWorkspace = "/addWorkspaceId",
+ getAllWorkspaces = "/getAllWorkspaceIds",
+ getActiveWorkspace = "/getActiveWorkspaceId",
+ setActiveWorkspace = "/setActiveWorkspaceId",
+ updateCursor = "/updateCursor",
+
+ openDocumentWithId = "/doc/:docId",
+
+ // AUTHENTICATION
+ signup = "/signup",
+ login = "/login",
+ logout = "/logout",
+ forgot = "/forgotpassword",
+ reset = "/reset/:token",
+
+} \ No newline at end of file
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index 5331c9e30..f10f82deb 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -14,13 +14,16 @@ 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 { InkField } from '../fields/InkField';
+import { PDFField } from '../fields/PDFField';
+import { TupleField } from '../fields/TupleField';
export class ServerUtils {
+ 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
@@ -55,6 +58,8 @@ export class ServerUtils {
return new AudioField(new URL(data), id, false)
case Types.Video:
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:
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 05f6c3133..b6fe15655 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -2,8 +2,9 @@ import * as passport from 'passport'
import * as passportLocal from 'passport-local';
import * as mongodb from 'mongodb';
import * as _ from "lodash";
-import { default as User } from '../models/User';
+import { default as User } from '../models/user_model';
import { Request, Response, NextFunction } from "express";
+import { RouteStore } from '../../RouteStore';
const LocalStrategy = passportLocal.Strategy;
@@ -18,7 +19,7 @@ passport.deserializeUser<any, any>((id, done) => {
});
// AUTHENTICATE JUST WITH EMAIL AND PASSWORD
-passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
+passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }, (req, email, password, done) => {
User.findOne({ email: email.toLowerCase() }, (error: any, user: any) => {
if (error) return done(error);
if (!user) return done(undefined, false, { message: "Invalid email or password" }) // invalid email
@@ -35,7 +36,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
if (req.isAuthenticated()) {
return next();
}
- return res.redirect("/login");
+ return res.redirect(RouteStore.login);
}
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
diff --git a/src/server/authentication/controllers/WorkspacesMenu.css b/src/server/authentication/controllers/WorkspacesMenu.css
new file mode 100644
index 000000000..b89039965
--- /dev/null
+++ b/src/server/authentication/controllers/WorkspacesMenu.css
@@ -0,0 +1,3 @@
+.ids:hover {
+ color: darkblue;
+} \ No newline at end of file
diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx
new file mode 100644
index 000000000..1533b1e62
--- /dev/null
+++ b/src/server/authentication/controllers/WorkspacesMenu.tsx
@@ -0,0 +1,96 @@
+import * as React from 'react';
+import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx';
+import { observer } from "mobx-react";
+import './WorkspacesMenu.css'
+import { Document } from '../../../fields/Document';
+import { EditableView } from '../../../client/views/EditableView';
+import { KeyStore } from '../../../fields/KeyStore';
+
+export interface WorkspaceMenuProps {
+ active: Document | undefined;
+ open: (workspace: Document) => void;
+ new: (init: boolean) => void;
+ allWorkspaces: Document[];
+}
+
+@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;
+ }
+
+ render() {
+ return (
+ <div
+ style={{
+ width: "auto",
+ maxHeight: '200px',
+ overflow: 'scroll',
+ borderRadius: 5,
+ position: "absolute",
+ top: 78,
+ left: this.workspacesExposed ? 11 : -500,
+ background: "white",
+ border: "black solid 2px",
+ transition: "all 1s ease",
+ zIndex: 15,
+ padding: 10,
+ paddingRight: 12,
+ }}>
+ <img
+ src="https://bit.ly/2IBBkxk"
+ style={{
+ width: 20,
+ height: 20,
+ marginTop: 3,
+ marginLeft: 3,
+ marginBottom: 3,
+ cursor: "grab"
+ }}
+ onClick={this.addNewWorkspace}
+ />
+ {this.props.allWorkspaces.map((s, i) =>
+ <div
+ key={s.Id}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ this.props.open(s);
+ }}
+ style={{
+ marginTop: 10,
+ color: s === this.props.active ? "red" : "black"
+ }}
+ >
+ <span>{i + 1} - </span>
+ <EditableView
+ display={"inline"}
+ GetValue={() => { return s.Title }}
+ SetValue={(title: string): boolean => {
+ s.SetText(KeyStore.Title, title);
+ return true;
+ }}
+ contents={s.Title}
+ height={20}
+ />
+ </div>
+ )}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts
deleted file mode 100644
index f74ff9039..000000000
--- a/src/server/authentication/controllers/user.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { default as User, UserModel, AuthToken } from "../models/User";
-import { Request, Response, NextFunction } from "express";
-import * as passport from "passport";
-import { IVerifyOptions } from "passport-local";
-import "../config/passport";
-import * as request from "express-validator";
-const flash = require("express-flash");
-import * as session from "express-session";
-import * as pug from 'pug';
-
-/**
- * GET /signup
- * Signup page.
- */
-export let getSignup = (req: Request, res: Response) => {
- if (req.user) {
- return res.redirect("/");
- }
- res.render("signup.pug", {
- title: "Sign Up"
- });
-};
-
-/**
- * POST /signup
- * Create a new local account.
- */
-export let postSignup = (req: Request, res: Response, next: NextFunction) => {
- req.assert("email", "Email is not valid").isEmail();
- req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
- req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
- req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
-
- const errors = req.validationErrors();
-
- if (errors) {
- req.flash("errors", "Unable to facilitate sign up. Please try again.");
- return res.redirect("/signup");
- }
-
- const user = new User({
- email: req.body.email,
- password: req.body.password
- });
-
- User.findOne({ email: req.body.email }, (err, existingUser) => {
- if (err) { return next(err); }
- if (existingUser) {
- req.flash("errors", "Account with that email address already exists.");
- return res.redirect("/signup");
- }
- user.save((err) => {
- if (err) { return next(err); }
- req.logIn(user, (err) => {
- if (err) {
- return next(err);
- }
- res.redirect("/");
- });
- });
- });
-};
-
-
-/**
- * GET /login
- * Login page.
- */
-export let getLogin = (req: Request, res: Response) => {
- if (req.user) {
- return res.redirect("/");
- }
- res.send("<p>dear lord please render</p>");
- // res.render("account/login", {
- // title: "Login"
- // });
-};
-
-/**
- * POST /login
- * Sign in using email and password.
- */
-export let postLogin = (req: Request, res: Response, next: NextFunction) => {
- req.assert("email", "Email is not valid").isEmail();
- req.assert("password", "Password cannot be blank").notEmpty();
- req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
-
- const errors = req.validationErrors();
-
- if (errors) {
- req.flash("errors", "Unable to login at this time. Please try again.");
- return res.redirect("/login");
- }
-
- passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => {
- if (err) { return next(err); }
- if (!user) {
- req.flash("errors", info.message);
- return res.redirect("/login");
- }
- req.logIn(user, (err) => {
- if (err) { return next(err); }
- req.flash("success", "Success! You are logged in.");
- res.redirect("/");
- });
- })(req, res, next);
-}; \ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
new file mode 100644
index 000000000..2cef958e8
--- /dev/null
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -0,0 +1,263 @@
+import { default as User, DashUserModel, AuthToken } from "../models/user_model";
+import { Request, Response, NextFunction } from "express";
+import * as passport from "passport";
+import { IVerifyOptions } from "passport-local";
+import "../config/passport";
+import * as request from "express-validator";
+const flash = require("express-flash");
+import * as session from "express-session";
+import * as pug from 'pug';
+import * as async from 'async';
+import * as nodemailer from 'nodemailer';
+import c = require("crypto");
+import { RouteStore } from "../../RouteStore";
+
+/**
+ * GET /signup
+ * Directs user to the signup page
+ * modeled by signup.pug in views
+ */
+export let getSignup = (req: Request, res: Response) => {
+ if (req.user) {
+ let user = req.user;
+ return res.redirect(RouteStore.home);
+ }
+ res.render("signup.pug", {
+ title: "Sign Up",
+ user: req.user,
+ });
+};
+
+/**
+ * POST /signup
+ * Create a new local account.
+ */
+export let postSignup = (req: Request, res: Response, next: NextFunction) => {
+ req.assert("email", "Email is not valid").isEmail();
+ req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
+ req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
+ req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
+
+ const errors = req.validationErrors();
+
+ if (errors) {
+ res.render("signup.pug", {
+ title: "Sign Up",
+ user: req.user,
+ });
+ return res.redirect(RouteStore.signup);
+ }
+
+ const email = req.body.email;
+ const password = req.body.password;
+
+ const user = new User({
+ email,
+ password,
+ userDoc: "document here"
+ });
+
+ User.findOne({ email }, (err, existingUser) => {
+ if (err) { return next(err); }
+ if (existingUser) {
+ return res.redirect(RouteStore.login);
+ }
+ user.save((err) => {
+ if (err) { return next(err); }
+ req.logIn(user, (err) => {
+ if (err) {
+ return next(err);
+ }
+ res.redirect(RouteStore.home);
+ });
+ });
+ });
+
+};
+
+
+/**
+ * GET /login
+ * Login page.
+ */
+export let getLogin = (req: Request, res: Response) => {
+ if (req.user) {
+ return res.redirect(RouteStore.home);
+ }
+ res.render("login.pug", {
+ title: "Log In",
+ user: req.user
+ });
+};
+
+/**
+ * POST /login
+ * Sign in using email and password.
+ * On failure, redirect to signup page
+ */
+export let postLogin = (req: Request, res: Response, next: NextFunction) => {
+ req.assert("email", "Email is not valid").isEmail();
+ req.assert("password", "Password cannot be blank").notEmpty();
+ req.sanitize("email").normalizeEmail({ gmail_remove_dots: false });
+
+ const errors = req.validationErrors();
+
+ if (errors) {
+ req.flash("errors", "Unable to login at this time. Please try again.");
+ return res.redirect(RouteStore.signup);
+ }
+
+ passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => {
+ if (err) { return next(err); }
+ if (!user) {
+ return res.redirect(RouteStore.signup);
+ }
+ req.logIn(user, (err) => {
+ if (err) { return next(err); }
+ res.redirect(RouteStore.home);
+ });
+ })(req, res, next);
+};
+
+/**
+ * GET /logout
+ * Invokes the logout function on the request
+ * and destroys the user's current session.
+ */
+export let getLogout = (req: Request, res: Response) => {
+ req.logout();
+ const sess = req.session;
+ if (sess) {
+ sess.destroy((err) => { if (err) { console.log(err); } });
+ }
+ res.redirect(RouteStore.login);
+}
+
+export let getForgot = function (req: Request, res: Response) {
+ res.render("forgot.pug", {
+ title: "Recover Password",
+ user: req.user,
+ });
+}
+
+export let postForgot = function (req: Request, res: Response, next: NextFunction) {
+ const email = req.body.email;
+ async.waterfall([
+ function (done: any) {
+ let token: string;
+ c.randomBytes(20, function (err: any, buffer: Buffer) {
+ if (err) {
+ done(null);
+ return;
+ }
+ done(null, buffer.toString('hex'));
+ })
+ },
+ function (token: string, done: any) {
+ User.findOne({ email }, function (err, user: DashUserModel) {
+ if (!user) {
+ // NO ACCOUNT WITH SUBMITTED EMAIL
+ return res.redirect(RouteStore.forgot);
+ }
+ user.passwordResetToken = token;
+ user.passwordResetExpires = new Date(Date.now() + 3600000); // 1 HOUR
+ user.save(function (err: any) {
+ done(null, token, user);
+ });
+ });
+ },
+ function (token: Uint16Array, user: DashUserModel, done: any) {
+ const smtpTransport = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'brownptcdash@gmail.com',
+ pass: 'browngfx1'
+ }
+ });
+ const mailOptions = {
+ to: user.email,
+ from: 'brownptcdash@gmail.com',
+ subject: 'Dash Password Reset',
+ text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
+ 'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
+ 'http://' + req.headers.host + '/reset/' + token + '\n\n' +
+ 'If you did not request this, please ignore this email and your password will remain unchanged.\n'
+ };
+ smtpTransport.sendMail(mailOptions, function (err) {
+ // req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
+ done(null, err, 'done');
+ });
+ }
+ ], function (err) {
+ if (err) return next(err);
+ res.redirect(RouteStore.forgot);
+ })
+}
+
+export let getReset = function (req: Request, res: Response) {
+ User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) {
+ if (!user || err) {
+ return res.redirect(RouteStore.forgot);
+ }
+ res.render("reset.pug", {
+ title: "Reset Password",
+ user: req.user,
+ });
+ });
+}
+
+export let postReset = function (req: Request, res: Response) {
+ async.waterfall([
+ function (done: any) {
+ User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) {
+ if (!user || err) {
+ return res.redirect('back');
+ }
+
+ req.assert("password", "Password must be at least 4 characters long").len({ min: 4 });
+ req.assert("confirmPassword", "Passwords do not match").equals(req.body.password);
+
+ if (req.validationErrors()) {
+ return res.redirect('back');
+ }
+
+ user.password = req.body.password;
+ user.passwordResetToken = undefined;
+ user.passwordResetExpires = undefined;
+
+ user.save(function (err) {
+ if (err) {
+ return res.redirect(RouteStore.login);
+ }
+ req.logIn(user, function (err) {
+ if (err) {
+ return;
+ }
+ });
+ done(null, user);
+ });
+ });
+ },
+ function (user: DashUserModel, done: any) {
+ const smtpTransport = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: 'brownptcdash@gmail.com',
+ pass: 'browngfx1'
+ }
+ });
+ const mailOptions = {
+ to: user.email,
+ from: 'brownptcdash@gmail.com',
+ subject: 'Your password has been changed',
+ text: 'Hello,\n\n' +
+ 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
+ };
+ smtpTransport.sendMail(mailOptions, function (err) {
+ done(null, err);
+ });
+ }
+ ], function (err) {
+ res.redirect(RouteStore.login);
+ });
+} \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
new file mode 100644
index 000000000..cc433eb73
--- /dev/null
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -0,0 +1,29 @@
+import { DashUserModel } from "./user_model";
+import * as request from 'request'
+import { RouteStore } from "../../RouteStore";
+import { ServerUtils } from "../../ServerUtil";
+
+export class CurrentUserUtils {
+ private static curr_email: string;
+ private static curr_id: string;
+
+ public static get email() {
+ return CurrentUserUtils.curr_email;
+ }
+
+ public static get id() {
+ return CurrentUserUtils.curr_id;
+ }
+
+ public static loadCurrentUser() {
+ request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => {
+ if (body) {
+ let obj = JSON.parse(body);
+ 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?")
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/user_model.ts
index 9752c4260..3d4ed6896 100644
--- a/src/server/authentication/models/User.ts
+++ b/src/server/authentication/models/user_model.ts
@@ -1,6 +1,5 @@
//@ts-ignore
import * as bcrypt from "bcrypt-nodejs";
-import * as crypto from "crypto";
//@ts-ignore
import * as mongoose from "mongoose";
var url = 'mongodb://localhost:27017/Dash'
@@ -16,12 +15,15 @@ mongoose.connection.on('error', function (error) {
mongoose.connection.on('disconnected', function () {
console.log('connection closed');
});
-export type UserModel = mongoose.Document & {
+export type DashUserModel = mongoose.Document & {
email: string,
password: string,
- passwordResetToken: string,
- passwordResetExpires: Date,
- tokens: AuthToken[],
+ passwordResetToken: string | undefined,
+ passwordResetExpires: Date | undefined,
+
+ allWorkspaceIds: Array<String>,
+ activeWorkspaceId: String,
+ activeUsersId: String,
profile: {
name: string,
@@ -47,10 +49,16 @@ const userSchema = new mongoose.Schema({
passwordResetToken: String,
passwordResetExpires: Date,
+ allWorkspaceIds: {
+ type: Array,
+ default: []
+ },
+ activeWorkspaceId: String,
+ activeUsersId: String,
+
facebook: String,
twitter: String,
google: String,
- tokens: Array,
profile: {
name: String,
@@ -65,7 +73,7 @@ const userSchema = new mongoose.Schema({
* Password hash middleware.
*/
userSchema.pre("save", function save(next) {
- const user = this as UserModel;
+ const user = this as DashUserModel;
if (!user.isModified("password")) { return next(); }
bcrypt.genSalt(10, (err, salt) => {
if (err) { return next(err); }
@@ -77,7 +85,7 @@ userSchema.pre("save", function save(next) {
});
});
-const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) {
+const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => {
cb(err, isMatch);
});
diff --git a/src/server/database.ts b/src/server/database.ts
index f3c1c9427..99b13805e 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -19,7 +19,7 @@ export class Database {
public update(id: string, value: any, callback: () => void) {
if (this.db) {
let collection = this.db.collection('documents');
- collection.update({ _id: id }, { $set: value }, {
+ collection.updateOne({ _id: id }, { $set: value }, {
upsert: true
}, callback);
}
@@ -32,9 +32,9 @@ export class Database {
}
}
- public deleteAll() {
+ public deleteAll(collectionName: string = 'documents') {
if (this.db) {
- let collection = this.db.collection('documents');
+ let collection = this.db.collection(collectionName);
collection.deleteMany({});
}
}
@@ -70,6 +70,10 @@ export class Database {
let collection = this.db.collection('documents');
let cursor = collection.find({ _id: { "$in": ids } })
cursor.toArray((err, docs) => {
+ if (err) {
+ console.log(err.message);
+ console.log(err.errmsg);
+ }
fn(docs);
})
};
diff --git a/src/server/index.ts b/src/server/index.ts
index 83fa84746..5773514ea 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -6,63 +6,64 @@ import * as whm from 'webpack-hot-middleware';
import * as path from 'path'
import * as formidable from 'formidable'
import * as passport from 'passport';
-import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message";
+import { MessageStore, Transferable } from "./Message";
import { Client } from './Client';
import { Socket } from 'socket.io';
import { Utils } from '../Utils';
import { ObservableMap } from 'mobx';
import { FieldId, Field } from '../fields/Field';
import { Database } from './database';
-import { ServerUtils } from './ServerUtil';
-import { ObjectID } from 'mongodb';
-import { Document } from '../fields/Document';
import * as io from 'socket.io'
-import * as passportConfig from './authentication/config/passport';
-import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user';
+import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller';
const config = require('../../webpack.config');
const compiler = webpack(config);
const port = 1050; // default port to listen
const serverPort = 1234;
import * as expressValidator from 'express-validator';
import expressFlash = require('express-flash');
+import flash = require('connect-flash');
import * as bodyParser from 'body-parser';
import * as session from 'express-session';
+import * as cookieParser from 'cookie-parser';
+import * as mobileDetect from 'mobile-detect';
import c = require("crypto");
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
-const bluebird = require('bluebird');
-import { performance } from 'perf_hooks'
+import { DashUserModel } from './authentication/models/user_model';
import * as fs from 'fs';
import * as request from 'request'
+import { RouteStore } from './RouteStore';
+import { exec } from 'child_process'
const download = (url: string, dest: fs.PathLike) => {
request.get(url).pipe(fs.createWriteStream(dest));
}
const mongoUrl = 'mongodb://localhost:27017/Dash';
-// mongoose.Promise = bluebird;
-mongoose.connect(mongoUrl)//.then(
-// () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },
-// ).catch((err: any) => {
-// console.log("MongoDB connection error. Please make sure MongoDB is running. " + err);
-// process.exit();
-// });
+mongoose.connect(mongoUrl)
mongoose.connection.on('connected', function () {
console.log("connected");
})
-app.use(bodyParser.json());
-app.use(bodyParser.urlencoded({ extended: true }));
-app.use(expressValidator());
-app.use(expressFlash());
-app.use(require('express-session')({
- secret: `${c.randomBytes(64)}`,
+// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE
+// ORDER OF IMPORTS MATTERS
+
+app.use(cookieParser());
+app.use(session({
+ secret: "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc",
resave: true,
+ cookie: { maxAge: 7 * 24 * 60 * 60 },
saveUninitialized: true,
store: new MongoStore({
url: 'mongodb://localhost:27017/Dash'
})
}));
+
+app.use(flash());
+app.use(expressFlash());
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(expressValidator());
app.use(passport.initialize());
app.use(passport.session());
app.use((req, res, next) => {
@@ -70,50 +71,194 @@ app.use((req, res, next) => {
next();
});
-app.get("/signup", getSignup);
-app.post("/signup", postSignup);
-app.get("/login", getLogin);
-app.post("/login", postLogin);
-
-// IMAGE UPLOADING HANDLER
-app.post("/upload", (req, res, err) => {
- let form = new formidable.IncomingForm()
- form.uploadDir = __dirname + "/public/files/"
- form.keepExtensions = true
- // let path = req.body.path;
- console.log("upload")
- form.parse(req, (err, fields, files) => {
- console.log("parsing")
- let names: any[] = [];
- for (const name in files) {
- let file = files[name];
- names.push(`/files/` + path.basename(file.path));
+app.get("/hello", (req, res) => {
+ res.send("<p>Hello</p>");
+})
+
+enum Method {
+ GET,
+ POST
+}
+
+/**
+ * Please invoke this function when adding a new route to Dash's server.
+ * It ensures that any requests leading to or containing user-sensitive information
+ * does not execute unless Passport authentication detects a user logged in.
+ * @param method whether or not the request is a GET or a POST
+ * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response
+ * @param onRejection an optional callback invoked on return if no user is found to be logged in
+ * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler
+ */
+function addSecureRoute(method: Method,
+ handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
+ onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout),
+ ...subscribers: string[]
+) {
+ let abstracted = (req: express.Request, res: express.Response) => {
+ const dashUser: DashUserModel = req.user;
+ if (!dashUser) return onRejection(res);
+ handler(dashUser, res, req);
+ }
+ subscribers.forEach(route => {
+ switch (method) {
+ case Method.GET:
+ app.get(route, abstracted);
+ break;
+ case Method.POST:
+ app.post(route, abstracted);
+ break;
}
- res.send(names);
});
-})
+}
-app.use(express.static(__dirname + '/public'));
-app.use('/images', express.static(__dirname + '/public'))
+// STATIC FILE SERVING
let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
-// define a route handler for the default home page
-app.get("/", (req, res) => {
- res.sendFile(path.join(__dirname, '../../deploy/index.html'));
+app.use(express.static(__dirname + RouteStore.public));
+app.use(RouteStore.images, express.static(__dirname + RouteStore.public))
+
+app.get("/pull", (req, res) => {
+ exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', (err, stdout, stderr) => {
+ if (err) {
+ res.send(err.message);
+ return;
+ }
+ res.redirect("/");
+ })
});
-app.get("/hello", (req, res) => {
- res.send("<p>Hello</p>");
-})
+// GETTERS
-app.use("/corsProxy", (req, res) => {
+// anyone attempting to navigate to localhost at this port will
+// first have to login
+addSecureRoute(
+ Method.GET,
+ (user, res) => res.redirect(RouteStore.home),
+ undefined,
+ RouteStore.root
+);
+
+addSecureRoute(
+ Method.GET,
+ (user, res, req) => {
+ let detector = new mobileDetect(req.headers['user-agent'] || "");
+ if (detector.mobile() != null) {
+ res.sendFile(path.join(__dirname, '../../deploy/mobile/image.html'));
+ } else {
+ res.sendFile(path.join(__dirname, '../../deploy/index.html'));
+ }
+ },
+ undefined,
+ RouteStore.home,
+ RouteStore.openDocumentWithId
+);
+
+addSecureRoute(
+ Method.GET,
+ (user, res) => res.send(user.activeWorkspaceId || ""),
+ undefined,
+ RouteStore.getActiveWorkspace,
+);
+
+addSecureRoute(
+ Method.GET,
+ (user, res) => res.send(JSON.stringify(user.allWorkspaceIds)),
+ undefined,
+ RouteStore.getAllWorkspaces
+);
+
+addSecureRoute(
+ Method.GET,
+ (user, res) => {
+ res.send(JSON.stringify({
+ id: user.id,
+ email: user.email
+ }));
+ },
+ undefined,
+ RouteStore.getCurrUser
+);
+
+// SETTERS
+
+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
+ // let path = req.body.path;
+ console.log("upload")
+ form.parse(req, (err, fields, files) => {
+ console.log("parsing")
+ let names: any[] = [];
+ for (const name in files) {
+ let file = files[name];
+ names.push(`/files/` + path.basename(file.path));
+ }
+ res.send(names);
+ });
+ },
+ undefined,
+ RouteStore.upload
+);
+
+// AUTHENTICATION
+
+// Sign Up
+app.get(RouteStore.signup, getSignup);
+app.post(RouteStore.signup, postSignup);
+
+// Log In
+app.get(RouteStore.login, getLogin);
+app.post(RouteStore.login, postLogin);
+
+// Log Out
+app.get(RouteStore.logout, getLogout);
+
+// FORGOT PASSWORD EMAIL HANDLING
+app.get(RouteStore.forgot, getForgot)
+app.post(RouteStore.forgot, postForgot)
+
+// RESET PASSWORD EMAIL HANDLING
+app.get(RouteStore.reset, getReset);
+app.post(RouteStore.reset, postReset);
+
+app.use(RouteStore.corsProxy, (req, res) => {
req.pipe(request(req.url.substring(1))).pipe(res);
});
-app.get("/delete", (req, res) => {
+app.get(RouteStore.delete, (req, res) => {
+ deleteFields();
+ res.redirect(RouteStore.home);
+});
+
+app.get(RouteStore.deleteAll, (req, res) => {
deleteAll();
- res.redirect("/");
+ res.redirect(RouteStore.home);
});
app.use(wdm(compiler, {
@@ -142,21 +287,23 @@ server.on("connection", function (socket: Socket) {
Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args))
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField)
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields)
- Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteAll)
+ Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields)
})
+function deleteFields() {
+ Database.Instance.deleteAll();
+}
+
function deleteAll() {
Database.Instance.deleteAll();
+ Database.Instance.deleteAll('sessions');
+ Database.Instance.deleteAll('users');
}
function barReceived(guid: String) {
clients[guid.toString()] = new Client(guid.toString());
}
-function addDocument(document: Document) {
-
-}
-
function getField([id, callback]: [string, (result: any) => void]) {
Database.Instance.getDocument(id, (result: any) => {
if (result) {
diff --git a/src/server/public/files/upload_e72669595eae4384a2a32196496f4f05.pdf b/src/server/public/files/upload_e72669595eae4384a2a32196496f4f05.pdf
new file mode 100644
index 000000000..8e58bfddd
--- /dev/null
+++ b/src/server/public/files/upload_e72669595eae4384a2a32196496f4f05.pdf
Binary files differ
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index e4a66f7f2..7939ae8be 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -1,322 +1,322 @@
/// <reference types="node" />
declare module '@react-pdf/renderer' {
- import * as React from 'react';
-
- namespace ReactPDF {
- interface Style {
- [property: string]: any;
- }
- interface Styles {
- [key: string]: Style;
- }
- type Orientation = 'portrait' | 'landscape';
-
- interface DocumentProps {
- title?: string;
- author?: string;
- subject?: string;
- keywords?: string;
- creator?: string;
- producer?: string;
- onRender?: () => any;
- }
-
- /**
- * This component represent the PDF document itself. It must be the root
- * of your tree element structure, and under no circumstances should it be
- * used as children of another react-pdf component. In addition, it should
- * only have childs of type <Page />.
- */
- class Document extends React.Component<DocumentProps> {}
-
- interface NodeProps {
- style?: Style | Style[];
- /**
- * Render component in all wrapped pages.
- * @see https://react-pdf.org/advanced#fixed-components
- */
- fixed?: boolean;
- /**
- * Force the wrapping algorithm to start a new page when rendering the
- * element.
- * @see https://react-pdf.org/advanced#page-breaks
- */
- break?: boolean;
- }
-
- interface PageProps extends NodeProps {
- /**
- * Enable page wrapping for this page.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- size?: string | [number, number] | {width: number; height: number};
- orientation?: Orientation;
- ruler?: boolean;
- rulerSteps?: number;
- verticalRuler?: boolean;
- verticalRulerSteps?: number;
- horizontalRuler?: boolean;
- horizontalRulerSteps?: number;
- ref?: Page;
- }
-
- /**
- * Represents single page inside the PDF document, or a subset of them if
- * using the wrapping feature. A <Document /> can contain as many pages as
- * you want, but ensure not rendering a page inside any component besides
- * Document.
- */
- class Page extends React.Component<PageProps> {}
-
- interface ViewProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (props: {pageNumber: number}) => React.ReactNode;
- children?: React.ReactNode;
- }
-
+ import * as React from 'react';
+
+ namespace ReactPDF {
+ interface Style {
+ [property: string]: any;
+ }
+ interface Styles {
+ [key: string]: Style;
+ }
+ type Orientation = 'portrait' | 'landscape';
+
+ interface DocumentProps {
+ title?: string;
+ author?: string;
+ subject?: string;
+ keywords?: string;
+ creator?: string;
+ producer?: string;
+ onRender?: () => any;
+ }
+
+ /**
+ * This component represent the PDF document itself. It must be the root
+ * of your tree element structure, and under no circumstances should it be
+ * used as children of another react-pdf component. In addition, it should
+ * only have childs of type <Page />.
+ */
+ class Document extends React.Component<DocumentProps> { }
+
+ interface NodeProps {
+ style?: Style | Style[];
/**
- * The most fundamental component for building a UI and is designed to be
- * nested inside other views and can have 0 to many children.
+ * Render component in all wrapped pages.
+ * @see https://react-pdf.org/advanced#fixed-components
*/
- class View extends React.Component<ViewProps> {}
-
- interface ImageProps extends NodeProps {
- debug?: boolean;
- src: string | {data: Buffer; format: 'png' | 'jpg'};
- cache?: boolean;
- }
-
+ fixed?: boolean;
/**
- * A React component for displaying network or local (Node only) JPG or
- * PNG images, as well as base64 encoded image strings.
+ * Force the wrapping algorithm to start a new page when rendering the
+ * element.
+ * @see https://react-pdf.org/advanced#page-breaks
*/
- class Image extends React.Component<ImageProps> {}
-
- interface TextProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (
- props: {pageNumber: number; totalPages: number},
- ) => React.ReactNode;
- children?: React.ReactNode;
- /**
- * How much hyphenated breaks should be avoided.
- */
- hyphenationCallback?: number;
- }
-
+ break?: boolean;
+ }
+
+ interface PageProps extends NodeProps {
/**
- * A React component for displaying text. Text supports nesting of other
- * Text or Link components to create inline styling.
+ * Enable page wrapping for this page.
+ * @see https://react-pdf.org/components#page-wrapping
*/
- class Text extends React.Component<TextProps> {}
-
- interface LinkProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- src: string;
- children?: React.ReactNode;
- }
-
+ wrap?: boolean;
+ debug?: boolean;
+ size?: string | [number, number] | { width: number; height: number };
+ orientation?: Orientation;
+ ruler?: boolean;
+ rulerSteps?: number;
+ verticalRuler?: boolean;
+ verticalRulerSteps?: number;
+ horizontalRuler?: boolean;
+ horizontalRulerSteps?: number;
+ ref?: Page;
+ }
+
+ /**
+ * Represents single page inside the PDF document, or a subset of them if
+ * using the wrapping feature. A <Document /> can contain as many pages as
+ * you want, but ensure not rendering a page inside any component besides
+ * Document.
+ */
+ class Page extends React.Component<PageProps> { }
+
+ interface ViewProps extends NodeProps {
/**
- * A React component for displaying an hyperlink. Link’s can be nested
- * inside a Text component, or being inside any other valid primitive.
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
*/
- class Link extends React.Component<LinkProps> {}
-
- interface NoteProps extends NodeProps {
- children: string;
- }
-
- class Note extends React.Component<NoteProps> {}
-
- interface BlobProviderParams {
- blob: Blob | null;
- url: string | null;
- loading: boolean;
- error: Error | null;
- }
- interface BlobProviderProps {
- document: React.ReactElement<DocumentProps>;
- children: (params: BlobProviderParams) => React.ReactNode;
- }
-
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (props: { pageNumber: number }) => React.ReactNode;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * The most fundamental component for building a UI and is designed to be
+ * nested inside other views and can have 0 to many children.
+ */
+ class View extends React.Component<ViewProps> { }
+
+ interface ImageProps extends NodeProps {
+ debug?: boolean;
+ src: string | { data: Buffer; format: 'png' | 'jpg' };
+ cache?: boolean;
+ }
+
+ /**
+ * A React component for displaying network or local (Node only) JPG or
+ * PNG images, as well as base64 encoded image strings.
+ */
+ class Image extends React.Component<ImageProps> { }
+
+ interface TextProps extends NodeProps {
/**
- * Easy and declarative way of getting document's blob data without
- * showing it on screen.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
*/
- class BlobProvider extends React.Component<BlobProviderProps> {}
-
- interface PDFViewerProps {
- width?: number;
- height?: number;
- style?: Style | Style[];
- className?: string;
- children?: React.ReactElement<DocumentProps>;
- }
-
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (
+ props: { pageNumber: number; totalPages: number },
+ ) => React.ReactNode;
+ children?: React.ReactNode;
/**
- * Iframe PDF viewer for client-side generated documents.
- * @platform web
+ * How much hyphenated breaks should be avoided.
*/
- class PDFViewer extends React.Component<PDFViewerProps> {}
-
- interface PDFDownloadLinkProps {
- document: React.ReactElement<DocumentProps>;
- fileName?: string;
- style?: Style | Style[];
- className?: string;
- children?:
- | React.ReactNode
- | ((params: BlobProviderParams) => React.ReactNode);
- }
-
+ hyphenationCallback?: number;
+ }
+
+ /**
+ * A React component for displaying text. Text supports nesting of other
+ * Text or Link components to create inline styling.
+ */
+ class Text extends React.Component<TextProps> { }
+
+ interface LinkProps extends NodeProps {
/**
- * Anchor tag to enable generate and download PDF documents on the fly.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
*/
- class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> {}
-
- interface EmojiSource {
- url: string;
- format: string;
- }
- interface RegisteredFont {
- src: string;
- loaded: boolean;
- loading: boolean;
- data: any;
- [key: string]: any;
- }
- type HyphenationCallback = (
- words: string[],
- glyphString: {[key: string]: any},
- ) => string[];
-
- const Font: {
- register: (
- src: string,
- options: {family: string; [key: string]: any},
- ) => void;
- getEmojiSource: () => EmojiSource;
- getRegisteredFonts: () => string[];
- registerEmojiSource: (emojiSource: EmojiSource) => void;
- registerHyphenationCallback: (
- hyphenationCallback: HyphenationCallback,
- ) => void;
- getHyphenationCallback: () => HyphenationCallback;
- getFont: (fontFamily: string) => RegisteredFont | undefined;
- load: (
- fontFamily: string,
- document: React.ReactElement<DocumentProps>,
- ) => Promise<void>;
- clear: () => void;
- reset: () => void;
- };
-
- const StyleSheet: {
- hairlineWidth: number;
- create: <TStyles>(styles: TStyles) => TStyles;
- resolve: (
- style: Style,
- container: {
- width: number;
- height: number;
- orientation: Orientation;
- },
- ) => Style;
- flatten: (...styles: Style[]) => Style;
- absoluteFillObject: {
- position: 'absolute';
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- };
- };
-
- const version: any;
-
- const PDFRenderer: any;
-
- const createInstance: (
- element: {
- type: string;
- props: {[key: string]: any};
- },
- root?: any,
- ) => any;
-
- const pdf: (
+ wrap?: boolean;
+ debug?: boolean;
+ src: string;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * A React component for displaying an hyperlink. Link’s can be nested
+ * inside a Text component, or being inside any other valid primitive.
+ */
+ class Link extends React.Component<LinkProps> { }
+
+ interface NoteProps extends NodeProps {
+ children: string;
+ }
+
+ class Note extends React.Component<NoteProps> { }
+
+ interface BlobProviderParams {
+ blob: Blob | null;
+ url: string | null;
+ loading: boolean;
+ error: Error | null;
+ }
+ interface BlobProviderProps {
+ document: React.ReactElement<DocumentProps>;
+ children: (params: BlobProviderParams) => React.ReactNode;
+ }
+
+ /**
+ * Easy and declarative way of getting document's blob data without
+ * showing it on screen.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class BlobProvider extends React.Component<BlobProviderProps> { }
+
+ interface PDFViewerProps {
+ width?: number;
+ height?: number;
+ style?: Style | Style[];
+ className?: string;
+ children?: React.ReactElement<DocumentProps>;
+ }
+
+ /**
+ * Iframe PDF viewer for client-side generated documents.
+ * @platform web
+ */
+ class PDFViewer extends React.Component<PDFViewerProps> { }
+
+ interface PDFDownloadLinkProps {
+ document: React.ReactElement<DocumentProps>;
+ fileName?: string;
+ style?: Style | Style[];
+ className?: string;
+ children?:
+ | React.ReactNode
+ | ((params: BlobProviderParams) => React.ReactNode);
+ }
+
+ /**
+ * Anchor tag to enable generate and download PDF documents on the fly.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
+
+ interface EmojiSource {
+ url: string;
+ format: string;
+ }
+ interface RegisteredFont {
+ src: string;
+ loaded: boolean;
+ loading: boolean;
+ data: any;
+ [key: string]: any;
+ }
+ type HyphenationCallback = (
+ words: string[],
+ glyphString: { [key: string]: any },
+ ) => string[];
+
+ const Font: {
+ register: (
+ src: string,
+ options: { family: string;[key: string]: any },
+ ) => void;
+ getEmojiSource: () => EmojiSource;
+ getRegisteredFonts: () => string[];
+ registerEmojiSource: (emojiSource: EmojiSource) => void;
+ registerHyphenationCallback: (
+ hyphenationCallback: HyphenationCallback,
+ ) => void;
+ getHyphenationCallback: () => HyphenationCallback;
+ getFont: (fontFamily: string) => RegisteredFont | undefined;
+ load: (
+ fontFamily: string,
document: React.ReactElement<DocumentProps>,
- ) => {
- isDirty: () => boolean;
- updateContainer: (document: React.ReactElement<any>) => void;
- toBuffer: () => NodeJS.ReadableStream;
- toBlob: () => Blob;
- toString: () => string;
+ ) => Promise<void>;
+ clear: () => void;
+ reset: () => void;
+ };
+
+ const StyleSheet: {
+ hairlineWidth: number;
+ create: <TStyles>(styles: TStyles) => TStyles;
+ resolve: (
+ style: Style,
+ container: {
+ width: number;
+ height: number;
+ orientation: Orientation;
+ },
+ ) => Style;
+ flatten: (...styles: Style[]) => Style;
+ absoluteFillObject: {
+ position: 'absolute';
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
};
-
- const renderToStream: (
- document: React.ReactElement<DocumentProps>,
- ) => NodeJS.ReadableStream;
-
- const renderToFile: (
- document: React.ReactElement<DocumentProps>,
- filePath: string,
- callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
- ) => Promise<NodeJS.ReadableStream>;
-
- const render: typeof renderToFile;
- }
-
- const Document: typeof ReactPDF.Document;
- const Page: typeof ReactPDF.Page;
- const View: typeof ReactPDF.View;
- const Image: typeof ReactPDF.Image;
- const Text: typeof ReactPDF.Text;
- const Link: typeof ReactPDF.Link;
- const Note: typeof ReactPDF.Note;
- const Font: typeof ReactPDF.Font;
- const StyleSheet: typeof ReactPDF.StyleSheet;
- const createInstance: typeof ReactPDF.createInstance;
- const PDFRenderer: typeof ReactPDF.PDFRenderer;
- const version: typeof ReactPDF.version;
- const pdf: typeof ReactPDF.pdf;
-
- export default ReactPDF;
- export {
- Document,
- Page,
- View,
- Image,
- Text,
- Link,
- Note,
- Font,
- StyleSheet,
- createInstance,
- PDFRenderer,
- version,
- pdf,
};
- } \ No newline at end of file
+
+ const version: any;
+
+ const PDFRenderer: any;
+
+ const createInstance: (
+ element: {
+ type: string;
+ props: { [key: string]: any };
+ },
+ root?: any,
+ ) => any;
+
+ const pdf: (
+ document: React.ReactElement<DocumentProps>,
+ ) => {
+ isDirty: () => boolean;
+ updateContainer: (document: React.ReactElement<any>) => void;
+ toBuffer: () => NodeJS.ReadableStream;
+ toBlob: () => Blob;
+ toString: () => string;
+ };
+
+ const renderToStream: (
+ document: React.ReactElement<DocumentProps>,
+ ) => NodeJS.ReadableStream;
+
+ const renderToFile: (
+ document: React.ReactElement<DocumentProps>,
+ filePath: string,
+ callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
+ ) => Promise<NodeJS.ReadableStream>;
+
+ const render: typeof renderToFile;
+ }
+
+ const Document: typeof ReactPDF.Document;
+ const Page: typeof ReactPDF.Page;
+ const View: typeof ReactPDF.View;
+ const Image: typeof ReactPDF.Image;
+ const Text: typeof ReactPDF.Text;
+ const Link: typeof ReactPDF.Link;
+ const Note: typeof ReactPDF.Note;
+ const Font: typeof ReactPDF.Font;
+ const StyleSheet: typeof ReactPDF.StyleSheet;
+ const createInstance: typeof ReactPDF.createInstance;
+ const PDFRenderer: typeof ReactPDF.PDFRenderer;
+ const version: typeof ReactPDF.version;
+ const pdf: typeof ReactPDF.pdf;
+
+ export default ReactPDF;
+ export {
+ Document,
+ Page,
+ View,
+ Image,
+ Text,
+ Link,
+ Note,
+ Font,
+ StyleSheet,
+ createInstance,
+ PDFRenderer,
+ version,
+ pdf,
+ };
+} \ No newline at end of file
diff --git a/views/forgot.pug b/views/forgot.pug
new file mode 100644
index 000000000..4036b49db
--- /dev/null
+++ b/views/forgot.pug
@@ -0,0 +1,22 @@
+
+extends ./layout
+
+block content
+ style
+ include ./stylesheets/authentication.css
+ form.form-horizontal(id='forgot-form', method='POST')
+ input(type='hidden', name='_csrf', value=_csrf)
+ .overlay(id='overlay_forgot')
+ a(href="/login")
+ img(id='to_login', src="https://bit.ly/2U6ouZk", alt="")
+ .inner.forgot
+ h3.auth_header Recover Password
+ .form-group
+ //- label.col-sm-3.control-label(for='email', id='email_label') Email
+ .col-sm-7
+ input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required)
+ .form-group
+ .col-sm-offset-3.col-sm-7
+ button.btn.btn-success(id='submit', type='submit')
+ i.fa.fa-user-plus
+ | Submit \ No newline at end of file
diff --git a/views/layout.pug b/views/layout.pug
index fb22ae770..b11291eb9 100644
--- a/views/layout.pug
+++ b/views/layout.pug
@@ -8,10 +8,7 @@ html(lang='')
meta(name='description', content='')
meta(name='theme-color' content='#4DA5F4')
meta(name='csrf-token', content=_csrf)
- link(rel='shortcut icon', href='/images/favicon.png')
link(rel='stylesheet', href='/css/main.css')
body
-
- .container
- block content \ No newline at end of file
+ block content \ No newline at end of file
diff --git a/views/login.pug b/views/login.pug
new file mode 100644
index 000000000..9bc40a495
--- /dev/null
+++ b/views/login.pug
@@ -0,0 +1,28 @@
+
+extends ./layout
+
+block content
+ style
+ include ./stylesheets/authentication.css
+ form.form-horizontal(id='login-form', method='POST')
+ input(type='hidden', name='_csrf', value=_csrf)
+ .overlay(id='overlay_login')
+ a(href="/signup")
+ img(id='new_user', src="https://bit.ly/2EuqPb4", alt="")
+ a(href="/forgot")
+ img(id='forgot', src="https://bit.ly/2XjHpSo", alt="")
+ .inner.login
+ h3.auth_header Log In
+ .form-group
+ //- label.col-sm-3.control-label(for='email', id='email_label') Email
+ .col-sm-7
+ input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required)
+ .form-group
+ //- label.col-sm-3.control-label(for='password') Password
+ .col-sm-7
+ input.form-control(type='password', name='password', id='password', placeholder='Password', required)
+ .form-group
+ .col-sm-offset-3.col-sm-7
+ button.btn.btn-success(id='submit', type='submit')
+ i.fa.fa-user-plus
+ | Submit \ No newline at end of file
diff --git a/views/reset.pug b/views/reset.pug
new file mode 100644
index 000000000..8b6fa952b
--- /dev/null
+++ b/views/reset.pug
@@ -0,0 +1,22 @@
+
+extends ./layout
+
+block content
+ style
+ include ./stylesheets/authentication.css
+ form.form-horizontal(id='reset-form', method='POST')
+ input(type='hidden', name='_csrf', value=_csrf)
+ .overlay(id='overlay_reset')
+ .inner.reset
+ h3.auth_header Reset Password
+ .form-group
+ .col-sm-7
+ input.form-control(type='password', name='password', id='password', placeholder='Password', required)
+ .form-group
+ .col-sm-7
+ input.form-control(type='password', name='confirmPassword', id='confirmPassword', placeholder='Confirm Password', required)
+ .form-group
+ .col-sm-offset-3.col-sm-7
+ button.btn.btn-success(type='submit')
+ i.fa.fa-user-plus
+ | Reset \ No newline at end of file
diff --git a/views/resources/dashlogo.png b/views/resources/dashlogo.png
new file mode 100644
index 000000000..3ba4e111b
--- /dev/null
+++ b/views/resources/dashlogo.png
Binary files differ
diff --git a/views/signup.pug b/views/signup.pug
index a23f334af..11b02a5eb 100644
--- a/views/signup.pug
+++ b/views/signup.pug
@@ -2,24 +2,26 @@
extends ./layout
block content
- .page-header
- h3 Sign up
- form.form-horizontal(id='signup-form', method='POST')
- input(type='hidden', name='_csrf', value=_csrf)
- .form-group
- label.col-sm-3.control-label(for='email') Email
- .col-sm-7
- input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required)
- .form-group
- label.col-sm-3.control-label(for='password') Password
- .col-sm-7
- input.form-control(type='password', name='password', id='password', placeholder='Password', required)
- .form-group
- label.col-sm-3.control-label(for='confirmPassword') Confirm Password
- .col-sm-7
- input.form-control(type='password', name='confirmPassword', id='confirmPassword', placeholder='Confirm Password', required)
- .form-group
- .col-sm-offset-3.col-sm-7
- button.btn.btn-success(type='submit')
- i.fa.fa-user-plus
- | Signup \ No newline at end of file
+ style
+ include ./stylesheets/authentication.css
+ form.form-horizontal(id='signup-form', method='POST')
+ input(type='hidden', name='_csrf', value=_csrf)
+ .overlay(id='overlay_signup')
+ a(href="/login")
+ img(id='to_login', src="https://bit.ly/2U6ouZk", alt="")
+ .inner.signup
+ h3.auth_header Create An Account
+ .form-group
+ .col-sm-7
+ input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required)
+ .form-group
+ .col-sm-7
+ input.form-control(type='password', name='password', id='password', placeholder='Password', required)
+ .form-group
+ .col-sm-7
+ input.form-control(type='password', name='confirmPassword', id='confirmPassword', placeholder='Confirm Password', required)
+ .form-group
+ .col-sm-offset-3.col-sm-7
+ button.btn.btn-success(type='submit')
+ i.fa.fa-user-plus
+ | Sign Up \ No newline at end of file
diff --git a/views/stylesheets/authentication.css b/views/stylesheets/authentication.css
new file mode 100644
index 000000000..dea0474e4
--- /dev/null
+++ b/views/stylesheets/authentication.css
@@ -0,0 +1,142 @@
+#email_label {
+ color: blue;
+ margin-top: 10px;
+}
+
+h3,
+label {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+body {
+ /* background-color: #ccbbcc; */
+ background-color: #251f1f;
+ /* background-image: url(https://bit.ly/2XibZvI);
+ background-repeat: no-repeat;
+ background-size: cover; */
+}
+
+#logo {
+ width: 100px;
+ height: 100px;
+ position: absolute;
+}
+
+.auth_header {
+ text-align: left;
+}
+
+.login,
+.reset {
+ height: 220px;
+}
+
+.forgot {
+ height: 175px;
+}
+
+.signup {
+ height: 273px;
+}
+
+.btn {
+ width: 224px;
+ height: 35px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ font-style: oblique;
+}
+
+#overlay_signup,
+#overlay_reset,
+#overlay_workspaces {
+ height: 345px;
+}
+
+.workspace-header {
+ margin-left: 20px;
+}
+
+.select-workspace {
+ margin-top: 15px;
+ margin-left: 20px;
+}
+
+#overlay_workspaces {
+ overflow-y: scroll;
+ text-align: left;
+}
+
+.workspaceId {
+ list-style-type: none;
+ font-family: Arial, Helvetica, sans-serif;
+ margin-left: -20px;
+ cursor: grab;
+ padding-bottom: 15px;
+}
+
+.workspaceId:hover {
+ color: red;
+}
+
+#overlay_login {
+ height: 300px;
+}
+
+#overlay_forgot {
+ height: 250px;
+}
+
+#new_user,
+#to_login {
+ right: 15px;
+}
+
+#new_user,
+#to_login,
+#forgot {
+ top: 15px;
+ width: 20px;
+ height: 20px;
+ position: absolute;
+}
+
+#forgot {
+ left: 15px;
+}
+
+.overlay {
+ border: 2px solid yellow;
+ text-align: center;
+ position: absolute;
+ margin: auto;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 400px;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 10px 10px 10px #00000099;
+}
+
+.inner {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 230px;
+ margin: auto;
+}
+
+.form-control {
+ width: 200px;
+ margin-bottom: 15px;
+ height: 30px;
+ outline: none;
+ padding-left: 10px;
+ padding-right: 10px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 16px;
+} \ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 815e2b477..ff03181c9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -8,6 +8,8 @@ module.exports = {
bundle: ["./src/client/views/Main.tsx", 'webpack-hot-middleware/client?reload=true'],
viewer: ["./src/debug/Viewer.tsx", 'webpack-hot-middleware/client?reload=true'],
test: ["./src/debug/Test.tsx", 'webpack-hot-middleware/client?reload=true'],
+ inkControls: ["./src/mobile/InkControls.tsx", 'webpack-hot-middleware/client?reload=true'],
+ imageUpload: ["./src/mobile/ImageUpload.tsx", 'webpack-hot-middleware/client?reload=true'],
},
devtool: "source-map",
node: {