diff options
| author | Bob Zeleznik <zzzman@gmail.com> | 2019-04-30 21:51:01 -0400 |
|---|---|---|
| committer | Bob Zeleznik <zzzman@gmail.com> | 2019-04-30 21:51:01 -0400 |
| commit | e0375046dd873745f51263c215395843e7b6c2ce (patch) | |
| tree | 52e085050f3af8fe4832e5f172893848653ac001 | |
| parent | 32ad476cb98d140586af8147c3d2431623325385 (diff) | |
| parent | 86e89178628a27a91665ad835046e536bdb89729 (diff) | |
merged with master
123 files changed, 4689 insertions, 3198 deletions
diff --git a/build/index.html b/build/index.html index fda212af4..a738d1092 100644 --- a/build/index.html +++ b/build/index.html @@ -1,12 +1,12 @@ <html> <head> - <title>Dash Web</title> + <title>Dash Web Build</title> </head> <body> - <div id="root"></div> - <script src="./bundle.js"></script> + <div id="root"></div> + <script src="./bundle.js"></script> </body> </html>
\ No newline at end of file diff --git a/deploy/index.html b/deploy/index.html index ca5c13e98..532b995f8 100644 --- a/deploy/index.html +++ b/deploy/index.html @@ -1,16 +1,16 @@ -<html> +<html style="overflow: hidden;"> <head> - <title>Dash Web</title> - <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"> - <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script> + <title>Dash Web</title> + <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"> + <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> + <div id="root"></div> + <script src="/bundle.js"></script> </body> </html>
\ No newline at end of file diff --git a/package.json b/package.json index 8c9c865e0..2463afa74 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "nodemon --watch src/server/**/*.ts --exec ts-node src/server/index.ts", + "start": "ts-node-dev -- src/server/index.ts", "build": "webpack --env production", "test": "mocha -r ts-node/register test/**/*.ts", "tsc": "tsc" @@ -20,11 +20,14 @@ "copy-webpack-plugin": "^4.6.0", "css-loader": "^2.1.1", "file-loader": "^3.0.1", + "fork-ts-checker-webpack-plugin": "^1.0.2", "mocha": "^5.2.0", "sass-loader": "^7.1.0", "scss-loader": "0.0.1", "style-loader": "^0.23.1", + "ts-loader": "^5.3.3", "ts-node": "^7.0.1", + "ts-node-dev": "^1.0.0-pre.32", "tslint": "^5.15.0", "tslint-loader": "^3.5.4", "typescript": "^3.4.1", @@ -70,6 +73,7 @@ "@types/prosemirror-history": "^1.0.1", "@types/prosemirror-inputrules": "^1.0.2", "@types/prosemirror-keymap": "^1.0.1", + "@types/prosemirror-menu": "^1.0.1", "@types/prosemirror-model": "^1.7.0", "@types/prosemirror-schema-basic": "^1.0.1", "@types/prosemirror-schema-list": "^1.0.1", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 90213270f..d70e95c0a 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index b0e66787e..8252ba011 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,10 +1,13 @@ import v4 = require('uuid/v4'); import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; -import { Message, Types } from './server/Message'; +import { Message } from './server/Message'; +import { Document } from './fields/Document'; export class Utils { + public static DRAG_THRESHOLD = 4; + public static GenerateGuid(): string { return v4(); } @@ -47,7 +50,7 @@ export class Utils { if (this.logFilter !== undefined && this.logFilter !== message.type) { return; } - let idString = (message._id || message.id || "").padStart(36, ' '); + let idString = (message.id || "").padStart(36, ' '); prefix = prefix.padEnd(16, ' '); console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)}`); } @@ -86,14 +89,27 @@ export class Utils { } } -export function returnTrue() { - return true; +export function OmitKeys(obj: any, keys: any, addKeyFunc?: (dup: any) => void) { + var dup: any = {}; + for (var key in obj) { + if (keys.indexOf(key) === -1) { + dup[key] = obj[key]; + } + } + addKeyFunc && addKeyFunc(dup); + return dup; } -export function returnFalse() { - return false; -} +export function returnTrue() { return true; } + +export function returnFalse() { return false; } + +export function returnOne() { return 1; } + +export function returnZero() { return 0; } export function emptyFunction() { } +export function emptyDocFunction(doc: Document) { } + export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
\ No newline at end of file diff --git a/src/client/Server.ts b/src/client/Server.ts index 857101a33..66e9878d9 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -1,10 +1,10 @@ import { Key } from "../fields/Key"; -import { ObservableMap, action, reaction } from "mobx"; +import { ObservableMap, action, reaction, runInAction } from "mobx"; import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field"; import { Document } from "../fields/Document"; import { SocketStub, FieldMap } from "./SocketStub"; import * as OpenSocket from 'socket.io-client'; -import { Utils } from "./../Utils"; +import { Utils, emptyFunction } from "./../Utils"; import { MessageStore, Types } from "./../server/Message"; export class Server { @@ -12,7 +12,6 @@ export class Server { static Socket: SocketIOClient.Socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); static GUID: string = Utils.GenerateGuid(); - // Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached). // Call this is from within a reaction and test whether the return value is FieldWaiting. public static GetField(fieldid: FieldId): Promise<Opt<Field>>; @@ -59,14 +58,14 @@ export class Server { public static GetFields(fieldIds: FieldId[]): Promise<{ [id: string]: Field }>; public static GetFields(fieldIds: FieldId[], callback: (fields: FieldMap) => any): void; public static GetFields(fieldIds: FieldId[], callback?: (fields: FieldMap) => any): Promise<FieldMap> | void { - let fn = (cb: (fields: FieldMap) => void) => { + let fn = action((cb: (fields: FieldMap) => void) => { let neededFieldIds: FieldId[] = []; let waitingFieldIds: FieldId[] = []; - let existingFields: { [id: string]: Field } = {}; + let existingFields: FieldMap = {}; for (let id of fieldIds) { let field = this.ClientFieldsCached.get(id); - if (!field) { + if (field === undefined) { neededFieldIds.push(id); this.ClientFieldsCached.set(id, FieldWaiting); } else if (field === FieldWaiting) { @@ -79,7 +78,7 @@ export class Server { for (let id of neededFieldIds) { let field = fields[id]; if (field) { - if (!(this.ClientFieldsCached.get(field.Id) instanceof Field)) { + if (this.ClientFieldsCached.get(field.Id) === FieldWaiting) { this.ClientFieldsCached.set(field.Id, field); } else { throw new Error("we shouldn't be trying to replace things that are already in the cache"); @@ -94,17 +93,17 @@ export class Server { } reaction(() => waitingFieldIds.map(id => this.ClientFieldsCached.get(id)), (cachedFields, reaction) => { - if (!cachedFields.some(field => !field)) { + if (!cachedFields.some(field => field === FieldWaiting)) { + const realFields = cachedFields as Opt<Field>[]; reaction.dispose(); - for (let field of cachedFields) { - let realField = field as Field; - existingFields[realField.Id] = realField; - } + waitingFieldIds.forEach((id, index) => { + existingFields[id] = realFields[index]; + }); cb({ ...fields, ...existingFields }); } }, { fireImmediately: true }); })); - }; + }); if (callback) { fn(callback); } else { @@ -127,13 +126,6 @@ export class Server { } } - public static AddDocument(document: Document) { - SocketStub.SEND_ADD_DOCUMENT(document); - } - public static AddDocumentField(doc: Document, key: Key, value: Field) { - console.log("Add doc field " + doc.Title + " " + key.Name + " fid " + value.Id + " " + value); - SocketStub.SEND_ADD_DOCUMENT_FIELD(doc, key, value); - } public static DeleteDocumentField(doc: Document, key: Key) { SocketStub.SEND_DELETE_DOCUMENT_FIELD(doc, key); } @@ -161,18 +153,18 @@ export class Server { } @action - static updateField(field: { _id: string, data: any, type: Types }) { - if (Server.ClientFieldsCached.has(field._id)) { - var f = Server.ClientFieldsCached.get(field._id); + static updateField(field: { id: string, data: any, type: Types }) { + if (Server.ClientFieldsCached.has(field.id)) { + var f = Server.ClientFieldsCached.get(field.id); if (f) { - // console.log("Applying : " + field._id); + // console.log("Applying : " + field.id); f.UpdateFromServer(field.data); - f.init(() => { }); + f.init(emptyFunction); } else { - // console.log("Not applying wa : " + field._id); + // console.log("Not applying wa : " + field.id); } } else { - // console.log("Not applying mi : " + field._id); + // console.log("Not applying mi : " + field.id); } } } diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts index 257973e3d..382a81f66 100644 --- a/src/client/SocketStub.ts +++ b/src/client/SocketStub.ts @@ -2,7 +2,7 @@ import { Key } from "../fields/Key"; import { Field, FieldId, Opt } from "../fields/Field"; import { ObservableMap } from "mobx"; import { Document } from "../fields/Document"; -import { MessageStore, DocumentTransfer } from "../server/Message"; +import { MessageStore, Transferable } from "../server/Message"; import { Utils } from "../Utils"; import { Server } from "./Server"; import { ServerUtils } from "../server/ServerUtil"; @@ -16,35 +16,12 @@ export interface FieldMap { export class SocketStub { static FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); - public static SEND_ADD_DOCUMENT(document: Document) { - - // Send a serialized version of the document to the server - // ...SOCKET(ADD_DOCUMENT, serialied document) - - // server stores each document field in its repository of stored fields - // document.fields.forEach((f, key) => this.FieldStore.set((f as Field).Id, f as Field)); - // let strippedDoc = new Document(document.Id); - // document.fields.forEach((f, key) => { - // if (f) { - // // let args: SetFieldArgs = new SetFieldArgs(f.Id, f.GetValue()) - // let args: Transferable = f.ToJson() - // Utils.Emit(Server.Socket, MessageStore.SetField, args) - // } - // }) - - // // server stores stripped down document (w/ only field id proxies) in the field store - // this.FieldStore.set(document.Id, new Document(document.Id)); - // document.fields.forEach((f, key) => (this.FieldStore.get(document.Id) as Document)._proxies.set(key.Id, (f as Field).Id)); - - console.log("sending " + document.Title); - Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(document.ToJson())); - } public static SEND_FIELD_REQUEST(fieldid: FieldId): Promise<Opt<Field>>; public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void): void; public static SEND_FIELD_REQUEST(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void { let fn = function (cb: (field: Opt<Field>) => void) { - Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => { + Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: Transferable) => { if (field) { ServerUtils.FromJson(field).init(cb); } else { @@ -60,33 +37,15 @@ export class SocketStub { } public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: FieldMap) => any) { - Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => { - let fieldMap: any = {}; - for (let field of fields) { - fieldMap[field._id] = ServerUtils.FromJson(field); - } - callback(fieldMap); + Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: Transferable[]) => { + let fieldMap: FieldMap = {}; + fields.map(field => fieldMap[field.id] = ServerUtils.FromJson(field)); + let proms = Object.values(fieldMap).map(val => + new Promise(resolve => val!.init(resolve))); + Promise.all(proms).then(() => callback(fieldMap)); }); } - public static SEND_ADD_DOCUMENT_FIELD(doc: Document, key: Key, value: Field) { - - // Send a serialized version of the field to the server along with the - // associated info of the document id and key where it is used. - - // ...SOCKET(ADD_DOCUMENT_FIELD, document id, key id, serialized field) - - // server updates its document to hold a proxy mapping from key => fieldId - var document = this.FieldStore.get(doc.Id) as Document; - if (document) { - document._proxies.set(key.Id, value.Id); - } - - // server adds the field to its repository of fields - this.FieldStore.set(value.Id, value); - // Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(doc.ToJson())) - } - public static SEND_DELETE_DOCUMENT_FIELD(doc: Document, key: Key) { // Send a request to delete the field stored under the specified key from the document diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 72e6e57ab..847f604d0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,6 +1,6 @@ import { AudioField } from "../../fields/AudioField"; import { Document } from "../../fields/Document"; -import { Field } from "../../fields/Field"; +import { Field, Opt } from "../../fields/Field"; import { HtmlField } from "../../fields/HtmlField"; import { ImageField } from "../../fields/ImageField"; import { InkField, StrokeData } from "../../fields/InkField"; @@ -26,6 +26,17 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox"; import { PDFBox } from "../views/nodes/PDFBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; +import { Gateway } from "../northstar/manager/Gateway"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { action } from "mobx"; +import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; +import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; +import { AggregateFunction } from "../northstar/model/idea/idea"; +import { Template } from "../views/Templates"; +import { TemplateField } from "../../fields/TemplateField"; +import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; +import { IconBox } from "../views/nodes/IconBox"; +import { IconField } from "../../fields/IconFIeld"; export interface DocumentOptions { x?: number; @@ -40,11 +51,16 @@ export interface DocumentOptions { pany?: number; page?: number; scale?: number; + baseLayout?: string; layout?: string; + templates?: Array<Template>; layoutKeys?: Key[]; viewType?: number; backgroundColor?: string; copyDraggedItems?: boolean; + documentText?: string; + borderRounding?: number; + columnsKey?: Array<Key>; } export namespace Documents { @@ -57,6 +73,7 @@ export namespace Documents { let videoProto: Document; let audioProto: Document; let pdfProto: Document; + let iconProto: Document; const textProtoId = "textProto"; const histoProtoId = "histoProto"; const pdfProtoId = "pdfProto"; @@ -66,6 +83,7 @@ export namespace Documents { const kvpProtoId = "kvpProto"; const videoProtoId = "videoProto"; const audioProtoId = "audioProto"; + const iconProtoId = "iconProto"; export function initProtos(): Promise<void> { return Server.GetFields([textProtoId, histoProtoId, collProtoId, pdfProtoId, imageProtoId, videoProtoId, audioProtoId, webProtoId, kvpProtoId]).then(fields => { @@ -78,6 +96,7 @@ export namespace Documents { videoProto = fields[videoProtoId] as Document || CreateVideoPrototype(); audioProto = fields[audioProtoId] as Document || CreateAudioPrototype(); pdfProto = fields[pdfProtoId] as Document || CreatePdfPrototype(); + iconProto = fields[iconProtoId] as Document || CreateIconPrototype(); }); } function assignOptions(doc: Document, options: DocumentOptions): Document { @@ -85,13 +104,20 @@ export namespace Documents { if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); } if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); } if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); } + if (options.documentText !== undefined) { doc.SetText(KeyStore.DocumentText, options.documentText); } if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } + if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); } + if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); } 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.baseLayout !== undefined) { doc.SetText(KeyStore.BaseLayout, options.baseLayout); } if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.templates !== undefined) { doc.Set(KeyStore.Templates, new TemplateField(options.templates)); } if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } if (options.copyDraggedItems !== undefined) { doc.SetBoolean(KeyStore.CopyDraggedItems, options.copyDraggedItems); } + if (options.borderRounding !== undefined) { doc.SetNumber(KeyStore.BorderRounding, options.borderRounding); } + if (options.columnsKey !== undefined) { doc.SetData(KeyStore.ColumnsKey, options.columnsKey, ListField); } return doc; } @@ -106,7 +132,7 @@ export namespace Documents { } function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { - return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout, baseLayout: layout }); } function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: [T, { new(): U }] | Document, id?: string) { var deleg = doc.MakeDelegate(id); @@ -121,9 +147,10 @@ export namespace Documents { function CreateImagePrototype(): Document { let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"), - { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); + { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString()); imageProto.SetNumber(KeyStore.CurPage, 0); + imageProto.SetData(KeyStore.LayoutFields, [KeyStore.Title], ListField); return imageProto; } @@ -133,6 +160,11 @@ export namespace Documents { histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString()); return histoProto; } + function CreateIconPrototype(): Document { + let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), + { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data] }); + return iconProto; + } function CreateTextPrototype(): Document { let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); @@ -197,9 +229,37 @@ export namespace Documents { export function TextDocument(options: DocumentOptions = {}) { return assignToDelegate(SetInstanceOptions(textProto, options, ["", TextField]).MakeDelegate(), options); } + export function IconDocument(icon: string, options: DocumentOptions = {}) { + return assignToDelegate(SetInstanceOptions(iconProto, { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data], layout: IconBox.LayoutString(), ...options }, [icon, IconField]), options); + } export function PdfDocument(url: string, options: DocumentOptions = {}) { return assignToDelegate(SetInstanceOptions(pdfProto, options, [new URL(url), PDFField]).MakeDelegate(), options); } + export async function DBDocument(url: string, options: DocumentOptions = {}) { + let schemaName = options.title ? options.title : "-no schema-"; + let ctlog = await Gateway.Instance.GetSchema(url, schemaName); + if (ctlog && ctlog.schemas) { + let schema = ctlog.schemas[0]; + let schemaDoc = Documents.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); + let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); + CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { + Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => { + if (field instanceof Document) { + schemaDocuments.push(field); + } else { + var atmod = new ColumnAttributeModel(attr); + let histoOp = new HistogramOperation(schema.displayName!, + new AttributeTransformationModel(atmod, AggregateFunction.None), + new AttributeTransformationModel(atmod, AggregateFunction.Count), + new AttributeTransformationModel(atmod, AggregateFunction.Count)); + schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); + } + })); + }); + return schemaDoc; + } + return Documents.TreeDocument([], { width: 50, height: 100, title: schemaName }); + } export function WebDocument(url: string, options: DocumentOptions = {}) { return assignToDelegate(SetInstanceOptions(webProto, options, [new URL(url), WebField]).MakeDelegate(), options); } @@ -213,13 +273,13 @@ export namespace Documents { if (!makePrototype) { return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id); } - return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id).MakeDelegate(), options); + return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id).MakeDelegate(), options); } export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { - return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options); + return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options); } export function TreeDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { - return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Tree }, [documents, ListField], id), options); + return assignToDelegate(SetInstanceOptions(collProto, { columnsKey: [KeyStore.Title], ...options, viewType: CollectionViewType.Tree }, [documents, ListField], id), options); } export function DockDocument(config: string, options: DocumentOptions, id?: string) { return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options); @@ -242,13 +302,15 @@ export namespace Documents { <div style="position:relative; height:15%; text-align:center; ">` + FormattedTextBox.LayoutString("CaptionKey") + `</div> - </div>`; } + </div>`; + } export function FixedCaption(fieldName: string = "Caption") { return `<div style="position:absolute; height:30px; bottom:0; width:100%"> <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">` + FormattedTextBox.LayoutString(fieldName + "Key") + `</div> - </div>`; } + </div>`; + } function OuterCaption() { return (` diff --git a/src/client/northstar/core/filter/ValueComparision.ts b/src/client/northstar/core/filter/ValueComparision.ts index 80b1242a9..65687a82b 100644 --- a/src/client/northstar/core/filter/ValueComparision.ts +++ b/src/client/northstar/core/filter/ValueComparision.ts @@ -62,13 +62,13 @@ export class ValueComparison { var rawName = this.attributeModel.CodeName; switch (this.Predicate) { case Predicate.STARTS_WITH: - ret += rawName + " !== null && " + rawName + ".StartsWith(" + val + ") "; + ret += rawName + " != null && " + rawName + ".StartsWith(" + val + ") "; return ret; case Predicate.ENDS_WITH: - ret += rawName + " !== null && " + rawName + ".EndsWith(" + val + ") "; + ret += rawName + " != null && " + rawName + ".EndsWith(" + val + ") "; return ret; case Predicate.CONTAINS: - ret += rawName + " !== null && " + rawName + ".Contains(" + val + ") "; + ret += rawName + " != null && " + rawName + ".Contains(" + val + ") "; return ret; default: ret += rawName + " " + op + " " + val + " "; diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts index 6abde4677..c699691a4 100644 --- a/src/client/northstar/dash-fields/HistogramField.ts +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -6,6 +6,7 @@ import { BasicField } from "../../../fields/BasicField"; import { Field, FieldId } from "../../../fields/Field"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { Types } from "../../../server/Message"; +import { OmitKeys } from "../../../Utils"; export class HistogramField extends BasicField<HistogramOperation> { @@ -13,17 +14,8 @@ export class HistogramField extends BasicField<HistogramOperation> { super(data ? data : HistogramOperation.Empty, save, id); } - omitKeys(obj: any, keys: any) { - var dup: any = {}; - for (var key in obj) { - if (keys.indexOf(key) === -1) { - dup[key] = obj[key]; - } - } - return dup; - } toString(): string { - return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand'])); + return JSON.stringify(OmitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand'])); } Copy(): Field { @@ -35,12 +27,11 @@ export class HistogramField extends BasicField<HistogramOperation> { } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.HistogramOp, - data: this.toString(), - _id: this.Id + id: this.Id }; } diff --git a/src/client/northstar/dash-nodes/HistogramBox.scss b/src/client/northstar/dash-nodes/HistogramBox.scss index e899cf15e..06d781263 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.scss +++ b/src/client/northstar/dash-nodes/HistogramBox.scss @@ -1,12 +1,12 @@ .histogrambox-container { padding: 0vw; position: absolute; - top: 0; - left:0; + top: -50%; + left:-50%; text-align: center; width: 100%; height: 100%; - background: black; + background: black; } .histogrambox-xaxislabel { position:absolute; diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index 7df59ef07..ac5f3c8cf 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -1,14 +1,13 @@ import React = require("react"); import { action, computed, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; import { FieldWaiting, Opt } from "../../../fields/Field"; import { Document } from "../../../fields/Document"; import { KeyStore } from "../../../fields/KeyStore"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange'; import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper"; -import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult, Result } from "../../northstar/model/idea/idea"; +import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult } from "../../northstar/model/idea/idea"; import { ModelHelpers } from "../../northstar/model/ModelHelpers"; import { HistogramOperation } from "../../northstar/operations/HistogramOperation"; import { SizeConverter } from "../../northstar/utils/SizeConverter"; @@ -31,8 +30,6 @@ export class HistogramBox extends React.Component<FieldViewProps> { private _dropXDisposer?: DragManager.DragDropDisposer; private _dropYDisposer?: DragManager.DragDropDisposer; - @observable public PanelWidth: number = 100; - @observable public PanelHeight: number = 100; @observable public HistoOp: HistogramOperation = HistogramOperation.Empty; @observable public VisualBinRanges: VisualBinRange[] = []; @observable public ValueRange: number[] = []; @@ -47,10 +44,6 @@ export class HistogramBox extends React.Component<FieldViewProps> { this.BinRanges[1] instanceof AggregateBinRange ? ChartType.VerticalBar : ChartType.HeatMap; } - constructor(props: FieldViewProps) { - super(props); - } - @action dropX = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { @@ -92,7 +85,7 @@ export class HistogramBox extends React.Component<FieldViewProps> { } reaction(() => CurrentUserUtils.NorthstarDBCatalog, (catalog?: Catalog) => this.activateHistogramOperation(catalog), { fireImmediately: true }); reaction(() => [this.VisualBinRanges && this.VisualBinRanges.slice()], () => this.SizeConverter.SetVisualBinRanges(this.VisualBinRanges)); - reaction(() => [this.PanelHeight, this.PanelWidth], () => this.SizeConverter.SetIsSmall(this.PanelWidth < 40 && this.PanelHeight < 40)); + reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], (size: number[]) => this.SizeConverter.SetIsSmall(size[0] < 40 && size[1] < 40)); reaction(() => this.HistogramResult ? this.HistogramResult.binRanges : undefined, (binRanges: BinRange[] | undefined) => { if (binRanges) { @@ -122,7 +115,7 @@ export class HistogramBox extends React.Component<FieldViewProps> { this.props.Document.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => { this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty; if (this.HistoOp !== HistogramOperation.Empty) { - reaction(() => this.props.Document.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); + reaction(() => this.props.Document.GetList(KeyStore.LinkedFromDocs, [] as Document[]), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); reaction(() => this.props.Document.GetList(KeyStore.BrushingDocs, []).length, () => { let brushingDocs = this.props.Document.GetList(KeyStore.BrushingDocs, [] as Document[]); @@ -138,38 +131,39 @@ export class HistogramBox extends React.Component<FieldViewProps> { })); } } + + @action + private onScrollWheel = (e: React.WheelEvent) => { + this.HistoOp.DrillDown(e.deltaY > 0); + e.stopPropagation(); + } + render() { let labelY = this.HistoOp && this.HistoOp.Y ? this.HistoOp.Y.PresentedName : "<...>"; let labelX = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.PresentedName : "<...>"; - var h = this.props.isTopMost ? this.PanelHeight : this.props.Document.GetNumber(KeyStore.Height, 0); - var w = this.props.isTopMost ? this.PanelWidth : this.props.Document.GetNumber(KeyStore.Width, 0); let loff = this.SizeConverter.LeftOffset; let toff = this.SizeConverter.TopOffset; let roff = this.SizeConverter.RightOffset; let boff = this.SizeConverter.BottomOffset; return ( - <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height; })}> - {({ measureRef }) => - <div className="histogrambox-container" ref={measureRef} style={{ transform: `translate(-50%, -50%)` }}> - <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} > - <span className="histogrambox-yaxislabel-text"> - {labelY} - </span> - </div> - <div className="histogrambox-primitives" style={{ - transform: `translate(${loff + 25}px, ${toff}px)`, - width: `calc(100% - ${loff + roff + 25}px)`, - height: `calc(100% - ${toff + boff}px)`, - }}> - <HistogramLabelPrimitives HistoBox={this} /> - <HistogramBoxPrimitives HistoBox={this} /> - </div> - <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} > - {labelX} - </div> - </div> - } - </Measure> + <div className="histogrambox-container" onWheel={this.onScrollWheel}> + <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} > + <span className="histogrambox-yaxislabel-text"> + {labelY} + </span> + </div> + <div className="histogrambox-primitives" style={{ + transform: `translate(${loff + 25}px, ${toff}px)`, + width: `calc(100% - ${loff + roff + 25}px)`, + height: `calc(100% - ${toff + boff}px)`, + }}> + <HistogramLabelPrimitives HistoBox={this} /> + <HistogramBoxPrimitives HistoBox={this} /> + </div> + <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} > + {labelX} + </div> + </div> ); } } diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx index 4c5bdb14b..721bf6a89 100644 --- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx +++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { computed, observable, reaction, runInAction, trace, action } from "mobx"; import { observer } from "mobx-react"; -import { Utils as DashUtils } from '../../../Utils'; +import { Utils as DashUtils, emptyFunction } from '../../../Utils'; import { FilterModel } from "../../northstar/core/filter/FilterModel"; import { ModelHelpers } from "../../northstar/model/ModelHelpers"; import { ArrayUtil } from "../../northstar/utils/ArrayUtil"; @@ -49,7 +49,7 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) { let rawAllBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex === allBrushIndex); if (!rawAllBrushPrim) { - return () => { }; + return emptyFunction; } let allBrushPrim = rawAllBrushPrim; return () => runInAction(() => { @@ -97,7 +97,7 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP let trans1Ypercent = `${yFrom / this.renderDimension * 100}%`; return <line className="histogramboxprimitives-line" key={DashUtils.GenerateGuid()} x1={trans1Xpercent} x2={`${trans2Xpercent}`} y1={trans1Ypercent} y2={`${trans2Ypercent}`} />; } - drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = () => { }) { + drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = emptyFunction) { if (r.height < 0) { r.y += r.height; r.height = -r.height; diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx index 5785fe838..62aebd3c6 100644 --- a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx +++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx @@ -12,7 +12,7 @@ import { HistogramPrimitivesProps } from "./HistogramBoxPrimitives"; @observer export class HistogramLabelPrimitives extends React.Component<HistogramPrimitivesProps> { componentDidMount() { - reaction(() => [this.props.HistoBox.PanelWidth, this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], + reaction(() => [this.props.HistoBox.props.PanelWidth(), this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], (fields) => HistogramLabelPrimitives.computeLabelAngle(fields[0], fields[1], this.props.HistoBox), { fireImmediately: true }); } @@ -35,7 +35,7 @@ export class HistogramLabelPrimitives extends React.Component<HistogramPrimitive if (!vb.length || !sc.Initialized) { return (null); } - let dim = (axis === 0 ? this.props.HistoBox.PanelWidth : this.props.HistoBox.PanelHeight) / ((axis === 0 && vb[axis] instanceof NominalVisualBinRange) ? + let dim = (axis === 0 ? this.props.HistoBox.props.PanelWidth() : this.props.HistoBox.props.PanelHeight()) / ((axis === 0 && vb[axis] instanceof NominalVisualBinRange) ? (12 + 5) : // (<number>FontStyles.AxisLabel.fontSize + 5))); sc.MaxLabelSizes[axis].coords[axis] + 5); @@ -49,12 +49,12 @@ export class HistogramLabelPrimitives extends React.Component<HistogramPrimitive let yStart = (axis === 1 ? r.yFrom - textHeight / 2 : r.yFrom); if (axis === 0 && vb[axis] instanceof NominalVisualBinRange) { - let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.PanelWidth; + let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.props.PanelWidth(); xStart += Math.max(textWidth / 2, (1 - textWidth / space) * textWidth / 2) - textHeight / 2; } let xPercent = axis === 1 ? `${xStart}px` : `${xStart / sc.RenderDimension * 100}%`; - let yPercent = axis === 0 ? `${this.props.HistoBox.PanelHeight - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%`; + let yPercent = axis === 0 ? `${this.props.HistoBox.props.PanelHeight() - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%`; prims.push( <div className="histogramLabelPrimitives-placer" key={DashUtils.GenerateGuid()} style={{ transform: `translate(${xPercent}, ${yPercent})` }}> diff --git a/src/client/northstar/manager/Gateway.ts b/src/client/northstar/manager/Gateway.ts index 8f3b6b11c..d26f2724f 100644 --- a/src/client/northstar/manager/Gateway.ts +++ b/src/client/northstar/manager/Gateway.ts @@ -23,9 +23,9 @@ export class Gateway { } } - public async GetSchema(dbName: string): Promise<Catalog> { + public async GetSchema(pathname: string, schemaname: string): Promise<Catalog> { try { - const json = await this.MakeGetRequest("schema", undefined, dbName); + const json = await this.MakeGetRequest("schema", undefined, { path: pathname, schema: schemaname }); const cat = Catalog.fromJS(json); return cat; } @@ -36,7 +36,7 @@ export class Gateway { public async ClearCatalog(): Promise<void> { try { - const json = await this.MakePostJsonRequest("Datamart/ClearAllAugmentations", {}); + await this.MakePostJsonRequest("Datamart/ClearAllAugmentations", {}); } catch (error) { throw new Error("can not reach northstar's backend"); @@ -144,13 +144,13 @@ export class Gateway { }); } - public async MakeGetRequest(endpoint: string, signal?: AbortSignal, data?: any): Promise<any> { - let url = !data ? Gateway.ConstructUrl(endpoint) : + public async MakeGetRequest(endpoint: string, signal?: AbortSignal, params?: any): Promise<any> { + let url = !params ? Gateway.ConstructUrl(endpoint) : (() => { let newUrl = new URL(Gateway.ConstructUrl(endpoint)); - newUrl.searchParams.append("data", data); + Object.getOwnPropertyNames(params).map(prop => + newUrl.searchParams.append(prop, params[prop])); return Gateway.ConstructUrl(endpoint) + newUrl.search; - return newUrl as any; })(); const response = await fetch(url, @@ -180,18 +180,18 @@ export class Gateway { public static ConstructUrl(appendix: string): string { - let base = Settings.Instance.ServerUrl; + let base = NorthstarSettings.Instance.ServerUrl; if (base.slice(-1) === "/") { base = base.slice(0, -1); } - let url = base + "/" + Settings.Instance.ServerApiPath + "/" + appendix; + let url = base + "/" + NorthstarSettings.Instance.ServerApiPath + "/" + appendix; return url; } } declare var ENV: any; -export class Settings { +export class NorthstarSettings { private _environment: any; @observable @@ -248,10 +248,10 @@ export class Settings { return window.location.origin + "/"; } - private static _instance: Settings; + private static _instance: NorthstarSettings; @action - public Update(environment: any): void { + public UpdateEnvironment(environment: any): void { /*let serverParam = new URL(document.URL).searchParams.get("serverUrl"); if (serverParam) { if (serverParam === "debug") { @@ -278,9 +278,9 @@ export class Settings { this.DegreeOfParallelism = environment.DEGREE_OF_PARALLISM; } - public static get Instance(): Settings { + public static get Instance(): NorthstarSettings { if (!this._instance) { - this._instance = new Settings(); + this._instance = new NorthstarSettings(); } return this._instance; } diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts index 9671e55f8..a92412686 100644 --- a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts +++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts @@ -4,7 +4,7 @@ import { NominalVisualBinRange } from "./NominalVisualBinRange"; import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange"; import { AlphabeticVisualBinRange } from "./AlphabeticVisualBinRange"; import { DateTimeVisualBinRange } from "./DateTimeVisualBinRange"; -import { Settings } from "../../manager/Gateway"; +import { NorthstarSettings } from "../../manager/Gateway"; import { ModelHelpers } from "../ModelHelpers"; import { AttributeTransformationModel } from "../../core/attribute/AttributeTransformationModel"; diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts index 760106023..6a8c9d8cf 100644 --- a/src/client/northstar/operations/HistogramOperation.ts +++ b/src/client/northstar/operations/HistogramOperation.ts @@ -23,7 +23,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons @observable public Links: Document[] = []; @observable public BrushLinks: { l: Document, b: Document }[] = []; @observable public BrushColors: number[] = []; - @observable public FilterModels: FilterModel[] = []; + @observable public BarFilterModels: FilterModel[] = []; @observable public Normalization: number = -1; @observable public X: AttributeTransformationModel; @@ -50,17 +50,24 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons throw new Error("Method not implemented."); } + + @computed public get FilterModels() { + return this.BarFilterModels; + } @action public AddFilterModels(filterModels: FilterModel[]): void { - filterModels.filter(f => f !== null).forEach(fm => this.FilterModels.push(fm)); + filterModels.filter(f => f !== null).forEach(fm => this.BarFilterModels.push(fm)); } @action public RemoveFilterModels(filterModels: FilterModel[]): void { - ArrayUtil.RemoveMany(this.FilterModels, filterModels); + ArrayUtil.RemoveMany(this.BarFilterModels, filterModels); } @computed public get FilterString(): string { + if (this.OverridingFilters.length > 0) { + return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")"; + } let filterModels: FilterModel[] = []; return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true); } @@ -79,6 +86,27 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons return brushes; } + _stackedFilters: (FilterModel[])[] = []; + @action + public DrillDown(up: boolean) { + if (!up) { + if (!this.BarFilterModels.length) + return; + this._stackedFilters.push(this.BarFilterModels.map(f => f)); + this.OverridingFilters.length = 0; + this.OverridingFilters.push(...this._stackedFilters[this._stackedFilters.length - 1]); + this.BarFilterModels.map(fm => fm).map(fm => this.RemoveFilterModels([fm])); + //this.updateHistogram(); + } else { + this.OverridingFilters.length = 0; + if (this._stackedFilters.length) { + this.OverridingFilters.push(...this._stackedFilters.pop()!); + } + // else + // this.updateHistogram(); + } + } + private getAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) { let allAttributes = new Array<AttributeTransformationModel>(histoX, histoY, histoValue); allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None)); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f38b8ca75..56669fb79 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,11 +1,9 @@ -import React = require('react'); -import { observer } from 'mobx-react'; -import { observable, action, computed } from 'mobx'; +import { computed, observable } from 'mobx'; import { Document } from "../../fields/Document"; -import { DocumentView } from '../views/nodes/DocumentView'; -import { KeyStore } from '../../fields/KeyStore'; import { FieldWaiting } from '../../fields/Field'; +import { KeyStore } from '../../fields/KeyStore'; import { ListField } from '../../fields/ListField'; +import { DocumentView } from '../views/nodes/DocumentView'; export class DocumentManager { @@ -27,11 +25,6 @@ export class DocumentManager { // this.DocumentViews = new Array<DocumentView>(); } - public getAllDocumentViews(collection: Document) { - return this.DocumentViews.filter(dv => - dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document === collection); - } - public getDocumentView(toFind: Document): DocumentView | null { let toReturn: DocumentView | null; @@ -40,19 +33,24 @@ export class DocumentManager { //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document; - // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) { if (doc === toFind) { toReturn = view; return; } - let docSrc = doc.GetT(KeyStore.Prototype, Document); - if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { - toReturn = view; - } }); + if (!toReturn) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document; + + let docSrc = doc.GetT(KeyStore.Prototype, Document); + if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { + toReturn = view; + } + }); + } - return (toReturn); + return toReturn; } public getDocumentViews(toFind: Document): DocumentView[] { @@ -73,7 +71,7 @@ export class DocumentManager { } }); - return (toReturn); + return toReturn; } @computed @@ -85,9 +83,8 @@ export class DocumentManager { if (link instanceof Document) { let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document); if (linkToDoc && linkToDoc !== FieldWaiting) { - DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - pairs.push({ a: dv, b: docView1, l: link }); - }); + DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => + pairs.push({ a: dv, b: docView1, l: link })); } } return pairs; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 4849ae9f7..136852e12 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,12 +1,14 @@ import { action } from "mobx"; import { Document } from "../../fields/Document"; +import { FieldWaiting } from "../../fields/Field"; +import { KeyStore } from "../../fields/KeyStore"; +import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { CollectionView } from "../views/collections/CollectionView"; import { DocumentDecorations } from "../views/DocumentDecorations"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { returnFalse } from "../../Utils"; +import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; -export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) { +export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) { let onRowMove = action((e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -38,6 +40,31 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: return onItemDown; } +export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Document) { + let srcTarg = sourceDoc.GetPrototype(); + let draggedDocs = srcTarg ? + srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => + (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; + let draggedFromDocs = srcTarg ? + srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => + (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; + draggedDocs.push(...draggedFromDocs); + if (draggedDocs.length) { + let moddrag = [] as Document[]; + for (const draggedDoc of draggedDocs) { + let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document); + if (doc) moddrag.push(doc); + } + let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); + DragManager.StartDocumentDrag([dragEle], dragData, x, y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } +} + export namespace DragManager { export function Root() { const root = document.getElementById("root"); @@ -112,11 +139,13 @@ export namespace DragManager { constructor(dragDoc: Document[]) { this.draggedDocuments = dragDoc; this.droppedDocuments = dragDoc; + this.xOffset = 0; + this.yOffset = 0; } draggedDocuments: Document[]; droppedDocuments: Document[]; - xOffset?: number; - yOffset?: number; + xOffset: number; + yOffset: number; aliasOnDrop?: boolean; copyOnDrop?: boolean; moveDocument?: MoveFunction; @@ -129,11 +158,13 @@ export namespace DragManager { } export class LinkDragData { - constructor(linkSourceDoc: DocumentView) { - this.linkSourceDocumentView = linkSourceDoc; + constructor(linkSourceDoc: Document, blacklist: Document[] = []) { + this.linkSourceDocument = linkSourceDoc; + this.blacklist = blacklist; } droppedDocuments: Document[] = []; - linkSourceDocumentView: DocumentView; + linkSourceDocument: Document; + blacklist: Document[]; [id: string]: any; } @@ -141,12 +172,16 @@ export namespace DragManager { StartDrag([ele], dragData, downX, downY, options); } + export let AbortDrag: () => void = emptyFunction; + function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; + dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } + MainOverlayTextBox.Instance.SetTextDoc(); let scaleXs: number[] = []; let scaleYs: number[] = []; @@ -175,7 +210,7 @@ export namespace DragManager { dragElement.style.bottom = ""; dragElement.style.left = "0"; dragElement.style.transformOrigin = "0 0"; - dragElement.style.zIndex = "1000"; + dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000"; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; dragElement.style.width = `${rect.width / scaleX}px`; dragElement.style.height = `${rect.height / scaleY}px`; @@ -220,11 +255,11 @@ export namespace DragManager { dragData.aliasOnDrop = e.ctrlKey || e.altKey; } if (e.shiftKey) { - abortDrag(); + AbortDrag(); CollectionDockingView.Instance.StartOtherDrag(docs, { pageX: e.pageX, pageY: e.pageY, - preventDefault: () => { }, + preventDefault: emptyFunction, button: 0 }); } @@ -239,21 +274,32 @@ export namespace DragManager { ); }; - const abortDrag = () => { + let hideDragElements = () => { + dragElements.map(dragElement => dragElement.parentNode == dragDiv && dragDiv.removeChild(dragElement)); + eles.map(ele => (ele.hidden = false)); + }; + let endDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - dragElements.map(dragElement => dragDiv.removeChild(dragElement)); - eles.map(ele => (ele.hidden = false)); + if (options) { + options.handlers.dragComplete({}); + } + } + + AbortDrag = () => { + hideDragElements(); + endDrag(); }; const upHandler = (e: PointerEvent) => { - abortDrag(); - FinishDrag(eles, e, dragData, options, finishDrag); + hideDragElements(); + dispatchDrag(eles, e, dragData, options, finishDrag); + endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { + function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { let removed = dragEles.map(dragEle => { let parent = dragEle.parentElement; if (parent) parent.removeChild(dragEle); @@ -278,11 +324,6 @@ export namespace DragManager { } }) ); - - if (options) { - options.handlers.dragComplete({}); - } } - DocumentDecorations.Instance.Hidden = false; } } diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorKeymap.ts new file mode 100644 index 000000000..00d086b97 --- /dev/null +++ b/src/client/util/ProsemirrorKeymap.ts @@ -0,0 +1,100 @@ +import { Schema } from "prosemirror-model"; +import { + wrapIn, setBlockType, chainCommands, toggleMark, exitCode, + joinUp, joinDown, lift, selectParentNode +} from "prosemirror-commands"; +import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list"; +import { undo, redo } from "prosemirror-history"; +import { undoInputRule } from "prosemirror-inputrules"; +import { Transaction, EditorState } from "prosemirror-state"; + +const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + +export type KeyMap = { [key: string]: any }; + +export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap { + let keys: { [key: string]: any } = {}, type; + + function bind(key: string, cmd: any) { + if (mapKeys) { + let mapped = mapKeys[key]; + if (mapped === false) return; + if (mapped) key = mapped; + } + keys[key] = cmd; + } + + bind("Mod-z", undo); + bind("Shift-Mod-z", redo); + bind("Backspace", undoInputRule); + + if (!mac) { + bind("Mod-y", redo); + } + + bind("Alt-ArrowUp", joinUp); + bind("Alt-ArrowDown", joinDown); + bind("Mod-BracketLeft", lift); + bind("Escape", selectParentNode); + + if (type = schema.marks.strong) { + bind("Mod-b", toggleMark(type)); + bind("Mod-B", toggleMark(type)); + } + if (type = schema.marks.em) { + bind("Mod-i", toggleMark(type)); + bind("Mod-I", toggleMark(type)); + } + if (type = schema.marks.code) { + bind("Mod-`", toggleMark(type)); + } + + if (type = schema.nodes.bullet_list) { + bind("Ctrl-b", wrapInList(type)); + } + if (type = schema.nodes.ordered_list) { + bind("Ctrl-n", wrapInList(type)); + } + if (type = schema.nodes.blockquote) { + bind("Ctrl->", wrapIn(type)); + } + if (type = schema.nodes.hard_break) { + let br = type, cmd = chainCommands(exitCode, (state, dispatch) => { + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); + return true; + } + return false; + }); + bind("Mod-Enter", cmd); + bind("Shift-Enter", cmd); + if (mac) { + bind("Ctrl-Enter", cmd); + } + } + if (type = schema.nodes.list_item) { + bind("Enter", splitListItem(type)); + bind("Shift-Tab", liftListItem(type)); + bind("Tab", sinkListItem(type)); + } + if (type = schema.nodes.paragraph) { + bind("Shift-Ctrl-0", setBlockType(type)); + } + if (type = schema.nodes.code_block) { + bind("Shift-Ctrl-\\", setBlockType(type)); + } + if (type = schema.nodes.heading) { + for (let i = 1; i <= 6; i++) { + bind("Shift-Ctrl-" + i, setBlockType(type, { level: i })); + } + } + if (type = schema.nodes.horizontal_rule) { + let hr = type; + bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + return true; + }); + } + + return keys; +}
\ No newline at end of file diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 92944bec0..9ef71e305 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -7,135 +7,134 @@ 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]; - + 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 } = { - // :: NodeSpec The top level document node. - doc: { - content: "block+" - }, - - // :: NodeSpec A plain paragraph textblock. Represented in the DOM - // as a `<p>` element. - paragraph: { - content: "inline*", - group: "block", - parseDOM: [{ tag: "p" }], - toDOM() { return pDOM; } - }, - - // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. - blockquote: { - content: "block+", - group: "block", - defining: true, - parseDOM: [{ tag: "blockquote" }], - toDOM() { return blockquoteDOM; } - }, - - // :: NodeSpec A horizontal rule (`<hr>`). - horizontal_rule: { - group: "block", - parseDOM: [{ tag: "hr" }], - toDOM() { return hrDOM; } - }, - - // :: NodeSpec A heading textblock, with a `level` attribute that - // should hold the number 1 to 6. Parsed and serialized as `<h1>` to - // `<h6>` elements. - heading: { - attrs: { level: { default: 1 } }, - content: "inline*", - group: "block", - defining: true, - parseDOM: [{ tag: "h1", attrs: { level: 1 } }, - { tag: "h2", attrs: { level: 2 } }, - { tag: "h3", attrs: { level: 3 } }, - { tag: "h4", attrs: { level: 4 } }, - { tag: "h5", attrs: { level: 5 } }, - { tag: "h6", attrs: { level: 6 } }], - toDOM(node: any) { return ["h" + node.attrs.level, 0]; } - }, - - // :: NodeSpec A code listing. Disallows marks or non-text inline - // nodes by default. Represented as a `<pre>` element with a - // `<code>` element inside of it. - code_block: { - content: "text*", - marks: "", - group: "block", - code: true, - defining: true, - parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], - toDOM() { return preDOM; } - }, - - // :: NodeSpec The text node. - text: { - group: "inline" - }, - - // :: NodeSpec An inline image (`<img>`) node. Supports `src`, - // `alt`, and `href` attributes. The latter two default to the empty - // string. - image: { - inline: true, - attrs: { - src: {}, - alt: { default: null }, - title: { default: null } - }, - group: "inline", - draggable: true, - parseDOM: [{ - tag: "img[src]", getAttrs(dom: any) { - return { - src: dom.getAttribute("src"), - title: dom.getAttribute("title"), - alt: dom.getAttribute("alt") - }; - } - }], - toDOM(node: any) { return ["img", node.attrs]; } - }, - - // :: NodeSpec A hard line break, represented in the DOM as `<br>`. - hard_break: { - inline: true, - group: "inline", - selectable: false, - parseDOM: [{ tag: "br" }], - toDOM() { return brDOM; } - }, - - ordered_list: { - ...orderedList, - 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 } - }, - //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*' - } + // :: NodeSpec The top level document node. + doc: { + content: "block+" + }, + + // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // as a `<p>` element. + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{ tag: "p" }], + toDOM() { return pDOM; } + }, + + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. + blockquote: { + content: "block+", + group: "block", + defining: true, + parseDOM: [{ tag: "blockquote" }], + toDOM() { return blockquoteDOM; } + }, + + // :: NodeSpec A horizontal rule (`<hr>`). + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + toDOM() { return hrDOM; } + }, + + // :: NodeSpec A heading textblock, with a `level` attribute that + // should hold the number 1 to 6. Parsed and serialized as `<h1>` to + // `<h6>` elements. + heading: { + attrs: { level: { default: 1 } }, + content: "inline*", + group: "block", + defining: true, + parseDOM: [{ tag: "h1", attrs: { level: 1 } }, + { tag: "h2", attrs: { level: 2 } }, + { tag: "h3", attrs: { level: 3 } }, + { tag: "h4", attrs: { level: 4 } }, + { tag: "h5", attrs: { level: 5 } }, + { tag: "h6", attrs: { level: 6 } }], + toDOM(node: any) { return ["h" + node.attrs.level, 0]; } + }, + + // :: NodeSpec A code listing. Disallows marks or non-text inline + // nodes by default. Represented as a `<pre>` element with a + // `<code>` element inside of it. + code_block: { + content: "text*", + marks: "", + group: "block", + code: true, + defining: true, + parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], + toDOM() { return preDOM; } + }, + + // :: NodeSpec The text node. + text: { + group: "inline" + }, + + // :: NodeSpec An inline image (`<img>`) node. Supports `src`, + // `alt`, and `href` attributes. The latter two default to the empty + // string. + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "img[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt") + }; + } + }], + toDOM(node: any) { return ["img", node.attrs]; } + }, + + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM() { return brDOM; } + }, + + ordered_list: { + ...orderedList, + 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 } + }, + //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*' + } }; const emDOM: DOMOutputSpecArray = ["em", 0]; @@ -145,84 +144,186 @@ const underlineDOM: DOMOutputSpecArray = ["underline", 0]; // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks: { [index: string]: MarkSpec } = { - // :: MarkSpec A link. Has `href` and `title` attributes. `title` - // defaults to the empty string. Rendered and parsed as an `<a>` - // element. - link: { - attrs: { - href: {}, - title: { default: null } - }, - inclusive: false, - parseDOM: [{ - tag: "a[href]", getAttrs(dom: any) { - return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; - } - }], - toDOM(node: any) { return ["a", node.attrs, 0]; } - }, - - // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. - // Has parse rules that also match `<i>` and `font-style: italic`. - em: { - parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], - toDOM() { return emDOM; } - }, - - // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules - // also match `<b>` and `font-weight: bold`. - strong: { - parseDOM: [{ tag: "strong" }, - { tag: "b" }, - { style: "font-weight" }], - toDOM() { return strongDOM; } - }, - - underline: { - parseDOM: [ - { tag: 'u' }, - { style: 'text-decoration=underline' } - ], - toDOM: () => ['span', { - style: 'text-decoration:underline' - }] - }, - - strikethrough: { - parseDOM: [ - { tag: 'strike' }, - { style: 'text-decoration=line-through' }, - { style: 'text-decoration-line=line-through' } - ], - toDOM: () => ['span', { - style: 'text-decoration-line:line-through' - }] - }, - - subscript: { - excludes: 'superscript', - parseDOM: [ - { tag: 'sub' }, - { style: 'vertical-align=sub' } - ], - toDOM: () => ['sub'] - }, - - superscript: { - excludes: 'subscript', - parseDOM: [ - { tag: 'sup' }, - { style: 'vertical-align=super' } - ], - toDOM: () => ['sup'] - }, - - - // :: MarkSpec Code font mark. Represented as a `<code>` element. - code: { - parseDOM: [{ tag: "code" }], - toDOM() { return codeDOM; } - } + // :: MarkSpec A link. Has `href` and `title` attributes. `title` + // defaults to the empty string. Rendered and parsed as an `<a>` + // element. + link: { + attrs: { + href: {}, + title: { default: null } + }, + inclusive: false, + parseDOM: [{ + tag: "a[href]", getAttrs(dom: any) { + return { href: dom.getAttribute("href"), title: dom.getAttribute("title") }; + } + }], + toDOM(node: any) { return ["a", node.attrs, 0]; } + }, + + // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. + // Has parse rules that also match `<i>` and `font-style: italic`. + em: { + parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], + toDOM() { return emDOM; } + }, + + // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules + // also match `<b>` and `font-weight: bold`. + strong: { + parseDOM: [{ tag: "strong" }, + { tag: "b" }, + { style: "font-weight" }], + toDOM() { return strongDOM; } + }, + + underline: { + parseDOM: [ + { tag: 'u' }, + { style: 'text-decoration=underline' } + ], + toDOM: () => ['span', { + style: 'text-decoration:underline' + }] + }, + + strikethrough: { + parseDOM: [ + { tag: 'strike' }, + { style: 'text-decoration=line-through' }, + { style: 'text-decoration-line=line-through' } + ], + toDOM: () => ['span', { + style: 'text-decoration-line:line-through' + }] + }, + + subscript: { + excludes: 'superscript', + parseDOM: [ + { tag: 'sub' }, + { style: 'vertical-align=sub' } + ], + toDOM: () => ['sub'] + }, + + superscript: { + excludes: 'subscript', + parseDOM: [ + { tag: 'sup' }, + { style: 'vertical-align=super' } + ], + toDOM: () => ['sup'] + }, + + + // :: MarkSpec Code font mark. Represented as a `<code>` element. + code: { + parseDOM: [{ tag: "code" }], + toDOM() { return codeDOM; } + }, + + + /* FONTS */ + timesNewRoman: { + parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Times New Roman", Times, serif;' + }] + }, + + arial: { + parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Arial, Helvetica, sans-serif;' + }] + }, + + georgia: { + parseDOM: [{ style: 'font-family: Georgia, serif;' }], + toDOM: () => ['span', { + style: 'font-family: Georgia, serif;' + }] + }, + + comicSans: { + parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Comic Sans MS", cursive, sans-serif;' + }] + }, + + tahoma: { + parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Tahoma, Geneva, sans-serif;' + }] + }, + + impact: { + parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: Impact, Charcoal, sans-serif;' + }] + }, + + crimson: { + parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }], + toDOM: () => ['span', { + style: 'font-family: "Crimson Text", sans-serif;' + }] + }, + + /** FONT SIZES */ + + p10: { + parseDOM: [{ style: 'font-size: 10px;' }], + toDOM: () => ['span', { + style: 'font-size: 10px;' + }] + }, + + p12: { + parseDOM: [{ style: 'font-size: 12px;' }], + toDOM: () => ['span', { + style: 'font-size: 12px;' + }] + }, + + p16: { + parseDOM: [{ style: 'font-size: 16px;' }], + toDOM: () => ['span', { + style: 'font-size: 16px;' + }] + }, + + p24: { + parseDOM: [{ style: 'font-size: 24px;' }], + toDOM: () => ['span', { + style: 'font-size: 24px;' + }] + }, + + p32: { + parseDOM: [{ style: 'font-size: 32px;' }], + toDOM: () => ['span', { + style: 'font-size: 32px;' + }] + }, + + p48: { + parseDOM: [{ style: 'font-size: 48px;' }], + toDOM: () => ['span', { + style: 'font-size: 48px;' + }] + }, + + p72: { + parseDOM: [{ style: 'font-size: 72px;' }], + toDOM: () => ['span', { + style: 'font-size: 72px;' + }] + }, }; // :: Schema diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 9015f21cf..c67cc067a 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -54,15 +54,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)]; let params: any[] = [KeyStore, Documents, ...fieldTypes]; let compiledFunction = new Function(...paramNames, `return ${script}`); + let { capturedVariables = {} } = options; let run = (args: { [name: string]: any } = {}): ScriptResult => { let argsArray: any[] = []; for (let name of customParams) { if (name === "this") { continue; } - argsArray.push(args[name]); + if (name in args) { + argsArray.push(args[name]); + } else { + argsArray.push(capturedVariables[name]); + } } - let thisParam = args.this; + let thisParam = args.this || capturedVariables.this; try { const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); return { success: true, result }; @@ -130,22 +135,30 @@ export interface ScriptOptions { requiredType?: string; addReturn?: boolean; params?: { [name: string]: string }; + capturedVariables?: { [name: string]: Field }; } -export function CompileScript(script: string, { requiredType = "", addReturn = false, params = {} }: ScriptOptions = {}): CompileResult { +export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { + const { requiredType = "", addReturn = false, params = {}, capturedVariables = {} } = options; let host = new ScriptingCompilerHost; - let paramArray: string[] = []; - if ("this" in params) { - paramArray.push("this"); + let paramNames: string[] = []; + if ("this" in params || "this" in capturedVariables) { + paramNames.push("this"); } for (const key in params) { if (key === "this") continue; - paramArray.push(key); + paramNames.push(key); } - let paramString = paramArray.map(key => { + let paramList = paramNames.map(key => { const val = params[key]; return `${key}: ${val}`; - }).join(", "); + }); + for (const key in capturedVariables) { + if (key === "this") continue; + paramNames.push(key); + paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); + } + let paramString = paramList.join(", "); let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} { ${addReturn ? `return ${script};` : script} })`; @@ -157,7 +170,7 @@ export function CompileScript(script: string, { requiredType = "", addReturn = f let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return Run(outputText, paramArray, diagnostics, script, { requiredType, addReturn, params }); + return Run(outputText, paramNames, diagnostics, script, options); } export function OrLiteralType(returnType: string): string { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 5ddaafc72..da66bf3fc 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,7 +1,7 @@ -import { observable, action } from "mobx"; -import { DocumentView } from "../views/nodes/DocumentView"; +import { action, observable } from "mobx"; import { Document } from "../../fields/Document"; -import { Main } from "../views/Main"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; +import { DocumentView } from "../views/nodes/DocumentView"; export namespace SelectionManager { class Manager { @@ -17,14 +17,25 @@ export namespace SelectionManager { if (manager.SelectedDocuments.indexOf(doc) === -1) { manager.SelectedDocuments.push(doc); - doc.props.onActiveChanged(true); + doc.props.whenActiveChanged(true); } } @action DeselectAll(): void { - manager.SelectedDocuments.map(dv => dv.props.onActiveChanged(false)); + manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false)); + manager.SelectedDocuments = []; + MainOverlayTextBox.Instance.SetTextDoc(); + } + @action + ReselectAll() { + let sdocs = manager.SelectedDocuments.map(d => d); manager.SelectedDocuments = []; + return sdocs; + } + @action + ReselectAll2(sdocs: DocumentView[]) { + sdocs.map(s => SelectionManager.SelectDoc(s, true)); } } @@ -48,9 +59,12 @@ export namespace SelectionManager { manager.DeselectAll(); if (found) manager.SelectDoc(found, false); - Main.Instance.SetTextDoc(undefined, undefined); } + export function ReselectAll() { + let sdocs = manager.ReselectAll(); + setTimeout(() => manager.ReselectAll2(sdocs), 0); + } export function SelectedDocuments(): Array<DocumentView> { return manager.SelectedDocuments; } diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx new file mode 100644 index 000000000..55e0eb909 --- /dev/null +++ b/src/client/util/TooltipLinkingMenu.tsx @@ -0,0 +1,86 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "./RichTextSchema"; +import { Schema, NodeType, MarkType } from "prosemirror-model"; +import React = require("react"); +import "./TooltipTextMenu.scss"; +const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; + +const SVG = "http://www.w3.org/2000/svg"; + +//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. +export class TooltipLinkingMenu { + + private tooltip: HTMLElement; + private view: EditorView; + private editorProps: FieldViewProps; + + constructor(view: EditorView, editorProps: FieldViewProps) { + this.view = view; + this.editorProps = editorProps; + this.tooltip = document.createElement("div"); + this.tooltip.className = "tooltipMenu linking"; + + //add the div which is the tooltip + view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + + let target = "https://www.google.com"; + + let link = document.createElement("a"); + link.href = target; + link.textContent = target; + link.target = "_blank"; + link.style.color = "white"; + this.tooltip.appendChild(link); + + this.update(view, undefined); + } + + //updates the tooltip menu when the selection changes + update(view: EditorView, lastState: EditorState | undefined) { + let state = view.state; + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return; + + // Hide the tooltip if the selection is empty + if (state.selection.empty) { + this.tooltip.style.display = "none"; + return; + } + + console.log("STORED:"); + console.log(state.doc.content.firstChild!.content); + + // Otherwise, reposition it and update its content + this.tooltip.style.display = ""; + let { from, to } = state.selection; + 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(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; + let mid = Math.min(start.left, end.left) + width; + + this.tooltip.style.width = "auto"; + this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + } + + destroy() { this.tooltip.remove(); } +} diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index ea580d104..70d9ad772 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -1,8 +1,252 @@ -@import "../views/global_variables"; +@import "../views/globalCssVariables"; + +.ProseMirror-textblock-dropdown { + min-width: 3em; + } + + .ProseMirror-menu { + margin: 0 -4px; + line-height: 1; + } + + .ProseMirror-tooltip .ProseMirror-menu { + width: -webkit-fit-content; + width: fit-content; + white-space: pre; + } + + .ProseMirror-menuitem { + margin-right: 3px; + display: inline-block; + } + + .ProseMirror-menuseparator { + // border-right: 1px solid #ddd; + margin-right: 3px; + } + + .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { + font-size: 90%; + white-space: nowrap; + } + + .ProseMirror-menu-dropdown { + vertical-align: 1px; + cursor: pointer; + position: relative; + padding-right: 15px; + margin: 3px; + background: #333333; + border-radius: 3px; + text-align: center; + } + + .ProseMirror-menu-dropdown-wrap { + padding: 1px 0 1px 4px; + display: inline-block; + position: relative; + } + + .ProseMirror-menu-dropdown:after { + content: ""; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 2px); + } + + .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { + position: absolute; + background: $dark-color; + color:white; + border: 1px solid rgb(223, 223, 223); + padding: 2px; + } + + .ProseMirror-menu-dropdown-menu { + z-index: 15; + min-width: 6em; + } + + .linking { + text-align: center; + } + + .ProseMirror-menu-dropdown-item { + cursor: pointer; + padding: 2px 8px 2px 4px; + width: auto; + } + + .ProseMirror-menu-dropdown-item:hover { + background: #2e2b2b; + } + + .ProseMirror-menu-submenu-wrap { + position: relative; + margin-right: -4px; + } + + .ProseMirror-menu-submenu-label:after { + content: ""; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid currentColor; + opacity: .6; + position: absolute; + right: 4px; + top: calc(50% - 4px); + } + + .ProseMirror-menu-submenu { + display: none; + min-width: 4em; + left: 100%; + top: -3px; + } + + .ProseMirror-menu-active { + background: #eee; + border-radius: 4px; + } + + .ProseMirror-menu-active { + background: #eee; + border-radius: 4px; + } + + .ProseMirror-menu-disabled { + opacity: .3; + } + + .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { + display: block; + } + + .ProseMirror-menubar { + border-top-left-radius: inherit; + border-top-right-radius: inherit; + position: relative; + min-height: 1em; + color: white; + padding: 1px 6px; + top: 0; left: 0; right: 0; + border-bottom: 1px solid silver; + background:$dark-color; + z-index: 10; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow: visible; + } + + .ProseMirror-icon { + display: inline-block; + line-height: .8; + vertical-align: -2px; /* Compensate for padding */ + padding: 2px 8px; + cursor: pointer; + } + + .ProseMirror-menu-disabled.ProseMirror-icon { + cursor: default; + } + + .ProseMirror-icon svg { + fill: currentColor; + height: 1em; + } + + .ProseMirror-icon span { + vertical-align: text-top; + } +.ProseMirror-example-setup-style hr { + padding: 2px 10px; + border: none; + margin: 1em 0; + } + + .ProseMirror-example-setup-style hr:after { + content: ""; + display: block; + height: 1px; + background-color: silver; + line-height: 2px; + } + + .ProseMirror ul, .ProseMirror ol { + padding-left: 30px; + } + + .ProseMirror blockquote { + padding-left: 1em; + border-left: 3px solid #eee; + margin-left: 0; margin-right: 0; + } + + .ProseMirror-example-setup-style img { + cursor: default; + } + + .ProseMirror-prompt { + background: white; + padding: 5px 10px 5px 15px; + border: 1px solid silver; + position: fixed; + border-radius: 3px; + z-index: 11; + box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); + } + + .ProseMirror-prompt h5 { + margin: 0; + font-weight: normal; + font-size: 100%; + color: #444; + } + + .ProseMirror-prompt input[type="text"], + .ProseMirror-prompt textarea { + background: #eee; + border: none; + outline: none; + } + + .ProseMirror-prompt input[type="text"] { + padding: 0 4px; + } + + .ProseMirror-prompt-close { + position: absolute; + left: 2px; top: 1px; + color: #666; + border: none; background: transparent; padding: 0; + } + + .ProseMirror-prompt-close:after { + content: "✕"; + font-size: 12px; + } + + .ProseMirror-invalid { + background: #ffc; + border: 1px solid #cc7; + border-radius: 4px; + padding: 5px 10px; + position: absolute; + min-width: 10em; + } + + .ProseMirror-prompt-buttons { + margin-top: 5px; + display: none; + } .tooltipMenu { position: absolute; - z-index: 20; + z-index: 200; background: $dark-color; border: 1px solid silver; border-radius: 4px; @@ -10,6 +254,7 @@ margin-bottom: 7px; -webkit-transform: translateX(-50%); transform: translateX(-50%); + pointer-events: all; } .tooltipMenu:before { @@ -39,7 +284,7 @@ display: inline-block; border-right: 1px solid rgba(0, 0, 0, 0.2); //color: rgb(19, 18, 18); - color: $light-color; + color: white; line-height: 1; padding: 0px 2px; margin: 1px; @@ -52,4 +297,8 @@ .underline {text-decoration: underline} .superscript {vertical-align:super} .subscript { vertical-align:sub } - .strikethrough {text-decoration-line:line-through}
\ No newline at end of file + .strikethrough {text-decoration-line:line-through} + .font-size-indicator { + font-size: 12px; + padding-right: 0px; + } diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index bd5753093..14af4bdfd 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,34 +1,59 @@ import { action, IReactionDisposer, reaction } from "mobx"; -import { baseKeymap } from "prosemirror-commands"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection } from "prosemirror-state"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { schema } from "./RichTextSchema"; -import { Schema, NodeType } from "prosemirror-model"; +import { Schema, NodeType, MarkType } from "prosemirror-model"; import React = require("react"); import "./TooltipTextMenu.scss"; const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList } from 'prosemirror-schema-list'; -import { faListUl } from '@fortawesome/free-solid-svg-icons'; +import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; +import { liftTarget } from 'prosemirror-transform'; +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; +const SVG = "http://www.w3.org/2000/svg"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { - private tooltip: HTMLElement; + public tooltip: HTMLElement; + private num_icons = 0; + private view: EditorView; + private fontStyles: MarkType[]; + private fontSizes: MarkType[]; + private listTypes: NodeType[]; + private editorProps: FieldViewProps; + private state: EditorState; + private fontSizeToNum: Map<MarkType, number>; + private fontStylesToName: Map<MarkType, string>; + private listTypeToIcon: Map<NodeType, string>; + private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); + //dropdown doms + private fontSizeDom: Node; + private fontStyleDom: Node; + private listTypeBtnDom: Node; - constructor(view: EditorView) { + constructor(view: EditorView, editorProps: FieldViewProps) { + this.view = view; + this.state = view.state; + this.editorProps = editorProps; this.tooltip = document.createElement("div"); this.tooltip.className = "tooltipMenu"; //add the div which is the tooltip - view.dom.parentNode!.appendChild(this.tooltip); + view.dom.parentNode!.parentNode!.appendChild(this.tooltip); //add additional icons library.add(faListUl); - //add the buttons to the tooltip let items = [ { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, @@ -37,36 +62,209 @@ 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") }, - //this doesn't work currently - look into notion of active block - { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, + // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, + // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, + // { command: lift, dom: this.icon("<", "lift") }, ]; - items.forEach(({ dom }) => this.tooltip.appendChild(dom)); - - //pointer down handler to activate button effects - this.tooltip.addEventListener("pointerdown", e => { - e.preventDefault(); - view.focus(); - items.forEach(({ command, dom }) => { - if (dom.contains(e.target as Node)) { - 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" - } + //add menu items + items.forEach(({ dom, command }) => { + this.tooltip.appendChild(dom); + + //pointer down handler to activate button effects + dom.addEventListener("pointerdown", e => { + e.preventDefault(); + view.focus(); + command(view.state, view.dispatch, view); }); + }); + //list of font styles + this.fontStylesToName = new Map(); + this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman"); + this.fontStylesToName.set(schema.marks.arial, "Arial"); + this.fontStylesToName.set(schema.marks.georgia, "Georgia"); + this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS"); + this.fontStylesToName.set(schema.marks.tahoma, "Tahoma"); + this.fontStylesToName.set(schema.marks.impact, "Impact"); + this.fontStylesToName.set(schema.marks.crimson, "Crimson Text"); + this.fontStyles = Array.from(this.fontStylesToName.keys()); + + //font sizes + this.fontSizeToNum = new Map(); + this.fontSizeToNum.set(schema.marks.p10, 10); + this.fontSizeToNum.set(schema.marks.p12, 12); + this.fontSizeToNum.set(schema.marks.p16, 16); + this.fontSizeToNum.set(schema.marks.p24, 24); + this.fontSizeToNum.set(schema.marks.p32, 32); + this.fontSizeToNum.set(schema.marks.p48, 48); + this.fontSizeToNum.set(schema.marks.p72, 72); + this.fontSizes = Array.from(this.fontSizeToNum.keys()); + + //list types + this.listTypeToIcon = new Map(); + this.listTypeToIcon.set(schema.nodes.bullet_list, ":"); + this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); + this.listTypes = Array.from(this.listTypeToIcon.keys()); + this.update(view, undefined); } + //label of dropdown will change to given label + updateFontSizeDropdown(label: string) { + //filtering function - might be unecessary + let cut = (arr: MenuItem[]) => arr.filter(x => x); + + //font SIZES + let fontSizeBtns: MenuItem[] = []; + this.fontSizeToNum.forEach((number, mark) => { + fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); + }); + + if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } + this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), { + label: label, + css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;" + }) as MenuItem).render(this.view).dom; + this.tooltip.appendChild(this.fontSizeDom); + } + + //label of dropdown will change to given label + updateFontStyleDropdown(label: string) { + //filtering function - might be unecessary + let cut = (arr: MenuItem[]) => arr.filter(x => x); + + //font STYLES + let fontBtns: MenuItem[] = []; + this.fontStylesToName.forEach((name, mark) => { + fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); + }); + + if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } + this.fontStyleDom = (new Dropdown(cut(fontBtns), { + label: label, + css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;" + }) as MenuItem).render(this.view).dom; + + this.tooltip.appendChild(this.fontStyleDom); + } + + //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown + updateListItemDropdown(label: string, listTypeBtn: Node) { + //remove old btn + if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); } + + //Make a dropdown of all list types + let toAdd: MenuItem[] = []; + this.listTypeToIcon.forEach((icon, type) => { + toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); + }); + //option to remove the list formatting + toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); + + listTypeBtn = (new Dropdown(toAdd, { + label: label, + css: "color:white; width: 40px;" + }) as MenuItem).render(this.view).dom; + + //add this new button and return it + this.tooltip.appendChild(listTypeBtn); + return listTypeBtn; + } + + //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text + changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { + let { empty, $cursor, ranges } = view.state.selection as TextSelection; + let state = view.state; + let dispatch = view.dispatch; + + //remove all other active font marks + fontMarks.forEach((type) => { + if (dispatch) { + if ($cursor) { + if (type.isInSet(state.storedMarks || $cursor.marks())) { + dispatch(state.tr.removeStoredMark(type)); + } + } else { + let has = false, tr = state.tr; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + has = state.doc.rangeHasMark($from.pos, $to.pos, type); + } + for (let i of ranges) { + let { $from, $to } = i; + if (has) { + toggleMark(type)(view.state, view.dispatch, view); + } + } + } + } + }); //actually apply font + return toggleMark(markType)(view.state, view.dispatch, view); + } + + //remove all node typeand apply the passed-in one to the selected text + changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) { + //remove old + liftListItem(schema.nodes.list_item)(view.state, view.dispatch); + if (nodeType) { //add new + wrapInList(nodeType)(view.state, view.dispatch); + } + } + + //makes a button for the drop down FOR MARKS + //css is the style you want applied to the button + dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) { + return new MenuItem({ + title: "", + label: label, + execEvent: "", + class: "menuicon", + css: css, + enable(state) { return true; }, + run() { + changeToMarkInGroup(markType, view, groupMarks); + } + }); + } + + //makes a button for the drop down FOR NODE TYPES + //css is the style you want applied to the button + dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) { + return new MenuItem({ + title: "", + label: label, + execEvent: "", + class: "menuicon", + css: css, + enable(state) { return true; }, + run() { + changeToNodeInGroup(nodeType, view, groupNodes); + } + }); + } + // Helper function to create menu icons icon(text: string, name: string) { let span = document.createElement("span"); - span.className = "menuicon " + name; + span.className = name + " menuicon"; span.title = name; span.textContent = text; + span.style.color = "white"; return span; } + //method for checking whether node can be inserted + canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) { + let $from = state.selection.$from; + for (let d = $from.depth; d >= 0; d--) { + let index = $from.index(d); + if ($from.node(d).canReplaceWith(index, index, nodeType)) return true; + } + return false; + } + + //adapted this method - use it to check if block has a tag (ie bulleting) blockActive(type: NodeType<Schema<string, string>>, state: EditorState) { let attrs = {}; @@ -85,15 +283,6 @@ export class TooltipTextMenu { } } - //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"); - icon.className = "menuicon fa fa-smile-o"; - span.appendChild(icon); - return span; - } - // Create an icon for a heading at the given level heading(level: number) { return { @@ -124,14 +313,61 @@ export class TooltipTextMenu { // Find a center-ish x position from the selection endpoints (when // crossing lines, end may be more to the left) let left = Math.max((start.left + end.left) / 2, start.left + 3); - this.tooltip.style.left = (left - box.left) + "px"; - let width = Math.abs(start.left - end.left) / 2; + this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; let mid = Math.min(start.left, end.left) + width; - //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 - this.tooltip.style.width = 122 + "px"; - this.tooltip.style.bottom = (box.bottom - start.top) + "px"; + this.tooltip.style.width = 225 + "px"; + this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + + //UPDATE LIST ITEM DROPDOWN + this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom); + + //UPDATE FONT STYLE DROPDOWN + let activeStyles = this.activeMarksOnSelection(this.fontStyles); + if (activeStyles.length === 1) { + // if we want to update something somewhere with active font name + let fontName = this.fontStylesToName.get(activeStyles[0]); + if (fontName) { this.updateFontStyleDropdown(fontName); } + } else if (activeStyles.length === 0) { + //crimson on default + this.updateFontStyleDropdown("Crimson Text"); + } else { + this.updateFontStyleDropdown("Various"); + } + + //UPDATE FONT SIZE DROPDOWN + let activeSizes = this.activeMarksOnSelection(this.fontSizes); + if (activeSizes.length === 1) { //if there's only one active font size + let size = this.fontSizeToNum.get(activeSizes[0]); + if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } + } else if (activeSizes.length === 0) { + //should be 14 on default + this.updateFontSizeDropdown("14 pt"); + } else { //multiple font sizes selected + this.updateFontSizeDropdown("Various"); + } + } + + //finds all active marks on selection in given group + activeMarksOnSelection(markGroup: MarkType[]) { + //current selection + let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; + let state = this.view.state; + let dispatch = this.view.dispatch; + + let activeMarks = markGroup.filter(mark => { + if (dispatch) { + let has = false, tr = state.tr; + for (let i = 0; !has && i < ranges.length; i++) { + let { $from, $to } = ranges[i]; + return state.doc.rangeHasMark($from.pos, $to.pos, mark); + } + } + return false; + }); + return activeMarks; } destroy() { this.tooltip.remove(); } -}
\ No newline at end of file +} diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 0964d5ff3..5008ddfcf 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index f6830d9cd..fe884ca85 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,8 +1,8 @@ -@import "global_variables"; +@import "globalCssVariables"; .contextMenu-cont { position: absolute; display: flex; - z-index: 1000; + z-index: $contextMenu-zindex; box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; flex-direction: column; } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 3871be35b..b00b48c9e 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,13 +1,18 @@ -@import "global_variables"; +@import "globalCssVariables"; -#documentDecorations-container { +$linkGap : 3px; +.documentDecorations { + position: absolute; +} + +.documentDecorations-container { + z-index: $docDecorations-zindex; position: absolute; top: 0; left: 0; display: grid; - z-index: 1000; grid-template-rows: 20px 8px 1fr 8px; - grid-template-columns: 8px 8px 1fr 8px 8px; + grid-template-columns: 8px 16px 1fr 8px 8px; pointer-events: none; #documentDecorations-centerCont { @@ -59,13 +64,12 @@ #documentDecorations-rightResizer { cursor: ew-resize; } - - .title { - width: 100%; + .title{ background: lightblue; grid-column-start: 3; grid-column-end: 4; pointer-events: auto; + overflow: hidden; } } @@ -77,6 +81,7 @@ grid-column-end: 6; pointer-events: all; text-align: center; + cursor: pointer; } .documentDecorations-minimizeButton { @@ -86,6 +91,12 @@ grid-column-end: 3; pointer-events: all; text-align: center; + cursor: pointer; + position: absolute; + left: 0px; + top: 0px; + width: $MINIMIZED_ICON_SIZE; + height: $MINIMIZED_ICON_SIZE; } .documentDecorations-background { @@ -125,9 +136,9 @@ // cursor: ew-resize; // } // } - .linkFlyout { grid-column: 2/4; + margin-top: $linkGap; } .linkButton-empty:hover { @@ -152,7 +163,7 @@ .linkButton-linker { margin-left: 5px; - margin-top: 10px; + margin-top: $linkGap; height: 20px; width: 20px; text-align: center; @@ -167,10 +178,9 @@ transform: scale(1.05); } -.linkButton-empty { +.linkButton-empty, .linkButton-nonempty { height: 20px; width: 20px; - margin-top: 10px; border-radius: 50%; opacity: 0.9; pointer-events: auto; @@ -184,14 +194,18 @@ display: flex; justify-content: center; align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } } -.linkButton-nonempty { - height: 20px; - width: 20px; - margin-top: 10px; - border-radius: 50%; - opacity: 0.9; +.templating-menu { + position: absolute; + bottom: 0; + left: 50px; pointer-events: auto; background-color: $dark-color; color: $light-color; @@ -207,4 +221,37 @@ .fa-icon-link { margin-top: 3px; +} +.templating-button { + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + background-color: $dark-color; + color: $light-color; + text-align: center; + cursor: pointer; + + &:hover { + background: $main-accent; + transform: scale(1.05); + } +} + +#template-list { + position: absolute; + top: 0; + left: 30px; + width: 150px; + line-height: 25px; + max-height: 175px; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + input { + margin-right: 10px; + } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index b730912b5..cc5b754f0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,20 +1,26 @@ -import { action, computed, observable, trace, runInAction } from "mobx"; +import { action, computed, observable, runInAction, untracked, reaction } from "mobx"; import { observer } from "mobx-react"; import { Key } from "../../fields/Key"; -//import ContentEditable from 'react-contenteditable' import { KeyStore } from "../../fields/KeyStore"; import { ListField } from "../../fields/ListField"; import { NumberField } from "../../fields/NumberField"; -import { Document } from "../../fields/Document"; import { TextField } from "../../fields/TextField"; -import { DragManager } from "../util/DragManager"; +import { Document } from "../../fields/Document"; +import { emptyFunction, Utils } from "../../Utils"; +import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; -import { CollectionView } from "./collections/CollectionView"; +import { undoBatch } from "../util/UndoManager"; import './DocumentDecorations.scss'; +import { MainOverlayTextBox } from "./MainOverlayTextBox"; import { DocumentView } from "./nodes/DocumentView"; import { LinkMenu } from "./nodes/LinkMenu"; +import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); -import { FieldWaiting } from "../../fields/Field"; +import { Template, Templates } from "./Templates"; +import { CompileScript } from "../util/Scripting"; +import { IconBox } from "./nodes/IconBox"; +import { FieldValue, Field } from "../../fields/Field"; +import { Documents } from "../documents/Documents"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -27,37 +33,36 @@ library.add(faLink); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; - private _resizer = ""; private _isPointerDown = false; + private _resizing = ""; private keyinput: React.RefObject<HTMLInputElement>; - private _documents: DocumentView[] = SelectionManager.SelectedDocuments(); private _resizeBorderWidth = 16; - private _linkBoxHeight = 30; + private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _linkButton = React.createRef<HTMLDivElement>(); private _linkerButton = React.createRef<HTMLDivElement>(); - //@observable private _title: string = this._documents[0].props.Document.Title; - @observable private _title: string = this._documents.length > 0 ? this._documents[0].props.Document.Title : ""; + private _downX = 0; + private _downY = 0; + @observable private _minimizedX = 0; + @observable private _minimizedY = 0; + @observable private _title: string = ""; + @observable private _edtingTitle = false; @observable private _fieldKey: Key = KeyStore.Title; @observable private _hidden = false; @observable private _opacity = 1; - @observable private _dragging = false; - + @observable private _iconifying = false; + @observable public Interacting = false; constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; - this.handleChange = this.handleChange.bind(this); this.keyinput = React.createRef(); + reaction(() => SelectionManager.SelectedDocuments().slice(), (docs) => docs.length === 0 && (this._edtingTitle = false)); } - @action - handleChange = (event: any) => { - this._title = event.target.value; - } - - @action - enterPressed = (e: any) => { + @action titleChanged = (event: any) => { this._title = event.target.value; } + @action titleBlur = () => { this._edtingTitle = false; } + @action titleEntered = (e: any) => { var key = e.keyCode || e.which; // enter pressed if (key === 13) { @@ -65,25 +70,52 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (text[0] === '#') { let command = text.slice(1, text.length); this._fieldKey = new Key(command); - // if (command === "Title" || command === "title") { - // this._fieldKey = KeyStore.Title; - // } - // else if (command === "Width" || command === "width") { - // this._fieldKey = KeyStore.Width; - // } - this._title = "changed"; - // TODO: Change field with switch statement + this._title = this.selectionTitle; } else { - this._title = "changed"; + if (SelectionManager.SelectedDocuments().length > 0) { + let field = SelectionManager.SelectedDocuments()[0].props.Document.Get(this._fieldKey); + if (field instanceof NumberField) { + SelectionManager.SelectedDocuments().forEach(d => + d.props.Document.SetNumber(this._fieldKey, +this._title)); + } else if (field instanceof TextField || true) { + SelectionManager.SelectedDocuments().forEach(d => + d.props.Document.SetText(this._fieldKey, this._title)); + } + } } e.target.blur(); } } + @action onTitleDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + e.stopPropagation(); + this.onBackgroundDown(e); + document.removeEventListener("pointermove", this.onTitleMove); + document.removeEventListener("pointerup", this.onTitleUp); + document.addEventListener("pointermove", this.onTitleMove); + document.addEventListener("pointerup", this.onTitleUp); + } + @action onTitleMove = (e: PointerEvent): void => { + if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { + this.Interacting = true; + } + if (this.Interacting) this.onBackgroundMove(e); + e.stopPropagation(); + } + @action onTitleUp = (e: PointerEvent): void => { + if (Math.abs(e.clientX - this._downX) < 4 || Math.abs(e.clientY - this._downY) < 4) { + this._title = this.selectionTitle; + this._edtingTitle = true; + } + document.removeEventListener("pointermove", this.onTitleMove); + document.removeEventListener("pointerup", this.onTitleUp); + this.onBackgroundUp(e); + } @computed get Bounds(): { x: number, y: number, b: number, r: number } { - this._documents = SelectionManager.SelectedDocuments(); return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.isTopMost) { return bounds; @@ -98,18 +130,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); } - - @computed - public get Hidden() { return this._hidden; } - public set Hidden(value: boolean) { this._hidden = value; } - - _lastDrag: number[] = [0, 0]; onBackgroundDown = (e: React.PointerEvent): void => { document.removeEventListener("pointermove", this.onBackgroundMove); - document.addEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); + document.addEventListener("pointermove", this.onBackgroundMove); document.addEventListener("pointerup", this.onBackgroundUp); - this._lastDrag = [e.clientX, e.clientY]; e.stopPropagation(); e.preventDefault(); } @@ -117,25 +142,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onBackgroundMove = (e: PointerEvent): void => { let dragDocView = SelectionManager.SelectedDocuments()[0]; - const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); + const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + dragData.xOffset = xoff; + dragData.yOffset = yoff; dragData.aliasOnDrop = false; - dragData.xOffset = e.x - left; - dragData.yOffset = e.y - top; - let move = SelectionManager.SelectedDocuments()[0].props.moveDocument; - dragData.moveDocument = move; - this._dragging = true; + dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; + this.Interacting = true; + this._hidden = true; document.removeEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); - DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentRef.current!), dragData, e.x, e.y, { - handlers: { - dragComplete: action(() => this._dragging = false), - }, + document.removeEventListener("pointermove", this.onTitleMove); + document.removeEventListener("pointerup", this.onTitleUp); + DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, { + handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) }, hideSource: true }); e.stopPropagation(); } + @action onBackgroundUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); @@ -157,6 +184,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (e.button === 0) { } } + @undoBatch @action onCloseUp = (e: PointerEvent): void => { e.stopPropagation(); @@ -167,34 +195,114 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.removeEventListener("pointerup", this.onCloseUp); } } + @action onMinimizeDown = (e: React.PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { + this._downX = e.pageX; + this._downY = e.pageY; + let selDoc = SelectionManager.SelectedDocuments()[0]; + let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); + this._minimizedX = selDocPos[0] + 12; + this._minimizedY = selDocPos[1] + 12; document.removeEventListener("pointermove", this.onMinimizeMove); document.addEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); document.addEventListener("pointerup", this.onMinimizeUp); } } + + @action onMinimizeMove = (e: PointerEvent): void => { e.stopPropagation(); - if (e.button === 0) { + if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) { + let selDoc = SelectionManager.SelectedDocuments()[0]; + let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); + let snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20; + this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX; + this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY; + let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); + Promise.all(selectedDocs.map(async selDoc => await this.getIconDoc(selDoc))).then(minDocSet => + this.moveIconDocs(SelectionManager.SelectedDocuments()) + ); + this._iconifying = snapped; } } + + + @action createIcon = (docView: DocumentView, layoutString: string): Document => { + let doc = docView.props.Document; + let iconDoc = Documents.IconDocument(layoutString); + iconDoc.SetText(KeyStore.Title, "ICON" + doc.Title) + iconDoc.SetBoolean(KeyStore.IsMinimized, false); + iconDoc.SetNumber(KeyStore.NativeWidth, 0); + iconDoc.SetNumber(KeyStore.NativeHeight, 0); + iconDoc.SetNumber(KeyStore.X, doc.GetNumber(KeyStore.X, 0)); + iconDoc.SetNumber(KeyStore.Y, doc.GetNumber(KeyStore.Y, 0) - 24); + iconDoc.Set(KeyStore.Prototype, doc); + iconDoc.Set(KeyStore.MaximizedDoc, doc); + doc.Set(KeyStore.MinimizedDoc, iconDoc); + docView.props.addDocument && docView.props.addDocument(iconDoc, false); + return iconDoc; + } + @action + public getIconDoc = async (docView: DocumentView): Promise<Document | undefined> => { + let doc = docView.props.Document; + let iconDoc = await doc.GetTAsync(KeyStore.MinimizedDoc, Document).then(async mindoc => + mindoc ? mindoc : + await doc.GetTAsync(KeyStore.BackgroundLayout, TextField).then(async field => + (field instanceof TextField) ? this.createIcon(docView, field.Data) : + await doc.GetTAsync(KeyStore.Layout, TextField).then(field => + (field instanceof TextField) ? this.createIcon(docView, field.Data) : undefined))); + if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined) + SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!); + return iconDoc; + } + @action onMinimizeUp = (e: PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { - SelectionManager.SelectedDocuments().map(dv => dv.minimize()); document.removeEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); + let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); + Promise.all(selectedDocs.map(async selDoc => await this.getIconDoc(selDoc))).then(minDocSet => { + let minDocs = minDocSet.filter(minDoc => minDoc instanceof Document).map(minDoc => minDoc as Document); + minDocs.map(minDoc => { + minDoc.SetNumber(KeyStore.X, minDocs[0].GetNumber(KeyStore.X, 0)); + minDoc.SetNumber(KeyStore.Y, minDocs[0].GetNumber(KeyStore.Y, 0)); + minDoc.SetData(KeyStore.LinkTags, minDocs, ListField); + if (this._iconifying && selectedDocs[0].props.removeDocument) { + selectedDocs[0].props.removeDocument(minDoc); + (minDoc.Get(KeyStore.MaximizedDoc, false) as Document)!.Set(KeyStore.MinimizedDoc, undefined); + } + }); + runInAction(() => this._minimizedX = this._minimizedY = 0); + if (!this._iconifying) selectedDocs[0].props.toggleMinimized(); + this._iconifying = false; + }); } } + moveIconDocs(selViews: DocumentView[], minDocSet?: FieldValue<Field>[]) { + selViews.map(selDoc => { + let minDoc = selDoc.props.Document.Get(KeyStore.MinimizedDoc); + if (minDoc instanceof Document) { + let zoom = selDoc.props.Document.GetNumber(KeyStore.ZoomBasis, 1); + let where = (selDoc.props.ScreenToLocalTransform()).scale(selDoc.props.ContentScaling()).scale(1 / zoom). + transformPoint(this._minimizedX - 12, this._minimizedY - 12); + minDoc.SetNumber(KeyStore.X, where[0] + selDoc.props.Document.GetNumber(KeyStore.X, 0)); + minDoc.SetNumber(KeyStore.Y, where[1] + selDoc.props.Document.GetNumber(KeyStore.Y, 0)); + } + }); + } + @action onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { this._isPointerDown = true; - this._resizer = e.currentTarget.id; + this._resizing = e.currentTarget.id; + this.Interacting = true; document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -219,10 +327,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (this._linkerButton.current !== null) { document.removeEventListener("pointermove", this.onLinkerButtonMoved); document.removeEventListener("pointerup", this.onLinkerButtonUp); - let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]); + let selDoc = SelectionManager.SelectedDocuments()[0]; + let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.GetPrototype() : undefined; + let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { handlers: { - dragComplete: action(() => { }), + dragComplete: action(emptyFunction), }, hideSource: false }); @@ -245,38 +355,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } onLinkButtonMoved = async (e: PointerEvent) => { - if (this._linkButton.current !== null) { + if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); - let sourceDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document); - let draggedDocs = (srcTarg && srcTarg !== FieldWaiting) ? - srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => - (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; - let draggedFromDocs = (srcTarg && srcTarg !== FieldWaiting) ? - srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => - (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; - draggedDocs.push(...draggedFromDocs); - if (draggedDocs.length) { - let moddrag = [] as Document[]; - for (const draggedDoc of draggedDocs) { - let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document); - if (doc) moddrag.push(doc); - } - let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); - DragManager.StartDocumentDrag([this._linkButton.current], dragData, e.x, e.y, { - handlers: { - dragComplete: action(() => { }), - }, - hideSource: false - }); - } + DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document); } e.stopPropagation(); } - onPointerMove = (e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -286,7 +373,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dX = 0, dY = 0, dW = 0, dH = 0; - switch (this._resizer) { + switch (this._resizing) { case "": break; case "documentDecorations-topLeftResizer": @@ -325,8 +412,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> break; } + MainOverlayTextBox.Instance.SetTextDoc(); SelectionManager.SelectedDocuments().forEach(element => { - const rect = element.screenRect(); + const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); + if (rect.width !== 0) { let doc = element.props.Document; let width = doc.GetNumber(KeyStore.Width, 0); @@ -354,8 +443,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }); } + @action onPointerUp = (e: PointerEvent): void => { e.stopPropagation(); + this._resizing = ""; + this.Interacting = false; + SelectionManager.ReselectAll(); if (e.button === 0) { e.preventDefault(); this._isPointerDown = false; @@ -364,17 +457,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } - getValue = (): string => { - if (this._title === "changed" && this._documents.length > 0) { - let field = this._documents[0].props.Document.Get(this._fieldKey); + @computed + get selectionTitle(): string { + if (SelectionManager.SelectedDocuments().length === 1) { + let field = SelectionManager.SelectedDocuments()[0].props.Document.Get(this._fieldKey); if (field instanceof TextField) { return (field).GetValue(); } else if (field instanceof NumberField) { return (field).GetValue().toString(); } + } else if (SelectionManager.SelectedDocuments().length > 1) { + return "-multiple-"; } - return this._title; + return "-unset-"; } changeFlyoutContent = (): void => { @@ -383,20 +479,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> // buttonOnPointerUp = (e: React.PointerEvent): void => { // e.stopPropagation(); // } + render() { var bounds = this.Bounds; - if (bounds.x === Number.MAX_VALUE) { - return (null); - } - // console.log(this._documents.length) - // let test = this._documents[0].props.Document.Title; - if (this.Hidden) { - return (null); - } - if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { - console.log("DocumentDecorations: Bounds Error"); + let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; + if (bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } + let minimizeIcon = ( + <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}> + {SelectionManager.SelectedDocuments().length == 1 ? IconBox.DocumentIcon(SelectionManager.SelectedDocuments()[0].props.Document.GetText(KeyStore.Layout, "...")) : "..."} + </div>); let linkButton = null; if (SelectionManager.SelectedDocuments().length > 0) { @@ -408,28 +501,45 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> anchorPoint={anchorPoints.RIGHT_TOP} content={<LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent} />}> - <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> + <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> </Flyout>); } + + let templates: Map<Template, boolean> = new Map(); + let doc = SelectionManager.SelectedDocuments()[0]; + Array.from(Object.values(Templates.TemplateList)).map(template => { + let docTemps = doc.templates; + let checked = false; + docTemps.forEach(temp => { + if (template.Name === temp.Name) { + checked = true; + } + }); + templates.set(template, checked); + }); + return (<div className="documentDecorations"> <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: this._dragging ? "none" : "all", + pointerEvents: this.Interacting ? "none" : "all", zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > </div> - <div id="documentDecorations-container" style={{ + <div className="documentDecorations-container" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, opacity: this._opacity }}> - <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>...</div> - <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} /> + {minimizeIcon} + + {this._edtingTitle ? + <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : + <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>} <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> @@ -449,7 +559,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" /> </div> </div> - + <TemplateMenu doc={doc} templates={templates} /> </div> </div > </div> diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index be3c5069a..ea401eaf9 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -2,5 +2,4 @@ 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/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index 42ae38c73..2c550051c 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables"; .inkingCanvas { opacity:0.99; diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 47ee8eb85..83836aa59 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -148,14 +148,15 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { } return paths; }, [] as JSX.Element[]); - return [<svg className={`inkingCanvas-paths-markers`} key="Markers" - style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} > - {paths.filter(path => path.props.tool === InkTool.Highlighter)} - </svg>, - <svg className={`inkingCanvas-paths-ink`} key="Pens" - style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}> - {paths.filter(path => path.props.tool !== InkTool.Highlighter)} - </svg>]; + return [ + <svg className={`inkingCanvas-paths-ink`} key="Pens" + style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}> + {paths.filter(path => path.props.tool !== InkTool.Highlighter)} + </svg>, + <svg className={`inkingCanvas-paths-markers`} key="Markers" + style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} > + {paths.filter(path => path.props.tool === InkTool.Highlighter)} + </svg>]; } render() { diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss index 0d8fd8784..ba4ec41af 100644 --- a/src/client/views/InkingControl.scss +++ b/src/client/views/InkingControl.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables"; .inking-control { position: absolute; left: 70px; diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 6139f4113..cbf920793 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables"; @import "nodeModuleOverrides"; html, @@ -13,6 +13,10 @@ body { left: 0; } +div { + user-select: none; +} + #dash-title { position: absolute; right: 46.5%; @@ -43,7 +47,9 @@ h1 { } .jsx-parser { - width: 100% + width:100%; + pointer-events: none; + border-radius: inherit; } p { @@ -54,7 +60,7 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; height: 5px; - width: 5px; + width: 10px; } ::-webkit-scrollbar-thumb { @@ -177,26 +183,6 @@ button:hover { left: 0; overflow: scroll; } - -.mainDiv-textInput { - background: pink; - width: 200px; - height: 200px; - position: absolute; - overflow: visible; - top: 0; - left: 0; - - .formattedTextBox-cont { - background: pink; - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - } -} - #mainContent-div { width: 100%; height: 100%; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 32798631d..129079271 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,7 +1,7 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, runInAction, trace } from 'mobx'; +import { action, computed, configure, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; @@ -9,26 +9,25 @@ import * as ReactDOM from 'react-dom'; import Measure from 'react-measure'; import * as request from 'request'; import { Document } from '../../fields/Document'; -import { Field, FieldWaiting, Opt } from '../../fields/Field'; +import { Field, FieldWaiting, Opt, FIELD_WAITING } from '../../fields/Field'; import { KeyStore } from '../../fields/KeyStore'; import { ListField } from '../../fields/ListField'; import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { MessageStore } from '../../server/Message'; -import { Utils, returnTrue, emptyFunction } from '../../Utils'; -import * as rp from 'request-promise'; import { RouteStore } from '../../server/RouteStore'; import { ServerUtils } from '../../server/ServerUtil'; +import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; import { Documents } from '../documents/Documents'; import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel'; import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; -import { Gateway, Settings } from '../northstar/manager/Gateway'; +import { Gateway, NorthstarSettings } from '../northstar/manager/Gateway'; import { AggregateFunction, Catalog } from '../northstar/model/idea/idea'; import '../northstar/model/ModelExtensions'; import { HistogramOperation } from '../northstar/operations/HistogramOperation'; import '../northstar/utils/Extensions'; import { Server } from '../Server'; -import { setupDrag } from '../util/DragManager'; +import { SetupDrag, DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; @@ -36,34 +35,26 @@ import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { InkingControl } from './InkingControl'; import "./Main.scss"; +import { MainOverlayTextBox } from './MainOverlayTextBox'; import { DocumentView } from './nodes/DocumentView'; -import { FormattedTextBox } from './nodes/FormattedTextBox'; +import { PreviewCursor } from './PreviewCursor'; +import { SelectionManager } from '../util/SelectionManager'; + @observer export class Main extends React.Component { - // dummy initializations keep the compiler happy - @observable private mainfreeform?: Document; + public static Instance: Main; + @observable private _workspacesShown: boolean = false; @observable public pwidth: number = 0; @observable public pheight: number = 0; - private _northstarSchemas: Document[] = []; - @computed private get mainContainer(): Document | undefined { - let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document); - return doc === FieldWaiting ? undefined : doc; + @computed private get mainContainer(): Document | undefined | FIELD_WAITING { + return CurrentUserUtils.UserDocument.GetT(KeyStore.ActiveWorkspace, Document); } - - private set mainContainer(doc: Document | undefined) { - if (doc) { - this.userDocument.Set(KeyStore.ActiveWorkspace, doc); - } - } - - private get userDocument(): Document { - return CurrentUserUtils.UserDocument; + private set mainContainer(doc: Document | undefined | FIELD_WAITING) { + doc && CurrentUserUtils.UserDocument.Set(KeyStore.ActiveWorkspace, doc); } - public static Instance: Main; - constructor(props: Readonly<{}>) { super(props); Main.Instance = this; @@ -94,9 +85,17 @@ export class Main extends React.Component { this.initEventListeners(); this.initAuthenticationRouters(); - this.initializeNorthstar(); + try { + this.initializeNorthstar(); + } catch (e) { + + } } + componentDidMount() { window.onpopstate = this.onHistory; } + + componentWillUnmount() { window.onpopstate = null; } + onHistory = () => { if (window.location.pathname !== RouteStore.home) { let pathname = window.location.pathname.split("/"); @@ -109,18 +108,16 @@ export class Main extends React.Component { } } - componentDidMount() { - window.onpopstate = this.onHistory; - } - - 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 + window.addEventListener("keydown", (e) => { + if (e.key == "Escape") { + DragManager.AbortDrag(); + SelectionManager.DeselectAll() + } + }, false); // drag event handler // click interactions for the context menu document.addEventListener("pointerdown", action(function (e: PointerEvent) { if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { @@ -132,7 +129,7 @@ export class Main extends React.Component { initAuthenticationRouters = () => { // Load the user's active workspace, or create a new one if initial session after signup if (!CurrentUserUtils.MainDocId) { - this.userDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => { + CurrentUserUtils.UserDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => { if (doc) { CurrentUserUtils.MainDocId = doc.Id; this.openWorkspace(doc); @@ -141,19 +138,15 @@ export class Main extends React.Component { } }); } else { - Server.GetField(CurrentUserUtils.MainDocId).then(field => { - if (field instanceof Document) { - this.openWorkspace(field); - } else { - this.createNewWorkspace(CurrentUserUtils.MainDocId); - } - }); + Server.GetField(CurrentUserUtils.MainDocId).then(field => + field instanceof Document ? this.openWorkspace(field) : + this.createNewWorkspace(CurrentUserUtils.MainDocId)); } } @action createNewWorkspace = (id?: string): void => { - this.userDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => { + CurrentUserUtils.UserDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => { if (list) { let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; @@ -174,93 +167,60 @@ export class Main extends React.Component { openWorkspace = (doc: Document, fromHistory = false): void => { this.mainContainer = doc; fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id); - this.userDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col => { + CurrentUserUtils.UserDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col => // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) - setTimeout(() => { - if (col) { - col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => { - if (f && f.Data.length > 0) { - CollectionDockingView.Instance.AddRightSplit(col); - } - }); - } - }, 100); - }); - } - - @observable - workspacesShown: boolean = false; - - areWorkspacesShown = () => this.workspacesShown; - @action - toggleWorkspaces = () => { - this.workspacesShown = !this.workspacesShown; - } - - pwidthFunc = () => this.pwidth; - pheightFunc = () => this.pheight; - focusDocument = (doc: Document) => { }; - noScaling = () => 1; - - @observable _textDoc?: Document = undefined; - _textRect: any; - @action - SetTextDoc(textDoc?: Document, div?: HTMLDivElement) { - this._textDoc = undefined; - this._textDoc = textDoc; - if (div) { - this._textRect = div.getBoundingClientRect(); - } - } - - @computed - get activeTextBox() { - if (this._textDoc) { - let x: number = this._textRect.x; - let y: number = this._textRect.y; - let w: number = this._textRect.width; - let h: number = this._textRect.height; - return <div className="mainDiv-textInput" style={{ transform: `translate(${x}px, ${y}px)`, width: `${w}px`, height: `${h}px` }} > - <FormattedTextBox fieldKey={KeyStore.Archives} Document={this._textDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true} onActiveChanged={emptyFunction} active={returnTrue} ScreenToLocalTransform={Transform.Identity} focus={(doc) => { }} /> - </ div>; - } - else return (null); + setTimeout(() => + col && col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => + f && f.Data.length > 0 && CollectionDockingView.Instance.AddRightSplit(col)) + , 100) + ); } @computed get mainContent() { - return !this.mainContainer ? (null) : - <DocumentView Document={this.mainContainer} - addDocument={undefined} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={this.noScaling} - PanelWidth={this.pwidthFunc} - PanelHeight={this.pheightFunc} - isTopMost={true} - selectOnLoad={false} - focus={this.focusDocument} - parentActive={returnTrue} - onActiveChanged={emptyFunction} - ContainingCollectionView={undefined} />; + let pwidthFunc = () => this.pwidth; + let pheightFunc = () => this.pheight; + let noScaling = () => 1; + let mainCont = this.mainContainer; + return <Measure onResize={action((r: any) => { this.pwidth = r.entry.width; this.pheight = r.entry.height; })}> + {({ measureRef }) => + <div ref={measureRef} id="mainContent-div"> + {!mainCont ? (null) : + <DocumentView Document={mainCont} + toggleMinimized={emptyFunction} + addDocument={undefined} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={noScaling} + PanelWidth={pwidthFunc} + PanelHeight={pheightFunc} + isTopMost={true} + selectOnLoad={false} + focus={emptyDocFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + ContainingCollectionView={undefined} />} + </div> + } + </Measure>; } /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ @computed get nodesMenu() { let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"; + let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf"; let weburl = "https://cs.brown.edu/courses/cs166/"; let audiourl = "http://techslides.com/demos/samples/sample.mp3"; let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; - let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })); + let addTextNode = action(() => Documents.TextDocument({ borderRounding: -1, width: 200, height: 50, title: "a text note" })); let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" })); let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); - 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 addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, title: "video node" })); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 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" })); @@ -284,7 +244,7 @@ export class Main extends React.Component { <ul id="add-options-list"> {btns.map(btn => <li key={btn[1]} ><div ref={btn[0]}> - <button className="round-button add-button" title={btn[2]} onPointerDown={setupDrag(btn[0], btn[3])}> + <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}> <FontAwesomeIcon icon={btn[1]} size="sm" /> </button> </div></li>)} @@ -298,6 +258,7 @@ export class Main extends React.Component { get miscButtons() { let workspacesRef = React.createRef<HTMLDivElement>(); let logoutRef = React.createRef<HTMLDivElement>(); + let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown); let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})); return [ @@ -308,54 +269,50 @@ export class Main extends React.Component { <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> </div >, <div className="main-buttonDiv" key="workspaces" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}> - <button onClick={this.toggleWorkspaces}>Workspaces</button></div>, + <button onClick={toggleWorkspaces}>Workspaces</button></div>, <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> - <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div> + <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div> ]; } + @computed + get workspaceMenu() { + let areWorkspacesShown = () => this._workspacesShown; + let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown); + let workspaces = CurrentUserUtils.UserDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField); + return (!workspaces || workspaces === FieldWaiting || this.mainContainer === FieldWaiting) ? (null) : + <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} + new={this.createNewWorkspace} allWorkspaces={workspaces.Data} + isShown={areWorkspacesShown} toggle={toggleWorkspaces} />; + } + render() { - let workspaceMenu: any = null; - let workspaces = this.userDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField); - if (workspaces && workspaces !== FieldWaiting) { - workspaceMenu = <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={workspaces.Data} - isShown={this.areWorkspacesShown} toggle={this.toggleWorkspaces} />; - } return ( - <> - <div id="main-div"> - <DocumentDecorations /> - <Measure onResize={(r: any) => runInAction(() => { - this.pwidth = r.entry.width; - this.pheight = r.entry.height; - })}> - {({ measureRef }) => - <div ref={measureRef} id="mainContent-div"> - {this.mainContent} - </div> - } - </Measure> - <ContextMenu /> - {this.nodesMenu} - {this.miscButtons} - {workspaceMenu} - <InkingControl /> - </div> - {this.activeTextBox} - </> + <div id="main-div"> + <DocumentDecorations /> + {this.mainContent} + <PreviewCursor /> + <ContextMenu /> + {this.nodesMenu} + {this.miscButtons} + {this.workspaceMenu} + <InkingControl /> + <MainOverlayTextBox /> + </div> ); } // --------------- Northstar hooks ------------- / + private _northstarSchemas: Document[] = []; - @action AddToNorthstarCatalog(ctlog: Catalog) { - CurrentUserUtils.NorthstarDBCatalog = CurrentUserUtils.NorthstarDBCatalog ? CurrentUserUtils.NorthstarDBCatalog : ctlog; + @action SetNorthstarCatalog(ctlog: Catalog) { + CurrentUserUtils.NorthstarDBCatalog = ctlog; if (ctlog && ctlog.schemas) { - this._northstarSchemas.push(...ctlog.schemas.map(schema => { - let schemaDoc = Documents.TreeDocument([], { width: 50, height: 100, title: schema.displayName! }); - let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); - CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { - Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => { + ctlog.schemas.map(schema => { + let schemaDocuments: Document[] = []; + let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema); + Promise.all(attributesToBecomeDocs.reduce((promises, attr) => { + promises.push(Server.GetField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { if (field instanceof Document) { schemaDocuments.push(field); } else { @@ -366,28 +323,17 @@ export class Main extends React.Component { new AttributeTransformationModel(atmod, AggregateFunction.Count)); schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); } - })); - }); - return schemaDoc; - })); + }))); + return promises; + }, [] as Promise<void>[])).finally(() => + this._northstarSchemas.push(Documents.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }))); + }); } } async initializeNorthstar(): Promise<void> { - let envPath = "/assets/env.json"; - const response = await fetch(envPath, { - redirect: "follow", - method: "GET", - credentials: "include" - }); - const env = await response.json(); - Settings.Instance.Update(env); - let cat = Gateway.Instance.ClearCatalog(); - cat.then(async () => { - this.AddToNorthstarCatalog(await Gateway.Instance.GetCatalog()); - if (!CurrentUserUtils.GetNorthstarSchema("Book1")) - this.AddToNorthstarCatalog(await Gateway.Instance.GetSchema("http://www.cs.brown.edu/~bcz/Book1.csv")); - }); - + const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); + NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); + Gateway.Instance.ClearCatalog().then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog())); } } diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss new file mode 100644 index 000000000..f6a746e63 --- /dev/null +++ b/src/client/views/MainOverlayTextBox.scss @@ -0,0 +1,20 @@ +@import "globalCssVariables"; +.mainOverlayTextBox-textInput { + background-color: rgba(248, 6, 6, 0.001); + width: 200px; + height: 200px; + position:absolute; + overflow: visible; + top: 0; + left: 0; + pointer-events: none; + z-index: $mainTextInput-zindex; + .formattedTextBox-cont { + background-color: rgba(248, 6, 6, 0.001); + width: 100%; + height: 100%; + position:absolute; + top: 0; + left: 0; + } +}
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx new file mode 100644 index 000000000..13e661b46 --- /dev/null +++ b/src/client/views/MainOverlayTextBox.tsx @@ -0,0 +1,103 @@ +import { action, observable, trace } from 'mobx'; +import { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import { Document } from '../../fields/Document'; +import { Key } from '../../fields/Key'; +import { KeyStore } from '../../fields/KeyStore'; +import { emptyDocFunction, emptyFunction, returnTrue, returnZero } from '../../Utils'; +import '../northstar/model/ModelExtensions'; +import '../northstar/utils/Extensions'; +import { DragManager } from '../util/DragManager'; +import { Transform } from '../util/Transform'; +import "./MainOverlayTextBox.scss"; +import { FormattedTextBox } from './nodes/FormattedTextBox'; + +interface MainOverlayTextBoxProps { +} + +@observer +export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> { + public static Instance: MainOverlayTextBox; + @observable public TextDoc?: Document = undefined; + public TextScroll: number = 0; + @observable _textXf: () => Transform = () => Transform.Identity(); + private _textFieldKey: Key = KeyStore.Data; + private _textColor: string | null = null; + private _textTargetDiv: HTMLDivElement | undefined; + private _textProxyDiv: React.RefObject<HTMLDivElement>; + + constructor(props: MainOverlayTextBoxProps) { + super(props); + this._textProxyDiv = React.createRef(); + MainOverlayTextBox.Instance = this; + } + + @action + SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: () => Transform) { + if (this._textTargetDiv) { + this._textTargetDiv.style.color = this._textColor; + } + + this.TextDoc = textDoc; + this._textFieldKey = textFieldKey!; + this._textXf = tx ? tx : () => Transform.Identity(); + this._textTargetDiv = div; + if (div) { + this._textColor = div.style.color; + div.style.color = "transparent"; + this.TextScroll = div.scrollTop; + } + } + + @action + textScroll = (e: React.UIEvent) => { + if (this._textProxyDiv.current && this._textTargetDiv) { + this.TextScroll = (e as any)._targetInst.stateNode.scrollTop;// this._textProxyDiv.current.children[0].scrollTop; + this._textTargetDiv.scrollTop = this.TextScroll; + } + } + + textBoxDown = (e: React.PointerEvent) => { + if (e.button !== 0 || e.metaKey || e.altKey) { + document.addEventListener("pointermove", this.textBoxMove); + document.addEventListener('pointerup', this.textBoxUp); + } + } + textBoxMove = (e: PointerEvent) => { + if (e.movementX > 1 || e.movementY > 1) { + document.removeEventListener("pointermove", this.textBoxMove); + document.removeEventListener('pointerup', this.textBoxUp); + let dragData = new DragManager.DocumentDragData([this.TextDoc!]); + const [left, top] = this._textXf().inverse().transformPoint(0, 0); + dragData.xOffset = e.clientX - left; + dragData.yOffset = e.clientY - top; + DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + } + textBoxUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.textBoxMove); + document.removeEventListener('pointerup', this.textBoxUp); + } + + render() { + if (this.TextDoc && this._textTargetDiv) { + let textRect = this._textTargetDiv.getBoundingClientRect(); + let s = this._textXf().Scale; + return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.x}px, ${textRect.y}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} > + <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} + style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}> + <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} + selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyDocFunction} /> + </div> + </ div>; + } + else return (null); + } +}
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss new file mode 100644 index 000000000..20f9b9a49 --- /dev/null +++ b/src/client/views/PreviewCursor.scss @@ -0,0 +1,9 @@ + +.previewCursor { + color: black; + position: absolute; + transform-origin: left top; + top: 0; + left:0; + pointer-events: none; +}
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx new file mode 100644 index 000000000..4359ba093 --- /dev/null +++ b/src/client/views/PreviewCursor.tsx @@ -0,0 +1,55 @@ +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import "./PreviewCursor.scss"; + +@observer +export class PreviewCursor extends React.Component<{}> { + private _prompt = React.createRef<HTMLDivElement>(); + static _onKeyPress?: (e: KeyboardEvent) => void; + @observable static _clickPoint = [0, 0]; + @observable public static Visible = false; + //when focus is lost, this will remove the preview cursor + @action onBlur = (): void => { + PreviewCursor.Visible = false; + } + + constructor(props: any) { + super(props); + document.addEventListener("keydown", this.onKeyPress) + } + + @action + onKeyPress = (e: KeyboardEvent) => { + // Mixing events between React and Native is finicky. In FormattedTextBox, we set the + // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore + // the keyPress here. + //if not these keys, make a textbox if preview cursor is active! + if (e.key.startsWith("F") && !e.key.endsWith("F")) { + } else if (e.key != "Escape" && e.key != "Alt" && e.key != "Shift" && e.key != "Meta" && e.key != "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { + if ((!e.ctrlKey && !e.metaKey) || e.key === "v") { + PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); + PreviewCursor.Visible = false; + } + } + } + @action + public static Show(x: number, y: number, onKeyPress: (e: KeyboardEvent) => void) { + this._clickPoint = [x, y]; + this._onKeyPress = onKeyPress; + setTimeout(action(() => this.Visible = true), (1)); + } + render() { + if (!PreviewCursor._clickPoint) { + return (null); + } + if (PreviewCursor.Visible && this._prompt.current) { + this._prompt.current.focus(); + } + return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._prompt} + style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}> + I + </div >; + } +}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx new file mode 100644 index 000000000..8eb2fc6c6 --- /dev/null +++ b/src/client/views/TemplateMenu.tsx @@ -0,0 +1,75 @@ +import { observable, computed, action } from "mobx"; +import React = require("react"); +import { observer } from "mobx-react"; +import './DocumentDecorations.scss'; +import { Template } from "./Templates"; +import { DocumentView } from "./nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> { + render() { + if (this.props.template) { + return ( + <li> + <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} /> + {this.props.template.Name} + </li> + ); + } else { + return (null); + } + } +} + +export interface TemplateMenuProps { + doc: DocumentView; + templates: Map<Template, boolean>; +} + +@observer +export class TemplateMenu extends React.Component<TemplateMenuProps> { + + @observable private _hidden: boolean = true; + @observable private _templates: Map<Template, boolean> = this.props.templates; + + + @action + toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { + if (event.target.checked) { + this.props.doc.addTemplate(template); + this._templates.set(template, true); + } else { + this.props.doc.removeTemplate(template); + this._templates.set(template, false); + } + } + + @action + componentWillReceiveProps(nextProps: TemplateMenuProps) { + this._templates = nextProps.templates; + } + + @action + toggleTemplateActivity = (): void => { + this._hidden = !this._hidden; + } + + render() { + let templateMenu: Array<JSX.Element> = []; + this._templates.forEach((checked, template) => { + templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />); + }); + + return ( + <div className="templating-menu" > + <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> + <ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}> + {templateMenu} + </ul> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx new file mode 100644 index 000000000..fef69392d --- /dev/null +++ b/src/client/views/Templates.tsx @@ -0,0 +1,66 @@ +import React = require("react"); + +export enum TemplatePosition { + InnerTop, + InnerBottom, + InnerRight, + InnerLeft, + OutterTop, + OutterBottom, + OutterRight, + OutterLeft, +} + +export class Template { + constructor(name: string, position: TemplatePosition, layout: string) { + this._name = name; + this._position = position; + this._layout = layout; + } + + private _name: string; + private _position: TemplatePosition; + private _layout: string; + + get Name(): string { + return this._name; + } + + get Position(): TemplatePosition { + return this._position; + } + + get Layout(): string { + return this._layout; + } +} + +export namespace Templates { + // export const BasicLayout = new Template("Basic layout", "{layout}"); + + export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom, + `<div><div style="margin:auto; height:calc(100%); width:100%;">{layout}</div><div style="height:(100% + 50px); width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={CaptionKey} /></div></div>` + ); + + export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom, + `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={CaptionKey}/></div></div>` + ); + + export const SideCaption = new Template("Side caption", TemplatePosition.OutterRight, + `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={CaptionKey}/></div> </div>` + ); + + export const Title = new Template("Title", TemplatePosition.InnerTop, + `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{Title}</div></div>` + ); + + export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption]; + + export function sortTemplates(a: Template, b: Template) { + if (a.Position < b.Position) { return -1; } + if (a.Position > b.Position) { return 1; } + return 0; + } + +} + diff --git a/src/client/views/_global_variables.scss b/src/client/views/_global_variables.scss deleted file mode 100644 index 44a819b79..000000000 --- a/src/client/views/_global_variables.scss +++ /dev/null @@ -1,17 +0,0 @@ -@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/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 4380c8194..aa8fce923 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,13 +1,14 @@ -import { action } from 'mobx'; +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Document } from '../../../fields/Document'; -import { Field, FieldValue, FieldWaiting } from '../../../fields/Field'; +import { FieldValue, FieldWaiting } from '../../../fields/Field'; import { KeyStore } from '../../../fields/KeyStore'; import { ListField } from '../../../fields/ListField'; import { NumberField } from '../../../fields/NumberField'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; +import { SelectionManager } from '../../util/SelectionManager'; export enum CollectionViewType { Invalid, @@ -22,7 +23,7 @@ export interface CollectionRenderProps { removeDocument: (document: Document) => boolean; moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; active: () => boolean; - onActiveChanged: (isActive: boolean) => void; + whenActiveChanged: (isActive: boolean) => void; } export interface CollectionViewProps extends FieldViewProps { @@ -32,19 +33,18 @@ export interface CollectionViewProps extends FieldViewProps { contentRef?: React.Ref<HTMLDivElement>; } -export const COLLECTION_BORDER_WIDTH = 1; @observer export class CollectionBaseView extends React.Component<CollectionViewProps> { - get collectionViewType(): CollectionViewType { + get collectionViewType(): CollectionViewType | undefined { let Document = this.props.Document; let viewField = Document.GetT(KeyStore.ViewType, NumberField); if (viewField === FieldWaiting) { - return CollectionViewType.Invalid; + return undefined; } else if (viewField) { return viewField.Data; } else { - return CollectionViewType.Freeform; + return CollectionViewType.Invalid; } } @@ -56,19 +56,22 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { //TODO should this be observable? private _isChildActive = false; - onActiveChanged = (isActive: boolean) => { + whenActiveChanged = (isActive: boolean) => { this._isChildActive = isActive; - this.props.onActiveChanged(isActive); + this.props.whenActiveChanged(isActive); } createsCycle(documentToAdd: Document, containerDocument: Document): boolean { - let data = documentToAdd.GetList<Document>(KeyStore.Data, []); - for (const doc of data) { + if (!(documentToAdd instanceof Document)) { + return false; + } + let data = documentToAdd.GetList(KeyStore.Data, [] as Document[]); + for (const doc of data.filter(d => d instanceof Document)) { if (this.createsCycle(doc, containerDocument)) { return true; } } - let annots = documentToAdd.GetList<Document>(KeyStore.Annotations, []); + let annots = documentToAdd.GetList(KeyStore.Annotations, [] as Document[]); for (const annot of annots) { if (this.createsCycle(annot, containerDocument)) { return true; @@ -81,53 +84,60 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } return false; } + @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? @action.bound addDocument(doc: Document, allowDuplicates: boolean = false): boolean { - let props = this.props; - var curPage = props.Document.GetNumber(KeyStore.CurPage, -1); + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage)); if (curPage >= 0) { - doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document); + doc.SetOnPrototype(KeyStore.AnnotationOn, this.props.Document); } - if (props.Document.Get(props.fieldKey) instanceof Field) { - //TODO This won't create the field if it doesn't already exist - const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()); - if (!this.createsCycle(doc, props.Document)) { - if (!value.some(v => v.Id === doc.Id) || allowDuplicates) { - value.push(doc); + if (!this.createsCycle(doc, this.props.Document)) { + let value = this.props.Document.Get(this.props.fieldKey) as ListField<Document>; + if (value) { + if (!value.Data.some(v => v.Id === doc.Id) || allowDuplicates) { + value.Data.push(doc); } + } else { + this.props.Document.Set(this.props.fieldKey, new ListField([doc])); } - else { - return false; - } - } else { - let proto = props.Document.GetPrototype(); - if (!proto || proto === FieldWaiting || !this.createsCycle(proto, doc)) { - const field = new ListField([doc]); - // const script = CompileScript(` - // if(added) { - // console.log("added " + field.Title); - // } else { - // console.log("removed " + field.Title); - // } - // `, { - // addReturn: false, - // params: { - // field: Document.name, - // added: "boolean" - // } - // }); - // if (script.compiled) { - // field.addScript(new ScriptField(script)); - // } - props.Document.SetOnPrototype(props.fieldKey, field); - } - else { - return false; + // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? + if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) { + let zoom = this.props.Document.GetNumber(KeyStore.Scale, 1); + doc.SetNumber(KeyStore.ZoomBasis, zoom); } } return true; + // bcz: What is this code trying to do? + // else { + // let proto = props.Document.GetPrototype(); + // if (!proto || proto === FieldWaiting || !this.createsCycle(proto, doc)) { + // const field = new ListField([doc]); + // // const script = CompileScript(` + // // if(added) { + // // console.log("added " + field.Title + " " + doc.Title); + // // } else { + // // console.log("removed " + field.Title + " " + doc.Title); + // // } + // // `, { + // // addReturn: false, + // // params: { + // // field: Document.name, + // // added: "boolean" + // // }, + // // capturedVariables: { + // // doc: this.props.Document + // // } + // // }); + // // if (script.compiled) { + // // field.addScript(new ScriptField(script)); + // // } + // props.Document.SetOnPrototype(props.fieldKey, field); + // return true; + // } + // } + return false; } @action.bound @@ -164,6 +174,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { return true; } if (this.removeDocument(doc)) { + SelectionManager.DeselectAll(); return addDocument(doc); } return false; @@ -175,11 +186,14 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { removeDocument: this.removeDocument, moveDocument: this.moveDocument, active: this.active, - onActiveChanged: this.onActiveChanged, + whenActiveChanged: this.whenActiveChanged, }; + const viewtype = this.collectionViewType; return ( - <div className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> - {this.props.children(this.collectionViewType, props)} + <div className={this.props.className || "collectionView-cont"} + style={{ borderRadius: "inherit", pointerEvents: "all" }} + onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> + {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> ); } diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 583d50c5b..0e7e0afa7 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,8 +1,29 @@ +@import "../../views/globalCssVariables.scss"; + .collectiondockingview-content { height: 100%; } +.lm_active .messageCounter{ + color:white; + background: #999999; +} +.messageCounter { + width:18px; + height:20px; + text-align: center; + border-radius: 20px; + margin-left: 5px; + transform: translate(0px, -8px); + display: inline-block; + background: transparent; + border: 1px #999999 solid; +} .collectiondockingview-container { + width: 100%; + height: 100%; + border-style: solid; + border-width: $COLLECTION_BORDER_WIDTH; position: absolute; top: 0; left: 0; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ea6d3a247..2a8191c3a 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -7,18 +7,19 @@ import * as ReactDOM from 'react-dom'; import { Document } from "../../../fields/Document"; import { KeyStore } from "../../../fields/KeyStore"; import Measure from "react-measure"; -import { FieldId, Opt, Field } from "../../../fields/Field"; -import { Utils, returnTrue, emptyFunction } from "../../../Utils"; +import { FieldId, Opt, Field, FieldWaiting } from "../../../fields/Field"; +import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne, returnZero } from "../../../Utils"; import { Server } from "../../Server"; -import { undoBatch } from "../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionBaseView"; import React = require("react"); import { SubCollectionViewProps } from "./CollectionSubView"; import { ServerUtils } from "../../../server/ServerUtil"; -import { DragManager } from "../../util/DragManager"; +import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; import { TextField } from "../../../fields/TextField"; +import { ListField } from "../../../fields/ListField"; +import { Transform } from '../../util/Transform' @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -47,10 +48,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp (window as any).React = React; (window as any).ReactDOM = ReactDOM; } + hack: boolean = false; + undohack: any = null; public StartOtherDrag(dragDocs: Document[], e: any) { + this.hack = true; + this.undohack = UndoManager.StartBatch("goldenDrag"); dragDocs.map(dragDoc => this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. - onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })); + onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action @@ -172,7 +177,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } catch (e) { } - this._goldenLayout.destroy(); + if (this._goldenLayout) this._goldenLayout.destroy(); this._goldenLayout = null; window.removeEventListener('resize', this.onResize); } @@ -194,23 +199,35 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { var className = (e.target as any).className; - if ((className === "lm_title" || className === "lm_tab lm_active") && (e.ctrlKey || e.altKey)) { + if (className === "messageCounter") { e.stopPropagation(); e.preventDefault(); + let x = e.clientX; + let y = e.clientY; let docid = (e.target as any).DashDocId; let tab = (e.target as any).parentElement as HTMLElement; - Server.GetField(docid, action((f: Opt<Field>) => { - if (f instanceof Document) { - DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), e.pageX, e.pageY, - { - handlers: { - dragComplete: action(() => { }), - }, - hideSource: false - }); - } - })); - } + Server.GetField(docid, action(async (sourceDoc: Opt<Field>) => + (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc))); + } else + if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { + e.stopPropagation(); + e.preventDefault(); + let x = e.clientX; + let y = e.clientY; + let docid = (e.target as any).DashDocId; + let tab = (e.target as any).parentElement as HTMLElement; + Server.GetField(docid, action((f: Opt<Field>) => { + if (f instanceof Document) { + DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, + { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + })); + } if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } @@ -223,30 +240,55 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stateChanged = () => { var json = JSON.stringify(this._goldenLayout.toConfig()); this.props.Document.SetText(KeyStore.Data, json); + if (this.undohack && !this.hack) { + this.undohack.end(); + this.undohack = undefined; + } + this.hack = false; } itemDropped = () => { this.stateChanged(); } + htmlToElement(html: string) { + var template = document.createElement('template'); + html = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = html; + return template.content.firstChild; + } + tabCreated = (tab: any) => { if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - if (tab.titleElement[0].textContent.indexOf("-waiting") !== -1) { - Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => { - if (f !== undefined && f instanceof Document) { - f.GetTAsync(KeyStore.Title, TextField, (tfield) => { - if (tfield !== undefined) { - tab.titleElement[0].textContent = f.Title; - } - }); - } - })); - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => { + if (f !== undefined && f instanceof Document) { + f.GetTAsync(KeyStore.Title, TextField, (tfield) => { + if (tfield !== undefined) { + tab.titleElement[0].textContent = f.Title; + } + }); + f.GetTAsync(KeyStore.LinkedFromDocs, ListField).then(lf => + f.GetTAsync(KeyStore.LinkedToDocs, ListField).then(lt => { + let count = (lf ? lf.Data.length : 0) + (lt ? lt.Data.length : 0); + let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`); + tab.element.append(counter); + counter.DashDocId = tab.contentItem.config.props.documentId; + tab.reactionDisposer = reaction(() => [f.GetT(KeyStore.LinkedFromDocs, ListField), f.GetT(KeyStore.LinkedToDocs, ListField)], + (lists) => { + let count = (lists.length > 0 && lists[0] && lists[0]!.Data ? lists[0]!.Data.length : 0) + + (lists.length > 1 && lists[1] && lists[1]!.Data ? lists[1]!.Data.length : 0); + counter.innerHTML = count; + }); + })); + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + } + })); } tab.closeElement.off('click') //unbind the current click handler .click(function () { + if (tab.reactionDisposer) { + tab.reactionDisposer(); + } tab.contentItem.remove(); }); } @@ -271,13 +313,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp render() { return ( <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} - style={{ - width: "100%", - height: "100%", - borderStyle: "solid", - borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }} /> + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> ); } } @@ -289,7 +325,7 @@ interface DockedFrameProps { @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - private _mainCont = React.createRef<HTMLDivElement>(); + _mainCont = React.createRef<HTMLDivElement>(); @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Document>; @@ -299,38 +335,56 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); } - private _nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); - private _nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); - private _contentScaling = () => this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); + nativeWidth = () => { + let pw = this._document!.GetNumber(KeyStore.NativeWidth, 0); + return pw ? pw : this._panelWidth; + } + nativeHeight = () => { + let pw = this._document!.GetNumber(KeyStore.NativeHeight, 0); + return pw ? pw : this._panelHeight; + } + contentScaling = () => { + let wscale = this._panelWidth / (this.nativeWidth() ? this.nativeWidth() : this._panelWidth); + if (wscale * this.nativeHeight() > this._panelHeight) + return this._panelHeight / (this.nativeHeight() ? this.nativeHeight() : this._panelHeight); + return wscale; + } ScreenToLocalTransform = () => { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); - return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()); + if (this._mainCont.current && this._mainCont.current.children) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!.children[0].firstChild as HTMLElement); + scale = Utils.GetScreenTransform(this._mainCont.current!).scale; + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling()); + } + return Transform.Identity(); } + get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - render() { - if (!this._document) { - return (null); - } - var content = - <div className="collectionDockingView-content" ref={this._mainCont}> - <DocumentView key={this._document.Id} Document={this._document} + get content() { + return ( + <div className="collectionDockingView-content" ref={this._mainCont} + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + <DocumentView key={this._document!.Id} Document={this._document!} + toggleMinimized={emptyFunction} addDocument={undefined} removeDocument={undefined} - ContentScaling={this._contentScaling} - PanelWidth={this._nativeWidth} - PanelHeight={this._nativeHeight} + ContentScaling={this.contentScaling} + PanelWidth={this.nativeWidth} + PanelHeight={this.nativeHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} selectOnLoad={false} parentActive={returnTrue} - onActiveChanged={emptyFunction} - focus={(doc: Document) => { }} + whenActiveChanged={emptyFunction} + focus={emptyDocFunction} ContainingCollectionView={undefined} /> - </div>; + </div>); + } - return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> - {({ measureRef }) => <div ref={measureRef}> {content} </div>} - </Measure>; + render() { + return !this._document ? (null) : + <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> + {({ measureRef }) => <div ref={measureRef}> {this.content} </div>} + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index 0eca3f1cd..f6fb79582 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,20 +1,39 @@ .collectionPdfView-buttonTray { - top : 25px; + top : 15px; left : 20px; position: relative; transform-origin: left top; position: absolute; } +.collectionPdfView-thumb { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: darkgray; +} +.collectionPdfView-slider { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: lightgray; +} .collectionPdfView-cont{ width: 100%; height: 100%; position: absolute; top: 0; left:0; - +} +.collectionPdfView-cont-dragging { + span { + user-select: none; + } } .collectionPdfView-backward { color : white; + font-size: 24px; top :0px; left : 0px; position: absolute; @@ -22,8 +41,9 @@ } .collectionPdfView-forward { color : white; + font-size: 24px; top :0px; - left : 35px; + left : 45px; position: absolute; background-color: rgba(50, 50, 50, 0.2); }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 97bac745c..497c3ee3c 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { KeyStore } from "../../../fields/KeyStore"; import { ContextMenu } from "../ContextMenu"; @@ -7,6 +7,7 @@ import React = require("react"); import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; +import { emptyFunction } from "../../../Utils"; @observer @@ -15,25 +16,51 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { public static LayoutString(fieldKey: string = "DataKey") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } + @observable _inThumb = false; private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); } + private set curPage(value: number) { this.props.Document.SetNumber(KeyStore.CurPage, value); } 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; + @action + onThumbDown = (e: React.PointerEvent) => { + document.addEventListener("pointermove", this.onThumbMove, false); + document.addEventListener("pointerup", this.onThumbUp, false); + e.stopPropagation(); + this._inThumb = true; + } + @action + onThumbMove = (e: PointerEvent) => { + let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; + this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); + e.stopPropagation(); + } + @action + onThumbUp = (e: PointerEvent) => { + this._inThumb = false; + document.removeEventListener("pointermove", this.onThumbMove); + document.removeEventListener("pointerup", this.onThumbUp); + } + nativeWidth = () => this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + nativeHeight = () => this.props.Document.GetNumber(KeyStore.NativeHeight, 0); private get uIButtons() { - let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="collectionPdfView-buttonTray" key="tray" style={{ transform: `scale(${scaling}, ${scaling})` }}> + <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}> <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> + <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > + <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> + </div> </div> ); } onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document.Id !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { } }); + ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction }); } } @@ -41,7 +68,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { let props = { ...this.props, ...renderProps }; return ( <> - <CollectionFreeFormView {...props} /> + <CollectionFreeFormView {...props} CollectionView={this} /> {this.props.isSelected() ? this.uIButtons : (null)} </> ); @@ -49,7 +76,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { render() { return ( - <CollectionBaseView {...this.props} className="collectionPdfView-cont" onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> {this.subView} </CollectionBaseView> ); diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index c3a2e88ac..cfdb3ab22 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,76 +1,41 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -//options menu styling -#schemaOptionsMenuBtn { - position: absolute; - height: 20px; - width: 20px; - border-radius: 50%; - z-index: 21; - right: 4px; - top: 4px; - pointer-events: auto; - background-color:black; - display:inline-block; - padding: 0px; - font-size: 100%; -} -#schema-options-header { - text-align: center; - padding: 0px; - margin: 0px; -} -.schema-options-subHeader { - color: $intermediate-color; - margin-bottom: 5px; -} -#schemaOptionsMenuBtn:hover { - transform: scale(1.15); -} - -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; -} - #options-flyout-div { - text-align: left; - padding:0px; - z-index: 100; - font-family: $sans-serif; - padding-left: 5px; - } - - #schema-col-checklist { - overflow: scroll; - text-align: left; - //background-color: $light-color-secondary; - line-height: 25px; - max-height: 175px; - font-family: $sans-serif; - font-size: 12px; - } - .collectionSchemaView-container { - border: 1px solid $intermediate-color; + border-width: $COLLECTION_BORDER_WIDTH; + border-color : $intermediate-color; + border-style: solid; border-radius: $border-radius; box-sizing: border-box; position: absolute; width: 100%; height: 100%; - - .collectionSchemaView-content { - position: absolute; - height: 100%; - width: 100%; - overflow: auto; + + .collectionSchemaView-cellContents { + height: $MAX_ROW_HEIGHT; } + .collectionSchemaView-previewRegion { position: relative; background: $light-color; float: left; height: 100%; + .collectionSchemaView-previewDoc { + height: 100%; + width: 100%; + position: absolute; + } + .collectionSchemaView-input { + position: absolute; + max-width: 150px; + width: 100%; + bottom: 0px; + } + .documentView-node:first-child { + position: relative; + background: $light-color; + } } .collectionSchemaView-previewHandle { position: absolute; @@ -144,7 +109,7 @@ } .rt-tr-group { direction: ltr; - max-height: 44px; + max-height: $MAX_ROW_HEIGHT; } .rt-td { border-width: 1px; @@ -176,7 +141,7 @@ } .ReactTable .rt-th, .ReactTable .rt-td { - max-height: 44; + max-height: $MAX_ROW_HEIGHT; padding: 3px 7px; font-size: 13px; text-align: center; @@ -186,13 +151,71 @@ border-bottom-style: solid; border-bottom-width: 1; } + .documentView-node-topmost { + text-align:left; + transform-origin: center top; + display: inline-block; + } .documentView-node:first-child { background: $light-color; - .imageBox-cont img { - object-fit: contain; - } } } +//options menu styling +#schemaOptionsMenuBtn { + position: absolute; + height: 20px; + width: 20px; + border-radius: 50%; + z-index: 21; + right: 4px; + top: 4px; + pointer-events: auto; + background-color:black; + display:inline-block; + padding: 0px; + font-size: 100%; +} + +ul { + list-style-type: disc; +} + +#schema-options-header { + text-align: center; + padding: 0px; + margin: 0px; +} +.schema-options-subHeader { + color: $intermediate-color; + margin-bottom: 5px; +} +#schemaOptionsMenuBtn:hover { + transform: scale(1.15); +} + +#preview-schema-checkbox-div { + margin-left: 20px; + font-size: 12px; +} + + #options-flyout-div { + text-align: left; + padding:0px; + z-index: 100; + font-family: $sans-serif; + padding-left: 5px; + } + + #schema-col-checklist { + overflow: scroll; + text-align: left; + //background-color: $light-color-secondary; + line-height: 25px; + max-height: 175px; + font-family: $sans-serif; + font-size: 12px; + } + .Resizer { box-sizing: border-box; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 587f60b3d..b3cc3fbd5 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,31 +2,29 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, trace, untracked } from "mobx"; +import { action, computed, observable, untracked, runInAction } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss' import "react-table/react-table.css"; import { Document } from "../../../fields/Document"; import { Field, Opt, FieldWaiting } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; +import { emptyDocFunction, emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { Server } from "../../Server"; -import { setupDrag } from "../../util/DragManager"; +import { SetupDrag } from "../../util/DragManager"; import { CompileScript, ToField } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; +import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; import { anchorPoints, Flyout } from "../DocumentDecorations"; import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; -import { CollectionView } from "./CollectionView"; import { CollectionSubView } from "./CollectionSubView"; -import { TextField } from "../../../fields/TextField"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionBaseView"; -import { emptyFunction, returnFalse } from "../../../Utils"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @@ -36,61 +34,57 @@ import { emptyFunction, returnFalse } from "../../../Utils"; 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; - } - })); + constructor(props: any) { + super(props); + Server.GetField(this.props.keyId, action((field: Opt<Field>) => field instanceof Key && (this.key = field))); } render() { - if (this.key) { - return (<div key={this.key.Id}> + return !this.key ? (null) : + (<div key={this.key.Id}> <input type="checkbox" checked={this.props.checked} onChange={() => this.key && this.props.toggle(this.key)} /> {this.key.Name} </div>); - } - return (null); } } @observer export class CollectionSchemaView extends CollectionSubView { - private _mainCont = React.createRef<HTMLDivElement>(); + private _mainCont?: HTMLDivElement; private _startSplitPercent = 0; 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 _columnsPercentage = 0; @observable _keys: Key[] = []; + @observable _newKeyName: string = ""; @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); } - + @computed get columns() { return this.props.Document.GetList(KeyStore.ColumnsKey, [] as Key[]); } + @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { Document: rowProps.value[0], fieldKey: rowProps.value[1], - isSelected: () => false, - select: () => { }, + ContainingCollectionView: this.props.CollectionView, + isSelected: returnFalse, + select: emptyFunction, isTopMost: false, selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, + focus: emptyDocFunction, active: returnFalse, - onActiveChanged: emptyFunction, + whenActiveChanged: emptyFunction, + PanelHeight: returnZero, + PanelWidth: returnZero, }; let contents = ( <FieldView {...props} /> ); let reference = React.createRef<HTMLDivElement>(); - let onItemDown = setupDrag(reference, () => props.Document, this.props.moveDocument); + let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument); let applyToDoc = (doc: Document, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; @@ -108,11 +102,11 @@ export class CollectionSchemaView extends CollectionSubView { return false; }; return ( - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "56px" }} key={props.Document.Id} ref={reference}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document.Id} ref={reference}> <EditableView display={"inline"} contents={contents} - height={56} + height={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document.Get(props.fieldKey); if (field && field instanceof Field) { @@ -166,53 +160,38 @@ export class CollectionSchemaView extends CollectionSubView { }; } - @computed - get columns() { - return this.props.Document.GetList<Key>(KeyStore.ColumnsKey, []); + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + super.CreateDropTarget(ele); } - @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); + this.props.Document.GetTAsync<ListField<Key>>(KeyStore.ColumnsKey, ListField).then(field => + runInAction(() => { + if (field !== FieldWaiting) { + if (field) { + const index = field.Data.indexOf(key); + if (index === -1) { + this.columns.push(key); + } else { + this.columns.splice(index, 1); + } + } else { + this.props.Document.SetData(KeyStore.ColumnsKey, [key], ListField); + } } - - }); + })); } //toggles preview side-panel of schema @action toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => { - this._startSplitPercent = this.splitPercentage; - if (this._startSplitPercent === this.splitPercentage) { - this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0); - } - } - - @computed - get findAllDocumentKeys(): { [id: string]: boolean } { - const docs = this.props.Document.GetList<Document>(this.props.fieldKey, []); - let keys: { [id: string]: boolean } = {}; - if (this._optionsActivated > -1) { - // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. - // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be - // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. - // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu - // is displayed (unlikely) it won't show up until something else changes. - untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); - } - this.columns.forEach(key => keys[key.Id] = true); - return keys; + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0); } @action onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + let nativeWidth = this._mainCont!.getBoundingClientRect(); this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100))); } @action @@ -231,155 +210,150 @@ export class CollectionSchemaView extends CollectionSubView { document.addEventListener('pointerup', this.onDividerUp); } - @observable _tableWidth = 0; - @action - setTableDimensions = (r: any) => { - this._tableWidth = r.entry.width; - } - @action - setScaling = (r: any) => { - const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); - const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; - this._panelWidth = r.entry.width; - this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight; - this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width); + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (this.props.isSelected()) + e.stopPropagation(); + else e.preventDefault(); + } } - getContentScaling = (): number => this._contentScaling; - getPanelWidth = (): number => this._panelWidth; - getPanelHeight = (): number => this._panelHeight; - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); - getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX - this._tableWidth, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); - - focusDocument = (doc: Document) => { }; - - onPointerDown = (e: React.PointerEvent): void => { - if (this.props.isSelected()) { + onWheel = (e: React.WheelEvent): void => { + if (this.props.active()) { e.stopPropagation(); } } @action addColumn = () => { - this.columns.push(new Key(this.newKeyName)); - this.newKeyName = ""; + this.columns.push(new Key(this._newKeyName)); + this._newKeyName = ""; } - @observable - newKeyName: string = ""; - @action newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.newKeyName = e.currentTarget.value; - } - onWheel = (e: React.WheelEvent): void => { - if (this.props.active()) { - e.stopPropagation(); - } + this._newKeyName = e.currentTarget.value; } - @observable _optionsActivated: number = 0; - @action - OptionsMenuDown = (e: React.PointerEvent) => { - this._optionsActivated++; - } - - @observable previewScript: string = "this"; + @observable previewScript: string = ""; @action onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.previewScript = e.currentTarget.value; } - render() { - library.add(faCog); - library.add(faPlus); - const columns = this.columns; - const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); + get previewDocument(): Document | undefined { + const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; - //all the keys/columns that will be displayed in the schema - const allKeys = this.findAllDocumentKeys; - let doc: any = selected ? selected.Get(new Key(this.previewScript)) : undefined; + return selected ? (this.previewScript ? selected.Get(new Key(this.previewScript)) as Document : selected) : undefined; + } + get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } + get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } + get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; } + + private previewDocNativeWidth = () => this.previewDocument!.GetNumber(KeyStore.NativeWidth, this.previewRegionWidth); + private previewDocNativeHeight = () => this.previewDocument!.GetNumber(KeyStore.NativeHeight, this.previewRegionHeight); + private previewContentScaling = () => { + let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth); + if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) + return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight); + return wscale; + } + private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling(); + private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling(); + get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; } + getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( + - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset, + - this.borderWidth).scale(1 / this.previewContentScaling()); + @computed + get previewPanel() { // let doc = CompileScript(this.previewScript, { this: selected }, true)(); - let content = this._selectedIndex === -1 || !selected ? (null) : ( - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="collectionSchemaView-content" ref={measureRef}> - {doc instanceof Document ? - <DocumentView Document={doc} - addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} - isTopMost={false} - selectOnLoad={false} - ScreenToLocalTransform={this.getPreviewTransform} - ContentScaling={this.getContentScaling} - PanelWidth={this.getPanelWidth} - PanelHeight={this.getPanelHeight} - ContainingCollectionView={undefined} - focus={this.focusDocument} - parentActive={this.props.active} - onActiveChanged={this.props.onActiveChanged} /> : null} - <input value={this.previewScript} onChange={this.onPreviewScriptChange} - style={{ position: 'absolute', bottom: '0px' }} /> - </div> - } - </Measure> - ); - let dividerDragger = this.splitPercentage === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; - - //options button and menu - let optionsMenu = !this.props.active() ? (null) : (<Flyout - anchorPoint={anchorPoints.LEFT_TOP} - content={<div> - <div id="schema-options-header"><h5><b>Options</b></h5></div> - <div id="options-flyout-div"> - <h6 className="schema-options-subHeader">Preview Window</h6> - <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div> - <h6 className="schema-options-subHeader" >Displayed Columns</h6> - <ul id="schema-col-checklist" > - {Array.from(Object.keys(allKeys)).map(item => - (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />))} - </ul> - <input value={this.newKeyName} onChange={this.newKeyChange} /> - <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> + return !this.previewDocument ? (null) : ( + <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> + <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + <DocumentView Document={this.previewDocument} isTopMost={false} selectOnLoad={false} + toggleMinimized={emptyFunction} + addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getPreviewTransform} + ContentScaling={this.previewContentScaling} + PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight} + ContainingCollectionView={this.props.CollectionView} + focus={emptyDocFunction} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + /> </div> + <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} /> </div> - }> - <button id="schemaOptionsMenuBtn" onPointerDown={this.OptionsMenuDown}><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> - </Flyout>); + ); + } - return ( - <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > - <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> - <Measure onResize={this.setTableDimensions}> - {({ measureRef }) => - <div className="collectionSchemaView-tableContainer" ref={measureRef} style={{ width: `calc(100% - ${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, + get documentKeysCheckList() { + const docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); + let keys: { [id: string]: boolean } = {}; + // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. + // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be + // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // is displayed (unlikely) it won't show up until something else changes. + untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); + + this.columns.forEach(key => keys[key.Id] = true); + return Array.from(Object.keys(keys)).map(item => + (<KeyToggle checked={keys[item]} key={item} keyId={item} toggle={this.toggleKey} />)); + } - }} - getTrProps={this.getTrProps} - /> - </div>} - </Measure> - {dividerDragger} - <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}> - {content} + get tableOptionsPanel() { + return !this.props.active() ? (null) : + (<Flyout + anchorPoint={anchorPoints.LEFT_TOP} + content={<div> + <div id="schema-options-header"><h5><b>Options</b></h5></div> + <div id="options-flyout-div"> + <h6 className="schema-options-subHeader">Preview Window</h6> + <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div> + <h6 className="schema-options-subHeader" >Displayed Columns</h6> + <ul id="schema-col-checklist" > + {this.documentKeysCheckList} + </ul> + <input value={this._newKeyName} onChange={this.newKeyChange} /> + <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> </div> - {optionsMenu} </div> - </div > + }> + <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> + </Flyout>); + } + + @computed + get dividerDragger() { + return this.splitPercentage === 0 ? (null) : + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + } + + render() { + library.add(faCog); + library.add(faPlus); + const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); + return ( + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}> + <ReactTable data={children} page={0} pageSize={children.length} showPagination={false} + columns={this.columns.map(col => ({ + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }))} + column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} + getTrProps={this.getTrProps} + /> + </div> + {this.dividerDragger} + {this.previewPanel} + {this.tableOptionsPanel} + </div> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5cdea0568..f9fc7be5a 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -15,14 +15,20 @@ import { ServerUtils } from "../../../server/ServerUtil"; import { Server } from "../../Server"; import { FieldViewProps } from "../nodes/FieldView"; import * as rp from 'request-promise'; +import { CollectionView } from "./CollectionView"; +import { CollectionPDFView } from "./CollectionPDFView"; +import { CollectionVideoView } from "./CollectionVideoView"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Document, allowDuplicates?: boolean) => boolean; removeDocument: (document: Document) => boolean; moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { + CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; } export type CursorEntry = TupleField<[string, string], [number, number]>; @@ -37,6 +43,9 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } + protected CreateDropTarget(ele: HTMLDivElement) { + this.createDropTarget(ele); + } @action protected setCursorPosition(position: [number, number]) { @@ -74,12 +83,21 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { } let added = false; if (de.data.aliasOnDrop || de.data.copyOnDrop) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => added || this.props.addDocument(d), false); + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); } else if (de.data.moveDocument) { const move = de.data.moveDocument; - added = de.data.droppedDocuments.reduce((added: boolean, d) => added || move(d, this.props.Document, this.props.addDocument), false); + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = move(d, this.props.Document, this.props.addDocument); + return moved || added; + }, false); } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => added || this.props.addDocument(d), false); + added = de.data.droppedDocuments.reduce((added: boolean, d) => { + let moved = this.props.addDocument(d); + return moved || added; + }, false); } e.stopPropagation(); return added; @@ -87,8 +105,8 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { return false; } - protected getDocumentFromType(type: string, path: string, options: DocumentOptions): Opt<Document> { - let ctor: ((path: string, options: DocumentOptions) => Document) | undefined; + protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Document>> { + let ctor: ((path: string, options: DocumentOptions) => (Document | Promise<Document | undefined>)) | undefined = undefined; if (type.indexOf("image") !== -1) { ctor = Documents.ImageDocument; } @@ -102,6 +120,10 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { ctor = Documents.PdfDocument; options.nativeWidth = 1200; } + if (type.indexOf("excel") !== -1) { + ctor = Documents.DBDocument; + options.copyDraggedItems = true; + } if (type.indexOf("html") !== -1) { if (path.includes('localhost')) { let s = path.split('/'); @@ -119,7 +141,7 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { return undefined; } ctor = Documents.WebDocument; - options = { height: options.width, ...options, title: path }; + options = { height: options.width, ...options, title: path, nativeWidth: undefined }; } return ctor ? ctor(path, options) : undefined; } @@ -137,10 +159,7 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { e.preventDefault(); if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { - console.log("not good"); - let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); - htmlDoc.SetText(KeyStore.DocumentText, text); - this.props.addDocument(htmlDoc, false); + this.props.addDocument(Documents.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }), false); return; } @@ -152,60 +171,36 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> { let item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.indexOf("uri") !== -1) { let str: string; - let prom = new Promise<string>(res => - e.dataTransfer.items[i].getAsString(res)).then(action((s: string) => { - str = s; - return rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + s)); - })).then(res => { - let type = res.headers["content-type"]; + let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) + .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(result => { + let type = result["content-type"]; if (type) { - let doc = this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }); - if (doc) { - this.props.addDocument(doc, false); - } + this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) + .then(doc => doc && this.props.addDocument(doc, false)); } }); promises.push(prom); - // this.props.addDocument(Documents.WebDocument(s, { ...options, width: 300, height: 300 }), false) } let type = item.type; if (item.kind === "file") { let file = item.getAsFile(); + let dropFileName = file ? file.name : "-empty-"; let formData = new FormData(); + if (file) formData.append('file', file); - if (file) { - formData.append('file', file); - } - - let prom = fetch(upload, { + promises.push(fetch(upload, { method: 'POST', body: formData - }).then(async (res: Response) => { - const json = await res.json(); - json.map((file: any) => { - let path = window.location.origin + file; - runInAction(() => { - let doc = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300 }); - - let docs = this.props.Document.GetT(KeyStore.Data, ListField); - if (docs !== FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - this.props.Document.Set(KeyStore.Data, docs); - } - if (doc) { - docs.Data.push(doc); - } - } - }); - }); - }); - promises.push(prom); + }).then(async (res: Response) => + (await res.json()).map(action((file: any) => + this.getDocumentFromType(type, window.location.origin + file, { ...options, nativeWidth: 600, width: 300, title: dropFileName }). + then(doc => doc && this.props.addDocument(doc, false)))))); } } if (promises.length) { - Promise.all(promises).catch(() => { }).then(() => batch.end()); + Promise.all(promises).finally(() => batch.end()); } else { batch.end(); } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index f2affbf55..8ecc5b67b 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,7 +1,9 @@ -@import "../global_variables"; +@import "../globalCssVariables"; .collectionTreeView-dropTarget { - border: 0px solid transparent; + border-width: $COLLECTION_BORDER_WIDTH; + border-color: transparent; + border-style: solid; border-radius: $border-radius; box-sizing: border-box; height: 100%; @@ -48,15 +50,15 @@ .docContainer:hover { .delete-button { display: inline; - width: auto; + // width: auto; } } .delete-button { color: $intermediate-color; - float: right; + // float: right; margin-left: 15px; - margin-top: 3px; + // margin-top: 3px; display: inline; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 659cff9fe..905b48db7 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,20 +1,17 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { FieldWaiting } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { setupDrag, DragManager } from "../../util/DragManager"; +import { DragManager, SetupDrag } from "../../util/DragManager"; import { EditableView } from "../EditableView"; -import "./CollectionTreeView.scss"; -import { CollectionView } from "./CollectionView"; import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; import React = require("react"); -import { COLLECTION_BORDER_WIDTH } from './CollectionBaseView'; -import { props } from 'bluebird'; export interface TreeViewProps { @@ -77,7 +74,7 @@ class TreeView extends React.Component<TreeViewProps> { */ renderTitle() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = setupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag); + let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag); let editableView = (titleString: string) => (<EditableView display={"inline"} @@ -139,7 +136,10 @@ export class CollectionTreeView extends CollectionSubView { ); return ( - <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> + <div id="body" className="collectionTreeView-dropTarget" + style={{ borderRadius: "inherit" }} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> <div className="coll-title"> <EditableView contents={this.props.Document.Title} diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index b02983a2e..779dc8fc3 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -7,6 +7,7 @@ import React = require("react"); import "./CollectionVideoView.scss"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { emptyFunction } from "../../../Utils"; @observer @@ -36,10 +37,13 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { ]); } + _ele: HTMLDivElement | null = null; @action mainCont = (ele: HTMLDivElement | null) => { + this._ele = ele; if (ele) { this._player = ele.getElementsByTagName("video")[0]; + console.log(this._player); if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) { this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1); } @@ -56,9 +60,12 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action updateTimecode = () => { + this._player = this._player ? this._player : this._ele ? this._ele.getElementsByTagName("video")[0] : undefined; if (this._player) { - if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero !== -1) { - this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero; + let timecode = (this._player as any).hasOwnProperty("AHackBecauseSomethingResetsTheVideoToZero") ? + (this._player as any).AHackBecauseSomethingResetsTheVideoToZero : -1; + if (timecode !== -1 && Object) { + this._player.currentTime = timecode; (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1; } else { this._currentTimecode = this._player.currentTime; @@ -100,7 +107,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document.Id !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "VideoOptions", event: () => { } }); + ContextMenu.Instance.addItem({ description: "VideoOptions", event: emptyFunction }); } } @@ -108,7 +115,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { let props = { ...this.props, ...renderProps }; return ( <> - <CollectionFreeFormView {...props} /> + <CollectionFreeFormView {...props} CollectionView={this} /> {this.props.isSelected() ? this.uIButtons : (null)} </> ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8abd0a02d..675e720e2 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -10,6 +10,7 @@ import { CurrentUserUtils } from '../../../server/authentication/models/current_ import { KeyStore } from '../../../fields/KeyStore'; import { observer } from 'mobx-react'; import { undoBatch } from '../../util/UndoManager'; +import { trace } from 'mobx'; @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -18,18 +19,20 @@ export class CollectionView extends React.Component<FieldViewProps> { private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; switch (type) { - case CollectionViewType.Schema: return (<CollectionSchemaView {...props} />); - case CollectionViewType.Docking: return (<CollectionDockingView {...props} />); - case CollectionViewType.Tree: return (<CollectionTreeView {...props} />); + case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); + case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); + case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />); case CollectionViewType.Freeform: default: - return (<CollectionFreeFormView {...props} />); + return (<CollectionFreeFormView {...props} CollectionView={this} />); } return (null); } + get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document.Id !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document.Id !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform)) }); ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema)) }); ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree)) }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 3b2f79be1..3e8a8a442 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -3,4 +3,10 @@ stroke-width: 3; transform: translate(10000px,10000px); pointer-events: all; +} +.collectionfreeformlinkview-linkCircle { + stroke: black; + stroke-width: 3; + transform: translate(10000px,10000px); + pointer-events: all; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 081b3eb6c..8cd6c7624 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -5,33 +5,54 @@ import { Utils } from "../../../../Utils"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); +import { InkingControl } from "../../InkingControl"; export interface CollectionFreeFormLinkViewProps { A: Document; B: Document; LinkDocs: Document[]; + addDocument: (document: Document, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Document) => boolean; } @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { onPointerDown = (e: React.PointerEvent) => { - this.props.LinkDocs.map(l => - console.log("Link:" + l.Title)); + if (e.button === 0 && !InkingControl.Instance.selectedTool) { + let a = this.props.A; + let b = this.props.B; + let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Width() / 2); + let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Height() / 2); + let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Width() / 2); + let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Height() / 2); + this.props.LinkDocs.map(l => { + let width = l.GetNumber(KeyStore.Width, 0); + l.SetNumber(KeyStore.X, (x1 + x2) / 2 - width / 2); + l.SetNumber(KeyStore.Y, (y1 + y2) / 2 + 10); + if (!this.props.removeDocument(l)) this.props.addDocument(l, false); + }); + e.stopPropagation(); + e.preventDefault(); + } } render() { let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; - let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Width, 0) / 2); - let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Height, 0) / 2); - let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Width, 0) / 2); - let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Height, 0) / 2); + let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Width() / 2); + let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Height() / 2); + let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Width() / 2); + let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Height() / 2); return ( - <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown} - style={{ strokeWidth: `${l.length * 5}` }} - x1={`${x1}`} y1={`${y1}`} - x2={`${x2}`} y2={`${y2}`} /> + <> + <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + style={{ strokeWidth: `${l.length * 5}` }} + x1={`${x1}`} y1={`${y1}`} + x2={`${x2}`} y2={`${y2}`} /> + <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} /> + </> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index cf058090d..b97df7556 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,7 +1,6 @@ -import { computed, reaction } from "mobx"; +import { computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../../fields/Document"; -import { FieldWaiting } from "../../../../fields/Field"; import { KeyStore } from "../../../../fields/KeyStore"; import { ListField } from "../../../../fields/ListField"; import { Utils } from "../../../../Utils"; @@ -15,18 +14,15 @@ import React = require("react"); @observer export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { - HackToAvoidReactionFiringUnnecessarily?: Document = undefined; + _brushReactionDisposer?: IReactionDisposer; componentDidMount() { - this.HackToAvoidReactionFiringUnnecessarily = this.props.Document; - reaction(() => - DocumentManager.Instance.getAllDocumentViews(this.HackToAvoidReactionFiringUnnecessarily!). - map(dv => dv.props.Document.GetNumber(KeyStore.X, 0)), + this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)), () => { - let views = DocumentManager.Instance.getAllDocumentViews(this.props.Document); + let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc.GetText(KeyStore.BackgroundLayout, "").indexOf("istogram") !== -1); for (let i = 0; i < views.length; i++) { for (let j = 0; j < views.length; j++) { - let srcDoc = views[j].props.Document; - let dstDoc = views[i].props.Document; + let srcDoc = views[j]; + let dstDoc = views[i]; let x1 = srcDoc.GetNumber(KeyStore.X, 0); let x1w = srcDoc.GetNumber(KeyStore.Width, -1); let x2 = dstDoc.GetNumber(KeyStore.X, 0); @@ -53,7 +49,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title); linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField); - brushAction = brushAction = (field: ListField<Document>) => { + brushAction = (field: ListField<Document>) => { if (findBrush(field) === -1) { console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title); (findBrush(field) === -1) && field.Data.push(linkDoc); @@ -67,11 +63,24 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP } }); } + componentWillUnmount() { + if (this._brushReactionDisposer) { + this._brushReactionDisposer(); + } + } documentAnchors(view: DocumentView) { let equalViews = [view]; let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document); if (containerDoc && containerDoc instanceof Document) { - equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype()!); + equalViews.push(...DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype()!)); + } + if (view.props.ContainingCollectionView) { + let collid = view.props.ContainingCollectionView.props.Document.Id; + this.props.Document.GetList(this.props.fieldKey, [] as Document[]). + filter(child => + child.Id === collid).map(view => + DocumentManager.Instance.getDocumentViews(view).map(view => + equalViews.push(view))); } return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); } @@ -83,22 +92,21 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP let targetViews = this.documentAnchors(connection.b); let possiblePairs: { a: Document, b: Document, }[] = []; srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); - possiblePairs.map(possiblePair => { - if (!drawnPairs.reduce((found, drawnPair) => { + possiblePairs.map(possiblePair => + drawnPairs.reduce((found, drawnPair) => { let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b); - if (match) { - if (!drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) { - drawnPair.l.push(connection.l); - } + if (match && !drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) { + drawnPair.l.push(connection.l); } return match || found; - }, false)) { - drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); - } - }); + }, false) + || + drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) + ); return drawnPairs; }, [] as { a: Document, b: Document, l: Document[] }[]); - return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); + return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} + removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss new file mode 100644 index 000000000..c5b8fc5e8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss @@ -0,0 +1,24 @@ +@import "globalCssVariables"; + +.collectionFreeFormRemoteCursors-cont { + + position:absolute; + z-index: $remoteCursors-zindex; + transform-origin: 'center center'; +} +.collectionFreeFormRemoteCursors-canvas { + + position:absolute; + width: 20px; + height: 20px; + opacity: 0.5; + border-radius: 50%; + border: 2px solid black; +} +.collectionFreeFormRemoteCursors-symbol { + font-size: 14; + color: black; + // fontStyle: "italic", + margin-left: -12; + margin-top: 4; +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index fc832264d..cf0a6de00 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -12,7 +12,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV protected getCursors(): CursorEntry[] { let doc = this.props.Document; let id = CurrentUserUtils.id; - let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []); + let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]); let notMe = cursors.filter(entry => entry.Data[0][0] !== id); return id ? notMe : []; } @@ -59,37 +59,17 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV 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', - }} + <div key={id} className="collectionFreeFormRemoteCursors-cont" + style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }} > - <canvas + <canvas className="collectionFreeFormRemoteCursors-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> + <p className="collectionFreeFormRemoteCursors-symbol"> + {email[0].toUpperCase()} + </p> </div> ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index f9d786c9b..cb849b325 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,12 +1,4 @@ -@import "../../global_variables"; - -.collectionfreeformview-measure { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} +@import "../../globalCssVariables"; .collectionfreeformview-ease { position: absolute; @@ -15,11 +7,11 @@ width: 100%; height: 100%; transform-origin: left top; - transition: transform 2s; + transition: transform 1s; } .collectionfreeformview-none { - position: absolute; + position: inherit; top: 0; left: 0; width: 100%; @@ -29,7 +21,7 @@ .collectionfreeformview-container { .collectionfreeformview>.jsx-parser { - position: absolute; + position: inherit; height: 100%; width: 100%; } @@ -40,7 +32,6 @@ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; // } - box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; border: 0px solid $light-color-secondary; border-radius: $border-radius; @@ -52,15 +43,17 @@ width: 100%; height: 100%; } + .collectionfreeformview-overlay { .collectionfreeformview>.jsx-parser { - position: absolute; + position: inherit; height: 100%; } .formattedTextBox-cont { background: $light-color-secondary; + overflow: visible; } opacity: 0.99; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4562fd8a7..2fa59d589 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,58 +1,63 @@ -import { action, computed, observable, trace, ObservableSet, runInAction } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../../fields/Document"; -import { FieldWaiting } from "../../../../fields/Field"; import { KeyStore } from "../../../../fields/KeyStore"; -import { TextField } from "../../../../fields/TextField"; +import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; +import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; +import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { InkingCanvas } from "../../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { DocumentViewProps } from "../../nodes/DocumentView"; -import { COLLECTION_BORDER_WIDTH } from "../CollectionBaseView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; +import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); -import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; -import { PreviewCursor } from "./PreviewCursor"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { NumberField } from "../../../../fields/NumberField"; -import { Main } from "../../Main"; -import Measure from "react-measure"; +import { BooleanField } from "../../../../fields/BooleanField"; @observer export class CollectionFreeFormView extends CollectionSubView { - public _canvasRef = React.createRef<HTMLDivElement>(); + public static RIGHT_BTN_DRAG = false; private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + private _lastX: number = 0; + private _lastY: number = 0; + private get _pwidth() { return this.props.PanelWidth(); } + private get _pheight() { return this.props.PanelHeight(); } - 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 and receive text input - this._selectOnLoaded = newBox.Id; + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } + private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + private panX = () => this.props.Document.GetNumber(KeyStore.PanX, 0); + private panY = () => this.props.Document.GetNumber(KeyStore.PanY, 0); + private zoomScaling = () => this.props.Document.GetNumber(KeyStore.Scale, 1); + private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0; // shift so pan position is at center of window for non-overlay collections + private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections + private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); + private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); + private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); + private addLiveTextBox = (newBox: Document) => { + this._selectOnLoaded = newBox.Id;// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox, false); } - - public addDocument = (newBox: Document, allowDuplicates: boolean) => { - let added = this.props.addDocument(newBox, false); + @action + private addDocument = (newBox: Document, allowDuplicates: boolean) => { + this.props.addDocument(newBox, false); this.bringToFront(newBox); - return added; + return true; } - - public selectDocuments = (docs: Document[]) => { + private selectDocuments = (docs: Document[]) => { SelectionManager.DeselectAll; - docs.map(doc => { - const dv = DocumentManager.Instance.getDocumentView(doc); - if (dv) { - SelectionManager.SelectDoc(dv, true); - } - }); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => + SelectionManager.SelectDoc(dv!, true)); } - public getActiveDocuments = () => { var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => { @@ -64,61 +69,38 @@ export class CollectionFreeFormView extends CollectionSubView { }, [] as Document[]); } - @observable public DownX: number = 0; - @observable public DownY: number = 0; - @observable private _lastX: number = 0; - @observable private _lastY: number = 0; - @observable private _pwidth: number = 0; - @observable private _pheight: 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); } - @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } - @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this._pwidth / 2 : 0; } // shift so pan position is at center of window for non-overlay collections - @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this._pheight / 2 : 0; }// shift so pan position is at center of window for non-overlay collections - @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (super.drop(e, de)) { - if (de.data instanceof DragManager.DocumentDragData) { - let droppedDocs = de.data.droppedDocuments; - let xoff = de.data.xOffset as number || 0; - let yoff = de.data.yOffset as number || 0; - if (droppedDocs.length) { - let screenX = de.x - xoff; - let screenY = de.y - yoff; - const [x, y] = this.getTransform().transformPoint(screenX, screenY); - let dragDoc = droppedDocs[0]; - let dragX = dragDoc.GetNumber(KeyStore.X, 0); - let dragY = dragDoc.GetNumber(KeyStore.Y, 0); - droppedDocs.map(async d => { - let docX = d.GetNumber(KeyStore.X, 0); - let docY = d.GetNumber(KeyStore.Y, 0); - d.SetNumber(KeyStore.X, x + (docX - dragX)); - d.SetNumber(KeyStore.Y, y + (docY - dragY)); - let docW = await d.GetTAsync(KeyStore.Width, NumberField); - let docH = await d.GetTAsync(KeyStore.Height, NumberField); - if (!docW) { - d.SetNumber(KeyStore.Width, 300); - } - if (!docH) { - d.SetNumber(KeyStore.Height, 300); - } - this.bringToFront(d); - }); - } + if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) { + if (de.data.droppedDocuments.length) { + let dragDoc = de.data.droppedDocuments[0]; + let zoom = dragDoc.GetNumber(KeyStore.ZoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = de.data.droppedDocuments[0].GetNumber(KeyStore.X, 0); + let dropY = de.data.droppedDocuments[0].GetNumber(KeyStore.Y, 0); + de.data.droppedDocuments.map(d => { + d.SetNumber(KeyStore.X, x + (d.GetNumber(KeyStore.X, 0) - dropX)); + d.SetNumber(KeyStore.Y, y + (d.GetNumber(KeyStore.Y, 0) - dropY)); + if (!d.GetNumber(KeyStore.Width, 0)) { + d.SetNumber(KeyStore.Width, 300); + } + if (!d.GetNumber(KeyStore.Height, 0)) { + let nw = d.GetNumber(KeyStore.NativeWidth, 0); + let nh = d.GetNumber(KeyStore.NativeHeight, 0); + d.SetNumber(KeyStore.Height, nw && nh ? nh / nw * d.Width() : 300); + } + this.bringToFront(d); + }); + SelectionManager.ReselectAll(); } return true; } return false; } - @action cleanupInteractions = () => { document.removeEventListener("pointermove", this.onPointerMove); @@ -127,83 +109,106 @@ export class CollectionFreeFormView extends CollectionSubView { @action onPointerDown = (e: React.PointerEvent): void => { - if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling !== 1)) || e.button === 0) && this.props.active()) { - document.removeEventListener("pointermove", this.onPointerMove); + let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { + var dv = DocumentManager.Instance.getDocumentView(doc); + return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); + }, false); + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && + (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || + (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && + ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) { + this.cleanupInteractions(); document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._lastX = this.DownX = e.pageX; - this._lastY = this.DownY = e.pageY; - if (this.props.isSelected()) { - e.stopPropagation(); - } + this._lastX = e.pageX; + this._lastY = e.pageY; } } - @action onPointerUp = (e: PointerEvent): void => { - e.stopPropagation(); - this.cleanupInteractions(); } @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.props.active()) { - 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.SetPan(x - dx, y - dy); - this._lastX = e.pageX; - this._lastY = e.pageY; - e.stopPropagation(); - e.preventDefault(); + if (!e.cancelBubble) { + let x = this.props.Document.GetNumber(KeyStore.PanX, 0); + let y = this.props.Document.GetNumber(KeyStore.PanY, 0); + let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + if (!this.isAnnotationOverlay) { + let minx = docs.length ? docs[0].GetNumber(KeyStore.X, 0) : 0; + let maxx = docs.length ? docs[0].Width() + minx : minx; + let miny = docs.length ? docs[0].GetNumber(KeyStore.Y, 0) : 0; + let maxy = docs.length ? docs[0].Height() + miny : miny; + let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let x = doc.GetNumber(KeyStore.X, 0); + let xe = x + doc.GetNumber(KeyStore.Width, 0); + let y = doc.GetNumber(KeyStore.Y, 0); + let ye = y + doc.GetNumber(KeyStore.Height, 0); + return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], + [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; + }, [[minx, maxx], [miny, maxy]]); + let panelwidth = this._pwidth / this.zoomScaling() / 2; + let panelheight = this._pheight / this.zoomScaling() / 2; + if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx; + if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx; + if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy; + if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy; } + this.setPan(x - dx, y - dy); + this._lastX = e.pageX; + this._lastY = e.pageY; + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); } } @action onPointerWheel = (e: React.WheelEvent): void => { - this.props.select(false); + // if (!this.props.active()) { + // return; + // } + let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { + var dv = DocumentManager.Instance.getDocumentView(doc); + return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); + }, false); + if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { + return; + } e.stopPropagation(); - let coefficient = 1000; + const coefficient = 1000; if (e.ctrlKey) { - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - const coefficient = 1000; let deltaScale = (1 - (e.deltaY / coefficient)); - this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); - this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeWidth, this.nativeWidth * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeHeight, this.nativeHeight * deltaScale); e.stopPropagation(); e.preventDefault(); } else { // if (modes[e.deltaMode] === 'pixels') coefficient = 50; // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? - let transform = this.getTransform(); - let deltaScale = (1 - (e.deltaY / coefficient)); - if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling; + if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { + deltaScale = 1 / this.zoomScaling(); } - let [x, y] = transform.transformPoint(e.clientX, e.clientY); - - let localTransform = this.getLocalTransform(); - localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y); - // console.log(localTransform) + if (deltaScale < 0) deltaScale = -deltaScale; + let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); + let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); - this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); + let safeScale = Math.abs(localTransform.Scale); + this.props.Document.SetNumber(KeyStore.Scale, Math.abs(safeScale)); + this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); + e.stopPropagation(); } } @action - private SetPan(panX: number, panY: number) { - Main.Instance.SetTextDoc(undefined, undefined); - var x1 = this.getLocalTransform().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)); + setPan(panX: number, panY: number) { + var scale = this.getLocalTransform().inverse().Scale; + const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); + const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); @@ -220,51 +225,31 @@ export class CollectionFreeFormView extends CollectionSubView { @action bringToFront(doc: Document) { - const { fieldKey: fieldKey, Document: Document } = this.props; - - const value: Document[] = Document.GetList<Document>(fieldKey, []).slice(); - value.sort((doc1, doc2) => { - if (doc1 === doc) { - return 1; - } - if (doc2 === doc) { - return -1; - } + let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).slice(); + docs.sort((doc1, doc2) => { + if (doc1 === doc) return 1; + if (doc2 === doc) return -1; return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); - }).map((doc, index) => { - doc.SetNumber(KeyStore.ZIndex, index + 1); - }); - } - - @computed get backgroundLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); - if (field && field !== FieldWaiting) { - return field.Data; - } - } - @computed get overlayLayout(): string | undefined { - let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); - if (field && field !== FieldWaiting) { - return field.Data; - } + }).map((doc, index) => doc.SetNumber(KeyStore.ZIndex, index + 1)); + doc.SetNumber(KeyStore.ZIndex, docs.length + 1); } focusDocument = (doc: Document) => { + SelectionManager.DeselectAll(); this.props.Document.SetText(KeyStore.PanTransformType, "Ease"); - let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; - let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; - this.SetPan(x, y); + this.setPan( + doc.GetNumber(KeyStore.X, 0) + doc.Width() / 2, + doc.GetNumber(KeyStore.Y, 0) + doc.Height() / 2); this.props.focus(this.props.Document); - // if(this.props.Document.GetText(KeyStore.PanTransformType, "") === "Ease") { - // setTimeout(function(){ - // this.props.Document.SetText(KeyStore.PanTransformType, "None"); - // }.bind(this),5000); // wait 5 seconds, then reset to false - // } + if (this.props.Document.GetText(KeyStore.PanTransformType, "") === "Ease") { + setTimeout(() => this.props.Document.SetText(KeyStore.PanTransformType, "None"), 5000); // wait 5 seconds, then reset to false + } } getDocumentViewProps(document: Document): DocumentViewProps { return { Document: document, + toggleMinimized: emptyFunction, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, @@ -273,86 +258,116 @@ export class CollectionFreeFormView extends CollectionSubView { selectOnLoad: document.Id === this._selectOnLoaded, PanelWidth: document.Width, PanelHeight: document.Height, - ContentScaling: this.noScaling, - ContainingCollectionView: undefined, + ContentScaling: returnOne, + ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, parentActive: this.props.active, - onActiveChanged: this.props.active, + whenActiveChanged: this.props.active, }; } @computed get views() { var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); - return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => { + let docviews = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => { var page = doc.GetNumber(KeyStore.Page, -1); if (page === curPage || page === -1) { - prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />); + let minim = doc.GetT(KeyStore.IsMinimized, BooleanField); + if (minim === undefined || (minim && !minim.Data)) + prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />); } return prev; }, [] as JSX.Element[]); - } - @computed - get backgroundView() { - return !this.backgroundLayout ? (null) : - (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)} - layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); + setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + + return docviews; } - @computed - get overlayView() { - return !this.overlayLayout ? (null) : - (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)} - layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); + + @action + onCursorMove = (e: React.PointerEvent) => { + super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()); - getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH); - getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.scale).translate(this.panX, this.panY); - noScaling = () => 1; - childViews = () => this.views; + private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />]; + render() { + const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; + const easing = () => this.props.Document.GetText(KeyStore.PanTransformType, "") === "Ease"; + return ( + <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} + style={{ borderRadius: "inherit" }} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + {/* <svg viewBox="0 0 180 18" style={{ top: "50%", opacity: 0.05, position: "absolute" }}> + <text y="15" > + {this.props.Document.Title} + </text> + </svg> */} + <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} + addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} + getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> + <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} + easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + + <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > + {this.childViews} + </InkingCanvas> + </CollectionFreeFormLinksView> + <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />] + </CollectionFreeFormViewPannableContents> + <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} /> + </MarqueeView> + </div> + ); + } +} +@observer +class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> { + @computed get overlayView() { + let overlayLayout = this.props.Document.GetText(KeyStore.OverlayLayout, ""); + return !overlayLayout ? (null) : + (<DocumentContentsView {...this.props} layoutKey={KeyStore.OverlayLayout} + isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); + } render() { - let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; + return this.overlayView; + } +} - const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); - const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); +@observer +class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { + @computed get backgroundView() { + let backgroundLayout = this.props.Document.GetText(KeyStore.BackgroundLayout, ""); + return !backgroundLayout ? (null) : + (<DocumentContentsView {...this.props} layoutKey={KeyStore.BackgroundLayout} + isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); + } + render() { + return this.backgroundView; + } +} - if (this.props.Document.GetText(KeyStore.PanTransformType, "") === "Ease") { - var freeformclass = "collectionfreeformview-ease"; - } else { - var freeformclass = "collectionfreeformview-none"; - } +interface CollectionFreeFormViewPannableContentsProps { + centeringShiftX: () => number; + centeringShiftY: () => number; + panX: () => number; + panY: () => number; + zoomScaling: () => number; + easing: () => boolean; +} - return ( - <Measure onResize={(r: any) => runInAction(() => { this._pwidth = r.entry.width; this._pheight = r.entry.height })}> - {({ measureRef }) => ( - <div className={`collectionfreeformview-measure`} ref={measureRef}> - <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} - onPointerDown={this.onPointerDown} onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))} - onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel} - style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} ref={this.createDropTarget}> - <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} - addDocument={this.addDocument} removeDocument={this.props.removeDocument} - getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> - <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} - getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} > - <div className={freeformclass} ref={this._canvasRef} - style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}> - {this.backgroundView} - <CollectionFreeFormLinksView {...this.props}> - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > - {this.childViews} - </InkingCanvas> - </CollectionFreeFormLinksView> - <CollectionFreeFormRemoteCursors {...this.props} /> - </div> - {this.overlayView} - </PreviewCursor> - </MarqueeView> - </div> - </div>)} - </Measure> - ); +@observer +class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ + render() { + let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none"); + const cenx = this.props.centeringShiftX(); + const ceny = this.props.centeringShiftY(); + const panx = -this.props.panX(); + const pany = -this.props.panY(); + const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire + return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> + {this.props.children} + </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 0b406e722..ae0a9fd48 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -1,6 +1,6 @@ .marqueeView { - position: absolute; + position: inherit; top:0; left:0; width:100%; @@ -13,4 +13,14 @@ border-width: 1px; border-color: black; pointer-events: none; + .marquee-legend { + bottom:-18px; + left:0; + position: absolute; + font-size: 9; + white-space:nowrap; + } + .marquee-legend::after { + content: "Press: C (collection), or Delete" + } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1e6faafb3..da1170759 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable, trace } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../../fields/Document"; import { FieldWaiting } from "../../../../fields/Field"; @@ -7,11 +7,13 @@ import { KeyStore } from "../../../../fields/KeyStore"; import { Documents } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; +import { PreviewCursor } from "../../PreviewCursor"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; -import { PreviewCursor } from "./PreviewCursor"; import React = require("react"); +import { Utils } from "../../../../Utils"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -21,6 +23,8 @@ interface MarqueeViewProps { activeDocuments: () => Document[]; selectDocuments: (docs: Document[]) => void; removeDocument: (doc: Document) => boolean; + addLiveTextDocument: (doc: Document) => void; + isSelected: () => boolean; } @observer @@ -30,32 +34,41 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; - @observable _used: boolean = false; @observable _visible: boolean = false; - static DRAG_THRESHOLD = 4; + _commandExecuted = false; @action cleanupInteractions = (all: boolean = false) => { if (all) { - document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); - } else { - this._used = true; + document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); this._visible = false; } @action + onKeyPress = (e: KeyboardEvent) => { + //make textbox and add it to this collection + let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + this.props.addLiveTextDocument(newBox); + e.stopPropagation(); + } + @action onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - this._used = false; + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; + this._commandExecuted = false; + PreviewCursor.Visible = false; + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); } + if (e.altKey) + e.preventDefault(); } @action @@ -63,24 +76,45 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._lastX = e.pageX; this._lastY = e.pageY; if (!e.cancelBubble) { - if (!this._used && e.buttons === 1 && !e.altKey && !e.metaKey && - (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) { - this._visible = true; + if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { + if (!this._commandExecuted) { + this._visible = true; + } + e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } + if (e.altKey) + e.preventDefault(); } @action onPointerUp = (e: PointerEvent): void => { + if (this._visible) { + let mselect = this.marqueeSelect(); + if (!e.shiftKey) { + SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); + } + this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); + } this.cleanupInteractions(true); - this._visible = false; - let mselect = this.marqueeSelect(); - if (!e.shiftKey) { - SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); + if (e.altKey) + e.preventDefault(); + } + + @action + onClick = (e: React.MouseEvent): void => { + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (this.props.isSelected()) { + PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress); + } + // let the DocumentView stopPropagation of this event when it selects this document + } else { // why do we get a click event when the cursor have moved a big distance? + // let's cut it off here so no one else has to deal with it. + e.stopPropagation(); } - this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -97,45 +131,74 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } + @undoBatch @action marqueeCommand = (e: KeyboardEvent) => { - if (e.key === "Backspace" || e.key === "Delete") { + if (this._commandExecuted) { + return; + } + if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") { + this._commandExecuted = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); if (ink && ink !== FieldWaiting) { this.marqueeInkDelete(ink.Data); } - this.cleanupInteractions(); + this.cleanupInteractions(false); + e.stopPropagation(); } - if (e.key === "c") { + if (e.key === "c" || e.key === "r" || e.key === "e") { + this._commandExecuted = true; + e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().map(d => { this.props.removeDocument(d); d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2); d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2); d.SetNumber(KeyStore.Page, -1); - d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); return d; }); let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); let inkData = ink && ink !== FieldWaiting ? ink.Data : undefined; - //setTimeout(() => { + let zoomBasis = this.props.container.props.Document.GetNumber(KeyStore.Scale, 1); let newCollection = Documents.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panx: 0, pany: 0, - width: bounds.width, - height: bounds.height, - backgroundColor: "Transparent", + borderRounding: e.key === "e" ? -1 : undefined, + scale: zoomBasis, + width: bounds.width * zoomBasis, + height: bounds.height * zoomBasis, ink: inkData ? this.marqueeInkSelect(inkData) : undefined, title: "a nested collection" }); - this.props.addDocument(newCollection, false); + this.marqueeInkDelete(inkData); - // }, 100); - this.cleanupInteractions(); + // SelectionManager.DeselectAll(); + if (e.key === "r") { + let summary = Documents.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + summary.GetPrototype()!.CreateLink(newCollection.GetPrototype()!); + this.props.addLiveTextDocument(summary); + e.preventDefault(); + } + else { + this.props.addDocument(newCollection, false); + } + this.cleanupInteractions(false); + } + if (e.key === "s") { + this._commandExecuted = true; + e.stopPropagation(); + e.preventDefault(); + let bounds = this.Bounds; + let selected = this.marqueeSelect(); SelectionManager.DeselectAll(); + let summary = Documents.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + this.props.addLiveTextDocument(summary); + selected.map(select => summary.GetPrototype()!.CreateLink(select.GetPrototype()!)); + + this.cleanupInteractions(false); } } @action @@ -176,10 +239,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let selRect = this.Bounds; let selection: Document[] = []; this.props.activeDocuments().map(doc => { + var z = doc.GetNumber(KeyStore.ZoomBasis, 1); 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); + var w = doc.Width() / z; + var h = doc.Height() / z; if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -191,11 +255,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> get marqueeDiv() { let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />; + return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} > + <span className="marquee-legend" /> + </div>; } render() { - return <div className="marqueeView" onPointerDown={this.onPointerDown}> + return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this.props.children} {!this._visible ? (null) : this.marqueeDiv} </div>; diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss deleted file mode 100644 index 7a67c29bf..000000000 --- a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss +++ /dev/null @@ -1,27 +0,0 @@ - -.previewCursor { - color: black; - position: absolute; - transform-origin: left top; - top: 0; - left:0; - pointer-events: none; -} -.previewCursorView { - top: 0; - left:0; - position: absolute; - width:100%; - height:100%; -} - -//this is an animation for the blinking cursor! -// @keyframes blink { -// 0% {opacity: 0} -// 49%{opacity: 0} -// 50% {opacity: 1} -// } - -// #previewCursor { -// animation: blink 1s infinite; -// }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx deleted file mode 100644 index 8eabb020a..000000000 --- a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { action, observable, trace, computed, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { Documents } from "../../../documents/Documents"; -import { Transform } from "../../../util/Transform"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; -import "./PreviewCursor.scss"; -import React = require("react"); -import { interfaceDeclaration } from "babel-types"; - - -export interface PreviewCursorProps { - getTransform: () => Transform; - getContainerTransform: () => Transform; - container: CollectionFreeFormView; - addLiveTextDocument: (doc: Document) => void; -} - -@observer -export class PreviewCursor extends React.Component<PreviewCursorProps> { - @observable _lastX: number = 0; - @observable _lastY: number = 0; - @observable public _visible: boolean = false; - @observable public DownX: number = 0; - @observable public DownY: number = 0; - _showOnUp: boolean = false; - - @action - cleanupInteractions = () => { - document.removeEventListener("pointerup", this.onPointerUp, true); - document.removeEventListener("pointermove", this.onPointerMove, true); - } - - @action - onPointerDown = (e: React.PointerEvent) => { - if (e.button === 0 && this.props.container.props.active()) { - document.removeEventListener("keypress", this.onKeyPress, false); - this._showOnUp = true; - this.DownX = e.pageX; - this.DownY = e.pageY; - document.addEventListener("pointerup", this.onPointerUp, true); - document.addEventListener("pointermove", this.onPointerMove, true); - } - } - @action - onPointerMove = (e: PointerEvent): void => { - if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) { - this._showOnUp = false; - this._visible = false; - } - } - - @action - onPointerUp = (e: PointerEvent): void => { - if (this._showOnUp) { - document.addEventListener("keypress", this.onKeyPress, false); - this._lastX = this.DownX; - this._lastY = this.DownY; - this._visible = true; - } - this.cleanupInteractions(); - } - - @action - onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. - //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - //make textbox and add it to this collection - let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); - this.props.addLiveTextDocument(newBox); - document.removeEventListener("keypress", this.onKeyPress, false); - this._visible = false; - e.stopPropagation(); - } - } - - getPoint = () => this.props.getContainerTransform().transformPoint(this._lastX, this._lastY); - getVisible = () => this._visible; - setVisible = (v: boolean) => { - this._visible = v; - document.removeEventListener("keypress", this.onKeyPress, false); - } - render() { - return ( - <div className="previewCursorView" onPointerDown={this.onPointerDown}> - {this.props.children} - <PreviewCursorPrompt setVisible={this.setVisible} getPoint={this.getPoint} getVisible={this.getVisible} /> - </div> - ); - } -} - -export interface PromptProps { - getPoint: () => number[]; - getVisible: () => boolean; - setVisible: (v: boolean) => void; -} - -@observer -export class PreviewCursorPrompt extends React.Component<PromptProps> { - private _promptRef = React.createRef<HTMLDivElement>(); - - //when focus is lost, this will remove the preview cursor - @action onBlur = (): void => this.props.setVisible(false); - - render() { - let p = this.props.getPoint(); - if (this.props.getVisible() && this._promptRef.current) { - this._promptRef.current.focus(); - } - return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._promptRef} - style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, opacity: this.props.getVisible() ? 1 : 0 }}> - I - </div >; - } -}
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss new file mode 100644 index 000000000..4f68b71b0 --- /dev/null +++ b/src/client/views/globalCssVariables.scss @@ -0,0 +1,33 @@ +@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; +// + + // dragged items +$contextMenu-zindex: 1000; // context menu shows up over everything +$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc +$docDecorations-zindex: 998; // then doc decorations appear over everything else +$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? +$COLLECTION_BORDER_WIDTH: 1; +$MINIMIZED_ICON_SIZE:25; +$MAX_ROW_HEIGHT: 44px; +:export { + contextMenuZindex: $contextMenu-zindex; + COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; + MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; + MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; +}
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts new file mode 100644 index 000000000..9788d31f7 --- /dev/null +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -0,0 +1,10 @@ + +interface IGlobalScss { + contextMenuZindex: string; // context menu shows up over everything + COLLECTION_BORDER_WIDTH: string; + MINIMIZED_ICON_SIZE: string; + MAX_ROW_HEIGHT: string; +} +declare const globalCssVariables: IGlobalScss; + +export = globalCssVariables;
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 77f41105f..bad78cbd5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,88 +1,193 @@ -import { computed } from "mobx"; +import { computed, trace, action } from "mobx"; import { observer } from "mobx-react"; import { KeyStore } from "../../../fields/KeyStore"; import { NumberField } from "../../../fields/NumberField"; +import { Document } from "../../../fields/Document"; import { Transform } from "../../util/Transform"; import { DocumentView, DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { thisExpression } from "babel-types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ListField } from "../../../fields/ListField"; +import { BooleanField } from "../../../fields/BooleanField"; +import { matchedData } from "express-validator/filter"; +export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { +} @observer -export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { +export class CollectionFreeFormDocumentView extends React.Component<CollectionFreeFormDocumentViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); + private _downX: number = 0; + private _downY: number = 0; - constructor(props: DocumentViewProps) { - super(props); + @computed get transform() { + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `; } - get screenRect(): ClientRect | DOMRect { - if (this._mainCont.current) { - return this._mainCont.current.getBoundingClientRect(); - } - return new DOMRect(); - } - - @computed - get transform(): string { - return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; - } - - @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } - @computed get width(): number { return this.props.Document.Width(); } - @computed get height(): number { return this.props.Document.Height(); } - @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - + @computed get X() { return this.props.Document.GetNumber(KeyStore.X, 0); } + @computed get Y() { return this.props.Document.GetNumber(KeyStore.Y, 0); } + @computed get zoom() { return 1 / this.props.Document.GetNumber(KeyStore.ZoomBasis, 1); } + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get width() { return this.props.Document.Width(); } + @computed get height() { return this.props.Document.Height(); } + @computed get zIndex() { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } set width(w: number) { this.props.Document.SetData(KeyStore.Width, w, NumberField); if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w); } } - set height(h: number) { this.props.Document.SetData(KeyStore.Height, h, NumberField); if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h); } } - set zIndex(h: number) { this.props.Document.SetData(KeyStore.ZIndex, h, NumberField); } - contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; - - getTransform = (): Transform => - this.props.ScreenToLocalTransform() - .translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)) - .scale(1 / this.contentScaling()) + contentScaling = () => (this.nativeWidth > 0 ? this.width / this.nativeWidth : 1); + panelWidth = () => this.props.PanelWidth(); + panelHeight = () => this.props.PanelHeight(); + toggleMinimized = () => this.toggleIcon(); + getTransform = (): Transform => this.props.ScreenToLocalTransform() + .translate(-this.X, -this.Y) + .scale(1 / this.contentScaling()).scale(1 / this.zoom) @computed get docView() { - return <DocumentView {...this.props} + return <DocumentView {...OmitKeys(this.props, ['zoomFade'])} + toggleMinimized={this.toggleMinimized} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} />; } - panelWidth = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelWidth(); - panelHeight = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelHeight(); + + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Document, maximizing: boolean) { + setTimeout(() => { + let now = Date.now(); + let progress = Math.min(1, (now - stime) / 200); + let pval = maximizing ? + [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : + [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; + target.SetNumber(KeyStore.Width, maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress); + target.SetNumber(KeyStore.Height, maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress); + target.SetNumber(KeyStore.X, pval[0]); + target.SetNumber(KeyStore.Y, pval[1]); + if (first) { + target.SetBoolean(KeyStore.IsMinimized, false); + } + if (now < stime + 200) { + this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing); + } + else { + if (!maximizing) { + target.SetBoolean(KeyStore.IsMinimized, true); + target.SetNumber(KeyStore.X, targ[0]); + target.SetNumber(KeyStore.Y, targ[1]); + target.SetNumber(KeyStore.Width, width); + target.SetNumber(KeyStore.Height, height); + } + (target as any).isIconAnimating = false; + } + }, + 2); + } + @action + public toggleIcon = async (): Promise<void> => { + SelectionManager.DeselectAll(); + let isMinimized: boolean | undefined; + let minimizedDocSet = await this.props.Document.GetTAsync(KeyStore.LinkTags, ListField); + if (!minimizedDocSet) return; + minimizedDocSet.Data.map(async minimizedDoc => { + if (minimizedDoc instanceof Document) { + this.props.addDocument && this.props.addDocument(minimizedDoc, false); + let maximizedDoc = await minimizedDoc.GetTAsync(KeyStore.MaximizedDoc, Document); + if (maximizedDoc instanceof Document && !(maximizedDoc as any).isIconAnimating) { + (maximizedDoc as any).isIconAnimating = true; + if (isMinimized === undefined) { + let maximizedDocMinimizedState = await maximizedDoc.GetTAsync(KeyStore.IsMinimized, BooleanField); + isMinimized = (maximizedDocMinimizedState && maximizedDocMinimizedState.Data) ? true : false; + } + let minx = await minimizedDoc.GetTAsync(KeyStore.X, NumberField); + let miny = await minimizedDoc.GetTAsync(KeyStore.Y, NumberField); + let maxx = await maximizedDoc.GetTAsync(KeyStore.X, NumberField); + let maxy = await maximizedDoc.GetTAsync(KeyStore.Y, NumberField); + let maxw = await maximizedDoc.GetTAsync(KeyStore.Width, NumberField); + let maxh = await maximizedDoc.GetTAsync(KeyStore.Height, NumberField); + if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && + maxw !== undefined && maxh !== undefined) + this.animateBetweenIcon( + true, + [minx.Data, miny.Data], [maxx.Data, maxy.Data], maxw.Data, maxh.Data, + Date.now(), maximizedDoc, isMinimized); + } + + } + }) + } + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + e.stopPropagation(); + } + onClick = (e: React.MouseEvent): void => { + e.stopPropagation(); + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + this.props.Document.GetTAsync(KeyStore.MaximizedDoc, Document).then(maxdoc => { + if (maxdoc instanceof Document) { // bcz: need a better way to associate behaviors with click events on widget-documents + this.props.addDocument && this.props.addDocument(maxdoc, false); + this.toggleIcon(); + } + }); + } + } + + borderRounding = () => { + let br = this.props.Document.GetNumber(KeyStore.BorderRounding, 0); + return br >= 0 ? br : + this.props.Document.GetNumber(KeyStore.NativeWidth, 0) === 0 ? + Math.min(this.props.PanelWidth(), this.props.PanelHeight()) + : + Math.min(this.props.Document.GetNumber(KeyStore.NativeWidth, 0), this.props.Document.GetNumber(KeyStore.NativeHeight, 0)); + } render() { + let maximizedDoc = this.props.Document.GetT(KeyStore.MaximizedDoc, Document); + let zoomFade = 1; + //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1); + let transform = this.getTransform().scale(this.contentScaling()).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); + let w = bptX - sptX; + //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; + const screenWidth = 1800; + let fadeUp = .75 * screenWidth; + let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; + return ( - <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ - transformOrigin: "left top", - transform: this.transform, - pointerEvents: "all", - width: this.width, - height: this.height, - position: "absolute", - zIndex: this.zIndex, - backgroundColor: "transparent" - }} > + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} + onPointerDown={this.onPointerDown} + onClick={this.onClick} + style={{ + opacity: zoomFade, + borderRadius: `${this.borderRounding()}px`, + transformOrigin: "left top", + transform: this.transform, + pointerEvents: (zoomFade < 0.09 ? "none" : "all"), + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.zIndex, + backgroundColor: "transparent" + }} > {this.docView} </div> ); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 5836da396..e9c46aa9d 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -15,6 +15,7 @@ import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; +import { IconBox } from "./IconBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; @@ -23,7 +24,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { Document } from "../../../fields/Document"; import { FieldViewProps } from "./FieldView"; -import { Without } from "../../../Utils"; +import { Without, OmitKeys } from "../../../Utils"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -44,35 +45,11 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CreateBindings(): JsxBindings { - let - { - Document, - isSelected, - select, - isTopMost, - selectOnLoad, - ScreenToLocalTransform, - addDocument, - removeDocument, - onActiveChanged, - parentActive: active, - } = this.props; - let bindings: JsxBindings = { - props: { - Document, - isSelected, - select, - isTopMost, - selectOnLoad, - ScreenToLocalTransform, - active, - onActiveChanged, - addDocument, - removeDocument, - focus, - } - }; - for (const key of this.layoutKeys) { + let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) }; + + let keys: Key[] = []; + keys.push(...this.layoutKeys, KeyStore.Caption) // bcz: hack to get templates to work + for (const key of keys) { 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) { @@ -88,7 +65,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { return <p>Error loading layout keys</p>; } return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + components={{ FormattedTextBox, ImageBox, IconBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} bindings={this.CreateBindings()} jsx={this.layout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 5126e69f9..7c72fb6e6 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,10 +1,11 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -.documentView-node { - position: absolute; +.documentView-node, .documentView-node-topmost { + position: inherit; top: 0; left:0; - background: $light-color; //overflow: hidden; + // background: $light-color; //overflow: hidden; + transform-origin: left top; &.minimized { width: 30px; @@ -12,7 +13,6 @@ } .top { - background: #232323; height: 20px; cursor: pointer; } @@ -28,16 +28,6 @@ height: calc(100% - 20px); } } - -.minimized-box { - height: 10px; - width: 10px; - border-radius: 2px; - background: $dark-color -} - -.minimized-box:hover { - background: $main-accent; - transform: scale(1.15); - cursor: pointer; +.documentView-node-topmost { + background: white; }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9670ca6b2..e8e4e8d3f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,29 +1,35 @@ -import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx"; +import { action, computed, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { Field, FieldWaiting, Opt } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { BooleanField } from "../../../fields/BooleanField"; -import { TextField } from "../../../fields/TextField"; +import { TemplateField } from "../../../fields/TemplateField"; import { ServerUtils } from "../../../server/ServerUtil"; -import { Utils } from "../../../Utils"; +import { emptyFunction, 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 { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { Template, Templates } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); - +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { MarqueeView } from "../collections/collectionFreeForm/MarqueeView"; +import { TextField } from "../../../fields/TextField"; export interface DocumentViewProps { - ContainingCollectionView: Opt<CollectionView>; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Document; addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Document) => boolean; @@ -36,7 +42,8 @@ export interface DocumentViewProps { focus: (doc: Document) => void; selectOnLoad: boolean; parentActive: () => boolean; - onActiveChanged: (isActive: boolean) => void; + whenActiveChanged: (isActive: boolean) => void; + toggleMinimized: () => void; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key }; @@ -62,14 +69,12 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { let Keys: { [name: string]: any } = {}; let Fields: { [name: string]: any } = {}; for (const key of keys) { - let fn = () => { }; - Object.defineProperty(fn, "name", { value: key + "Key" }); - Keys[key] = fn; + Object.defineProperty(emptyFunction, "name", { value: key + "Key" }); + Keys[key] = emptyFunction; } for (const field of fields) { - let fn = () => { }; - Object.defineProperty(fn, "name", { value: field }); - Fields[field] = fn; + Object.defineProperty(emptyFunction, "name", { value: field }); + Fields[field] = emptyFunction; } let args: JsxArgs = { Document: function Document() { }, @@ -82,209 +87,159 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { @observer export class DocumentView extends React.Component<DocumentViewProps> { - private _mainCont = React.createRef<HTMLDivElement>(); - public get ContentRef() { - return this._mainCont; - } private _downX: number = 0; private _downY: number = 0; + private _mainCont = React.createRef<HTMLDivElement>(); + private _dropDisposer?: DragManager.DragDropDisposer; + + public get ContentDiv() { return this._mainCont.current; } @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost(): boolean { return this.props.isTopMost; } @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - if (e.shiftKey && e.buttons === 2) { - if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); - } else { - CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); - } - e.stopPropagation(); - } else { - if (this.active && !e.isDefaultPrevented()) { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - } - } + @computed get templates(): Array<Template> { + let field = this.props.Document.GetT(KeyStore.Templates, TemplateField); + return !field || field === FieldWaiting ? [] : field.Data; } + set templates(templates: Array<Template>) { this.props.Document.SetData(KeyStore.Templates, templates, TemplateField); } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - private dropDisposer?: DragManager.DragDropDisposer; - + @action componentDidMount() { if (this._mainCont.current) { - this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); } - runInAction(() => DocumentManager.Instance.DocumentViews.push(this)); + DocumentManager.Instance.DocumentViews.push(this); } - + @action componentDidUpdate() { - if (this.dropDisposer) { - this.dropDisposer(); + if (this._dropDisposer) { + this._dropDisposer(); } if (this._mainCont.current) { - this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); } } - + @action componentWillUnmount() { - if (this.dropDisposer) { - this.dropDisposer(); + if (this._dropDisposer) { + this._dropDisposer(); } - runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1)); + DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + } + + stopPropagation = (e: React.SyntheticEvent) => { + e.stopPropagation(); } startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) { if (this._mainCont.current) { - const [left, top] = this.props - .ScreenToLocalTransform() - .inverse() - .transformPoint(0, 0); + const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData([this.props.Document]); + const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.aliasOnDrop = dropAliasOfDraggedDoc; - dragData.xOffset = x - left; - dragData.yOffset = y - top; + dragData.xOffset = xoff; + dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { - dragComplete: action(() => { }) + dragComplete: action(emptyFunction) }, hideSource: !dropAliasOfDraggedDoc }); } } - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) { + onClick = (e: React.MouseEvent): void => { + if (CurrentUserUtils.MainDocId != this.props.Document.Id && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + SelectionManager.SelectDoc(this, e.ctrlKey); + } + } + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - if ( - Math.abs(this._downX - e.clientX) > 3 || - Math.abs(this._downY - e.clientY) > 3 - ) { + if (e.shiftKey && e.buttons === 1) { + if (this.props.isTopMost) { + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); + } else { + CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); + } + e.stopPropagation(); + } else if (this.active) { document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - if (!this.topMost || e.buttons === 2 || e.altKey) { - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey); + document.addEventListener("pointerup", this.onPointerUp); + e.preventDefault(); + } + } + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey); + } } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - if (!SelectionManager.IsSelected(this) && - Math.abs(e.clientX - this._downX) < 4 && - Math.abs(e.clientY - this._downY) < 4 - ) { - SelectionManager.SelectDoc(this, e.ctrlKey); - } - } - stopPropogation = (e: React.SyntheticEvent) => { - e.stopPropagation(); } deleteClicked = (): void => { - if (this.props.removeDocument) { - this.props.removeDocument(this.props.Document); - } + this.props.removeDocument && this.props.removeDocument(this.props.Document); } - fieldsClicked = (e: React.MouseEvent): void => { - if (this.props.addDocument) { - this.props.addDocument(Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }), false); - } + let kvp = Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }); + CollectionDockingView.Instance.AddRightSplit(kvp); } fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.OpenFullScreen(this.props.Document); + CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate()); ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ - description: "Close Full Screen", - event: this.closeFullScreenClicked - }); + ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } - closeFullScreenClicked = (e: React.MouseEvent): void => { CollectionDockingView.Instance.CloseFullScreen(); ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ - description: "Full Screen", - event: this.fullScreenClicked - }); + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } - @action - public minimize = (): void => { - this.props.Document.SetData( - KeyStore.Minimized, - true as boolean, - BooleanField - ); - SelectionManager.DeselectAll(); - } - + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { 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(); + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.Document; destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest => sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc => - runInAction(() => { - linkDoc.Set(KeyStore.Title, new TextField("New Link")); - linkDoc.Set(KeyStore.LinkDescription, new TextField("")); - linkDoc.Set(KeyStore.LinkTags, new TextField("Default")); - - let dstTarg = protoDest ? protoDest : destDoc; - let srcTarg = protoSrc ? protoSrc : sourceDoc; - linkDoc.Set(KeyStore.LinkedToDocs, dstTarg); - linkDoc.Set(KeyStore.LinkedFromDocs, srcTarg); - dstTarg.GetOrCreateAsync( - KeyStore.LinkedFromDocs, - ListField, - field => { - (field as ListField<Document>).Data.push(linkDoc); - } - ); - srcTarg.GetOrCreateAsync( - KeyStore.LinkedToDocs, - ListField, - field => { - (field as ListField<Document>).Data.push(linkDoc); - } - ); - }) - ) + (protoSrc ? protoSrc : sourceDoc).CreateLink(protoDest ? protoDest : destDoc)) ); e.stopPropagation(); } } + @action onDrop = (e: React.DragEvent) => { - if (e.isDefaultPrevented()) { - return; - } let text = e.dataTransfer.getData("text/plain"); - if (text && text.startsWith("<div")) { + if (!e.isDefaultPrevented() && 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); @@ -293,141 +248,93 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } - @action - onContextMenu = (e: React.MouseEvent): void => { - e.stopPropagation(); - let moved = - Math.abs(this._downX - e.clientX) > 3 || - Math.abs(this._downY - e.clientY) > 3; - if (moved || e.isDefaultPrevented()) { - e.preventDefault(); - return; - } - e.preventDefault(); + updateLayout = async () => { + const baseLayout = await this.props.Document.GetTAsync(KeyStore.BaseLayout, TextField); + if (baseLayout) { + let base = baseLayout.Data; + let layout = baseLayout.Data; - if (!this.isMinimized()) { - ContextMenu.Instance.addItem({ - description: "Minimize", - event: this.minimize + this.templates.forEach(template => { + let temp = template.Layout; + layout = temp.replace("{layout}", base); + base = layout; }); + + this.props.Document.SetText(KeyStore.Layout, layout); } - ContextMenu.Instance.addItem({ - description: "Full Screen", - event: this.fullScreenClicked - }); - ContextMenu.Instance.addItem({ - description: "Fields", - event: this.fieldsClicked - }); - ContextMenu.Instance.addItem({ - description: "Center", - event: () => this.props.focus(this.props.Document) - }); - ContextMenu.Instance.addItem({ - description: "Open Right", - event: () => - CollectionDockingView.Instance.AddRightSplit(this.props.Document) - }); - ContextMenu.Instance.addItem({ - description: "Copy URL", - event: () => { - Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)); - } - }); - ContextMenu.Instance.addItem({ - description: "Copy ID", - event: () => { - 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) { - // DocumentViews should stop propagation of this event - e.stopPropagation(); - } + } - ContextMenu.Instance.addItem({ - description: "Delete", - event: this.deleteClicked - }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - SelectionManager.SelectDoc(this, e.ctrlKey); + @action + addTemplate = (template: Template) => { + let templates = this.templates; + templates.push(template); + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); } - isMinimized = () => { - let field = this.props.Document.GetT(KeyStore.Minimized, BooleanField); - if (field && field !== FieldWaiting) { - return field.Data; + @action + removeTemplate = (template: Template) => { + let templates = this.templates; + for (let i = 0; i < templates.length; i++) { + let temp = templates[i]; + if (temp.Name === template.Name) { + templates.splice(i, 1); + break; + } } + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); } @action - expand = () => { - this.props.Document.SetData( - KeyStore.Minimized, - false as boolean, - BooleanField - ); + onContextMenu = (e: React.MouseEvent): void => { + e.stopPropagation(); + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || + e.isDefaultPrevented()) { + e.preventDefault(); + return; + } + e.preventDefault(); + + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); + ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }); + ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); + ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) }); + 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.addItem({ description: "Delete", event: this.deleteClicked }); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + if (!SelectionManager.IsSelected(this)) + SelectionManager.SelectDoc(this, false); } isSelected = () => SelectionManager.IsSelected(this); - - select = (ctrlPressed: boolean) => { - SelectionManager.SelectDoc(this, ctrlPressed); - } + select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />); } render() { - if (!this.props.Document) { - return null; - } - var scaling = this.props.ContentScaling(); - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%"; + var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%"; - if (this.isMinimized()) { - return ( - <div - className="minimized-box" - ref={this._mainCont} - style={{ - transformOrigin: "left top", - transform: `scale(${scaling} , ${scaling})` - }} - onClick={this.expand} - onDrop={this.onDrop} - onPointerDown={this.onPointerDown} - /> - ); - } else { - var backgroundcolor = this.props.Document.GetText( - KeyStore.BackgroundColor, - "" - ); - return ( - <div - className="documentView-node" - ref={this._mainCont} - style={{ - background: backgroundcolor, - width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", - height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", - transformOrigin: "left top", - transform: `scale(${scaling} , ${scaling})` - }} - onDrop={this.onDrop} - onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} - > - <DocumentContentsView - {...this.props} - isSelected={this.isSelected} - select={this.select} - layoutKey={KeyStore.Layout} - /> - </div> - ); - } + return ( + <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} + ref={this._mainCont} + style={{ + borderRadius: "inherit", + background: this.props.Document.GetText(KeyStore.BackgroundColor, ""), + width: nativeWidth, height: nativeHeight, + transform: `scale(${scaling}, ${scaling})` + }} + onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} + > + {this.contents} + </div> + ); } } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 07c5b332c..9e175b0d1 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Field, FieldWaiting, FieldValue, Opt } from "../../../fields/Field"; import { Document } from "../../../fields/Document"; import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; @@ -19,7 +19,12 @@ import { ListField } from "../../../fields/ListField"; import { DocumentContentsView } from "./DocumentContentsView"; import { Transform } from "../../util/Transform"; import { KeyStore } from "../../../fields/KeyStore"; -import { returnFalse } from "../../../Utils"; +import { returnFalse, emptyDocFunction, emptyFunction, returnOne, returnZero } from "../../../Utils"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { IconField } from "../../../fields/IconFIeld"; +import { IconBox } from "./IconBox"; // @@ -29,6 +34,7 @@ import { returnFalse } from "../../../Utils"; // export interface FieldViewProps { fieldKey: Key; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Document; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; @@ -39,8 +45,10 @@ export interface FieldViewProps { moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; ScreenToLocalTransform: () => Transform; active: () => boolean; - onActiveChanged: (isActive: boolean) => void; + whenActiveChanged: (isActive: boolean) => void; focus: (doc: Document) => void; + PanelWidth: () => number; + PanelHeight: () => number; } @observer @@ -68,6 +76,9 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} />; } + else if (field instanceof IconField) { + return <IconBox {...this.props} />; + } else if (field instanceof VideoField) { return <VideoBox {...this.props} />; } @@ -85,13 +96,14 @@ export class FieldView extends React.Component<FieldViewProps> { PanelHeight={() => 100} isTopMost={true} //TODO Why is this top most? selectOnLoad={false} - focus={() => { }} - isSelected={() => false} - select={() => false} + focus={emptyDocFunction} + isSelected={this.props.isSelected} + select={returnFalse} layoutKey={KeyStore.Layout} - ContainingCollectionView={undefined} + ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} - onActiveChanged={this.props.onActiveChanged} /> + toggleMinimized={emptyFunction} + whenActiveChanged={this.props.whenActiveChanged} /> ); } else if (field instanceof ListField) { @@ -111,7 +123,7 @@ export class FieldView extends React.Component<FieldViewProps> { } else { return <p> {"Waiting for server..."} </p>; - } + } } }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 32da2632e..727d3c0b2 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import "../global_variables"; +@import "../globalCssVariables"; .ProseMirror { width: 100%; height: auto; @@ -10,11 +10,11 @@ outline: none !important; } -.formattedTextBox-cont { +.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { background: $light-color-secondary; - padding: 0.9em; + padding: 0; border-width: 0px; - border-radius: $border-radius; + border-radius: inherit; border-color: $intermediate-color; box-sizing: border-box; border-style: solid; @@ -22,6 +22,20 @@ overflow-x: hidden; color: initial; height: 100%; + pointer-events: all; +} + +.formattedTextBox-cont-hidden { + overflow: hidden; + pointer-events: none; +} +.formattedTextBox-inner-rounded { + height: calc(100% - 40px); + width: calc(100% - 40px); + position: absolute; + overflow: scroll; + top: 20; + left: 20; } .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index beca6cdc6..41ee24498 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,22 +1,27 @@ -import { action, IReactionDisposer, reaction } from "mobx"; +import { action, IReactionDisposer, reaction, trace, computed, _allowStateChangesInsideComputed } from "mobx"; import { baseKeymap } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; +import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { FieldWaiting, Opt } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { RichTextField } from "../../../fields/RichTextField"; +import { TextField } from "../../../fields/TextField"; +import { Document } from "../../../fields/Document"; +import buildKeymap from "../../util/ProsemirrorKeymap"; import { inpRules } from "../../util/RichTextRules"; import { schema } from "../../util/RichTextSchema"; +import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { ContextMenu } from "../../views/ContextMenu"; -import { Main } from "../Main"; +import { MainOverlayTextBox } from "../MainOverlayTextBox"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); -const { buildMenuItems } = require("prosemirror-example-setup"); -const { menuBar } = require("prosemirror-menu"); +import { SelectionManager } from "../../util/SelectionManager"; +import { observer } from "mobx-react"; +import { InkingControl } from "../InkingControl"; // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // @@ -34,90 +39,108 @@ const { menuBar } = require("prosemirror-menu"); // 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> { + +export interface FormattedTextBoxOverlay { + isOverlay?: boolean; +} + +@observer +export class FormattedTextBox extends React.Component<(FieldViewProps & FormattedTextBoxOverlay)> { public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } private _ref: React.RefObject<HTMLDivElement>; + private _proseRef: React.RefObject<HTMLDivElement>; private _editorView: Opt<EditorView>; + private _gotDown: boolean = false; private _reactionDisposer: Opt<IReactionDisposer>; private _inputReactionDisposer: Opt<IReactionDisposer>; + private _proxyReactionDisposer: Opt<IReactionDisposer>; constructor(props: FieldViewProps) { super(props); this._ref = React.createRef(); + this._proseRef = React.createRef(); this.onChange = this.onChange.bind(this); } + _applyingChange: boolean = false; + + _lastState: any = undefined; dispatchTransaction = (tx: Transaction) => { if (this._editorView) { - const state = this._editorView.state.apply(tx); + const state = this._lastState = this._editorView.state.apply(tx); this._editorView.updateState(state); - this.FieldDoc.SetDataOnPrototype( - this.FieldKey, + this._applyingChange = true; + this.props.Document.SetDataOnPrototype( + this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField ); - // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); + this.props.Document.SetDataOnPrototype(KeyStore.DocumentText, state.doc.textBetween(0, state.doc.content.size, "\n\n"), TextField); + this._applyingChange = false; + if (this.props.Document.Title.startsWith("-") && this._editorView) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + this.props.Document.SetText(KeyStore.Title, "-" + titlestr + (str.length > 40 ? "..." : "")); + }; } } - get FieldDoc() { return this.props.fieldKey === KeyStore.Archives ? Main.Instance._textDoc! : this.props.Document; } - get FieldKey() { return this.props.fieldKey === KeyStore.Archives ? KeyStore.Data : this.props.fieldKey; } - componentDidMount() { const config = { schema, inpRules, //these currently don't do anything, but could eventually be helpful - plugins: [ + plugins: this.props.isOverlay ? [ + this.tooltipTextMenuPlugin(), history(), - keymap({ "Mod-z": undo, "Mod-y": redo }), + keymap(buildKeymap(schema)), keymap(baseKeymap), - this.tooltipMenuPlugin() - ] + // this.tooltipLinkingMenuPlugin(), + new Plugin({ + props: { + attributes: { class: "ProseMirror-example-setup-style" } + } + }) + ] : [ + history(), + keymap(buildKeymap(schema)), + keymap(baseKeymap), + ] }; - if (this.props.fieldKey === KeyStore.Archives) { - this._inputReactionDisposer = reaction(() => Main.Instance._textDoc && Main.Instance._textDoc.Id, + if (this.props.isOverlay) { + this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc.Id, () => { if (this._editorView) { this._editorView.destroy(); } - - this.setupEditor(config); + this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox } ); + } else { + this._proxyReactionDisposer = reaction(() => this.props.isSelected(), + () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); } + this._reactionDisposer = reaction( () => { - const field = this.FieldDoc.GetT(this.FieldKey, RichTextField); + const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined; return field && field !== FieldWaiting ? field.Data : undefined; }, - field => { - if (field && this._editorView) { - this._editorView.updateState( - EditorState.fromJSON(config, JSON.parse(field)) - ); - } - } + field => field && this._editorView && !this._applyingChange && + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); - this.setupEditor(config); + this.setupEditor(config, this.props.Document); } - private setupEditor(config: any) { - - let state: EditorState; - let field = this.FieldDoc.GetT(this.FieldKey, RichTextField); - if (field && field !== FieldWaiting && field.Data) { - state = EditorState.fromJSON(config, JSON.parse(field.Data)); - } else { - state = EditorState.create(config); - } + private setupEditor(config: any, doc?: Document) { + let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined; if (this._ref.current) { this._editorView = new EditorView(this._ref.current, { - state, + state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction }); } @@ -138,10 +161,9 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { if (this._inputReactionDisposer) { this._inputReactionDisposer(); } - } - - shouldComponentUpdate() { - return false; + if (this._proxyReactionDisposer) { + this._proxyReactionDisposer(); + } } @action @@ -150,24 +172,33 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { Document.SetOnPrototype(fieldKey, new RichTextField(e.target.value)); // doc.SetData(fieldKey, e.target.value, RichTextField); } + @action onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { + if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) + this._toolTipTextMenu.tooltip.style.opacity = "0"; + } + if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { + this._gotDown = true; + e.preventDefault(); } } onPointerUp = (e: React.PointerEvent): void => { + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) + this._toolTipTextMenu.tooltip.style.opacity = "1"; if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } - if (this.props.fieldKey !== KeyStore.Archives) { - e.preventDefault(); - Main.Instance.SetTextDoc(this.props.Document, this._ref.current!); - } } onFocused = (e: React.FocusEvent): void => { - if (this.props.fieldKey !== KeyStore.Archives) { - Main.Instance.SetTextDoc(this.props.Document, this._ref.current!); + if (!this.props.isOverlay) { + MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform); + } else { + if (this._ref.current) { + this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; + } } } @@ -175,6 +206,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { textCapability = (e: React.MouseEvent): void => { }; specificContextMenu = (e: React.MouseEvent): void => { + if (!this._gotDown) { + e.preventDefault(); + return; + } ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability @@ -195,35 +230,69 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { } onPointerWheel = (e: React.WheelEvent): void => { - e.stopPropagation(); + if (this.props.isSelected()) { + e.stopPropagation(); + } + } + + onClick = (e: React.MouseEvent): void => { + this._proseRef.current!.focus(); + } + + tooltipTextMenuPlugin() { + let myprops = this.props; + let self = this; + return new Plugin({ + view(_editorView) { + return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); + } + }); } - tooltipMenuPlugin() { + _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + tooltipLinkingMenuPlugin() { + let myprops = this.props; return new Plugin({ view(_editorView) { - return new TooltipTextMenu(_editorView); + return new TooltipLinkingMenu(_editorView, myprops); } }); } - onKeyPress(e: React.KeyboardEvent) { + + @action + onKeyPress = (e: React.KeyboardEvent) => { + if (e.key == "Escape") { + SelectionManager.DeselectAll(); + } e.stopPropagation(); + if (e.key === "Tab") e.preventDefault(); // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. - // (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; + (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; } render() { + let style = this.props.isOverlay ? "-scroll" : "-hidden"; + let rounded = this.props.Document.GetNumber(KeyStore.BorderRounding, 0) < 0 ? "-rounded" : ""; + let color = this.props.Document.GetText(KeyStore.BackgroundColor, ""); + let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; return ( - <div - className="formattedTextBox-cont" + <div className={`formattedTextBox-cont${style}`} ref={this._ref} + style={{ + pointerEvents: interactive ? "all" : "none", + background: color, + }} onKeyDown={this.onKeyPress} onKeyPress={this.onKeyPress} + onFocus={this.onFocused} + onClick={this.onClick} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} // tfs: do we need this event handler onWheel={this.onPointerWheel} - ref={this._ref} - /> + > + <div className={`formattedTextBox-inner${rounded}`} ref={this._proseRef} /> + </div> ); } } diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss new file mode 100644 index 000000000..ce0ee2e09 --- /dev/null +++ b/src/client/views/nodes/IconBox.scss @@ -0,0 +1,12 @@ + +@import "../globalCssVariables"; +.iconBox-container { + position: absolute; + left:0; + top:0; + svg { + width: 100% !important; + height: 100%; + background: white; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx new file mode 100644 index 000000000..9c90c0a0e --- /dev/null +++ b/src/client/views/nodes/IconBox.tsx @@ -0,0 +1,45 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from '../../../fields/Document'; +import { IconField } from "../../../fields/IconFIeld"; +import { KeyStore } from "../../../fields/KeyStore"; +import { SelectionManager } from "../../util/SelectionManager"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./IconBox.scss"; + + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +@observer +export class IconBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(IconBox); } + + @computed get maximized() { return this.props.Document.GetT(KeyStore.MaximizedDoc, Document); } + @computed get layout(): string { return this.props.Document.GetData(this.props.fieldKey, IconField, "<p>Error loading layout data</p>" as string); } + @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } + + public static DocumentIcon(layout: string) { + let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : + layout.indexOf("ImageBox") !== -1 ? faImage : + layout.indexOf("Formatted") !== -1 ? faStickyNote : + layout.indexOf("Video") !== -1 ? faFilm : + layout.indexOf("Collection") !== -1 ? faObjectGroup : + faCaretUp; + return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" /> + } + + render() { + return ( + <div className="iconBox-container"> + {this.minimizedIcon} + </div>); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 487038841..9fe211df0 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -6,6 +6,20 @@ height: auto; max-width: 100%; max-height: 100%; + pointer-events: none; +} +.imageBox-cont-interactive { + pointer-events: all; +} + +.imageBox-dot { + position:absolute; + bottom: 10; + left: 0; + border-radius: 10px; + width:20px; + height:20px; + background:gray; } .imageBox-cont img { @@ -18,4 +32,4 @@ border: none; width: 100%; height: 100%; -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6b0a3a799..2cbb0fa90 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,57 +1,95 @@ -import { action, observable, trace } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Document } from '../../../fields/Document'; import { FieldWaiting } from '../../../fields/Field'; import { ImageField } from '../../../fields/ImageField'; import { KeyStore } from '../../../fields/KeyStore'; +import { ListField } from '../../../fields/ListField'; +import { Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { Utils } from '../../../Utils'; +import { InkingControl } from '../InkingControl'; +import { NumberField } from '../../../fields/NumberField'; @observer export class ImageBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(ImageBox); } - private _ref: React.RefObject<HTMLDivElement>; private _imgRef: React.RefObject<HTMLImageElement>; private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @observable private _photoIndex: number = 0; @observable private _isOpen: boolean = false; + private dropDisposer?: DragManager.DragDropDisposer; constructor(props: FieldViewProps) { super(props); - this._ref = React.createRef(); this._imgRef = React.createRef(); - this.state = { - photoIndex: 0, - isOpen: false, - }; } @action onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; - this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); + if (this._photoIndex === 0) { + this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); + this.props.Document.GetTAsync(KeyStore.Width, NumberField, field => + field && this.props.Document.SetNumber(KeyStore.Height, field.Data * h / w)); + } } - componentDidMount() { + + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + onDrop = (e: React.DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + console.log("IMPLEMENT ME PLEASE"); } - componentWillUnmount() { + + @undoBatch + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + de.data.droppedDocuments.map(action((drop: Document) => { + let layout = drop.GetText(KeyStore.BackgroundLayout, ""); + if (layout.indexOf(ImageBox.name) !== -1) { + let imgData = this.props.Document.Get(KeyStore.Data); + if (imgData instanceof ImageField && imgData) { + this.props.Document.SetOnPrototype(KeyStore.Data, new ListField([imgData])); + } + let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]); + if (imgList) { + let field = drop.Get(KeyStore.Data); + if (field === FieldWaiting) { } + else if (field instanceof ImageField) imgList.push(field); + else if (field instanceof ListField) imgList.push(field.Data); + } + e.stopPropagation(); + } + })); + // de.data.removeDocument() bcz: need to implement + } } onPointerDown = (e: React.PointerEvent): void => { if (Date.now() - this._lastTap < 300) { if (e.buttons === 1) { - e.stopPropagation(); this._downX = e.clientX; this._downY = e.clientY; document.removeEventListener("pointerup", this.onPointerUp); @@ -70,8 +108,7 @@ export class ImageBox extends React.Component<FieldViewProps> { e.stopPropagation(); } - lightbox = (path: string) => { - const images = [path]; + lightbox = (images: string[]) => { if (this._isOpen) { return (<Lightbox mainSrc={images[this._photoIndex]} @@ -102,15 +139,36 @@ export class ImageBox extends React.Component<FieldViewProps> { } } + @action + onDotDown(index: number) { + this._photoIndex = index; + this.props.Document.SetNumber(KeyStore.CurPage, index); + } + + dots(paths: string[]) { + let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + let dist = Math.min(nativeWidth / paths.length, 40); + let left = (nativeWidth - paths.length * dist) / 2; + return paths.map((p, i) => + <div className="imageBox-placer" key={i} > + <div className="imageBox-dot" style={{ background: (i == this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> + </div> + ); + } + render() { let field = this.props.Document.Get(this.props.fieldKey); - let path = field === FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; + let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; + if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"]; + else if (field instanceof ImageField) paths = [field.Data.href]; + else if (field instanceof ListField) paths = field.Data.filter(val => val as ImageField).map(p => (p as ImageField).Data.href); let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; return ( - <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}> - <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> - {this.lightbox(path)} + <div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> + {paths.length > 1 ? this.dots(paths) : (null)} + {this.lightbox(paths)} </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 63ae75424..20cae03d4 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,38 +1,67 @@ -@import "../global_variables"; +@import "../globalCssVariables"; .keyValueBox-cont { overflow-y: scroll; + width:100%; height: 100%; background-color: $light-color; border: 1px solid $intermediate-color; border-radius: $border-radius; box-sizing: border-box; display: inline-block; + pointer-events: all; .imageBox-cont img { - 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; - } + width: auto; } } +$header-height: 30px; +.keyValueBox-tbody { + width:100%; + height:100%; + position: absolute; + overflow-y: scroll; +} +.keyValueBox-key { + display: inline-block; + height:100%; + width:50%; + text-align: center; +} +.keyValueBox-fields { + display: inline-block; + height:100%; + width:50%; + text-align: center; +} .keyValueBox-table { - position: relative; + position: absolute; + width:100%; + height:100%; border-collapse: collapse; } - +.keyValueBox-td-key { + display:inline-block; + height:30px; +} +.keyValueBox-td-value { + display:inline-block; + height:30px; +} +.keyValueBox-valueRow { + width:100%; + height:30px; + display: inline-block; +} .keyValueBox-header { + width:100%; + position: relative; + display: inline-block; background: $intermediate-color; color: $light-color; text-transform: uppercase; letter-spacing: 2px; font-size: 12px; - height: 30px; + height: $header-height; padding-top: 4px; th { font-weight: normal; @@ -43,13 +72,50 @@ } .keyValueBox-evenRow { + position: relative; + display: inline-block; + width:100%; + height:$header-height; background: $light-color; .formattedTextBox-cont { background: $light-color; } } +.keyValueBox-cont { + .collectionfreeformview-overlay { + position: relative; + } +} +.keyValueBox-dividerDraggerThumb{ + position: relative; + width: 4px; + float: left; + height: 30px; + width: 10px; + z-index: 20; + right: 0; + top: 0; + border-radius: 10px; + background: gray; + pointer-events: all; +} +.keyValueBox-dividerDragger{ + position: relative; + width: 100%; + float: left; + height: 37px; + z-index: 20; + right: 0; + top: 0; + background: transparent; + pointer-events: none; +} .keyValueBox-oddRow { + position: relative; + display: inline-block; + width:100%; + height:30px; background: $light-color-secondary; .formattedTextBox-cont { background: $light-color-secondary; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index bcac113f0..ddbec014b 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,35 +1,31 @@ +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Document } from '../../../fields/Document'; -import { FieldWaiting, Field } from '../../../fields/Field'; +import { Field, FieldWaiting } from '../../../fields/Field'; +import { Key } from '../../../fields/Key'; import { KeyStore } from '../../../fields/KeyStore'; +import { CompileScript, ToField } from "../../util/Scripting"; 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> { + private _mainCont = React.createRef<HTMLDivElement>(); public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr); } @observable private _keyInput: string = ""; @observable private _valueInput: string = ""; + @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 50); } constructor(props: FieldViewProps) { super(props); } - - - shouldComponentUpdate() { - return false; - } - @action onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { @@ -90,7 +86,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let rows: JSX.Element[] = []; let i = 0; for (let key in ids) { - rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />); + rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />); } return rows; } @@ -107,24 +103,51 @@ export class KeyValueBox extends React.Component<FieldViewProps> { newKeyValue = () => ( - <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 className="keyValueBox-valueRow"> + <td className="keyValueBox-td-key" style={{ width: `${100 - this.splitPercentage}%` }}> + <input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /> + </td> + <td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}> + <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /> + </td> </tr> ) + @action + onDividerMove = (e: PointerEvent): void => { + let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 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); + } + onDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } + render() { - return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> + let dividerDragger = this.splitPercentage === 0 ? (null) : + <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> + <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> + </div>; + + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} ref={this._mainCont}> <table className="keyValueBox-table"> - <tbody> + <tbody className="keyValueBox-tbody"> <tr className="keyValueBox-header"> - <th>Key</th> - <th>Fields</th> + <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th> + <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> </tr> {this.createTable()} {this.newKeyValue()} </tbody> </table> + {dividerDragger} </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 64e871e1c..ff6885965 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,12 +1,29 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -.container{ - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; -} -.delete{ - color: red; +.keyValuePair-td-key { + display:inline-block; + .keyValuePair-td-key-container{ + width:100%; + height:100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + .keyValuePair-td-key-delete{ + position: relative; + background-color: transparent; + color:red; + } + .keyValuePair-keyField { + width:100%; + text-align: center; + position: relative; + overflow: auto; + } + } +} +.keyValuePair-td-value { + display:inline-block; + overflow: scroll; }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index a1050dc6e..d480eb5af 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,18 +1,18 @@ -import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import "./KeyValueBox.scss"; -import "./KeyValuePair.scss"; -import React = require("react"); -import { FieldViewProps, FieldView } from './FieldView'; -import { Opt, Field } from '../../../fields/Field'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { observable, action } from 'mobx'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Document } from '../../../fields/Document'; +import { Field, Opt } from '../../../fields/Field'; import { Key } from '../../../fields/Key'; +import { emptyDocFunction, emptyFunction, returnFalse } from '../../../Utils'; import { Server } from "../../Server"; -import { EditableView } from "../EditableView"; import { CompileScript, ToField } from "../../util/Scripting"; import { Transform } from '../../util/Transform'; -import { returnFalse, emptyFunction } from '../../../Utils'; +import { EditableView } from "../EditableView"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./KeyValueBox.scss"; +import "./KeyValuePair.scss"; +import React = require("react"); // Represents one row in a key value plane @@ -20,86 +20,81 @@ export interface KeyValuePairProps { rowStyle: string; fieldId: string; doc: Document; + keyWidth: number; } @observer export class KeyValuePair extends React.Component<KeyValuePairProps> { - @observable - private key: Opt<Key>; + @observable private key: Opt<Key>; constructor(props: KeyValuePairProps) { super(props); Server.GetField(this.props.fieldId, - action((field: Opt<Field>) => { - if (field) { - this.key = field as Key; - } - })); + action((field: Opt<Field>) => field instanceof Key && (this.key = field))); } render() { if (!this.key) { - return <tr><td>error</td><td></td></tr>; - + return <tr><td>error</td><td /></tr>; } let props: FieldViewProps = { Document: this.props.doc, + ContainingCollectionView: undefined, fieldKey: this.key, isSelected: returnFalse, select: emptyFunction, isTopMost: false, selectOnLoad: false, active: returnFalse, - onActiveChanged: emptyFunction, + whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, + focus: emptyDocFunction, }; - let contents = ( - <FieldView {...props} /> - ); + let contents = <FieldView {...props} />; return ( <tr className={this.props.rowStyle}> - {/* <button>X</button> */} - <td> - <div className="container"> - <div>{this.key.Name}</div> - <button className="delete" onClick={() => { + <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> + <div className="keyValuePair-td-key-container"> + <button className="keyValuePair-td-key-delete" onClick={() => { let field = props.Document.Get(props.fieldKey); - if (field && field instanceof Field) { - props.Document.Set(props.fieldKey, undefined); - } - }}>X</button> + field && field instanceof Field && props.Document.Set(props.fieldKey, undefined); + }}> + X + </button> + <div className="keyValuePair-keyField">{this.key.Name}</div> </div> </td> - <td><EditableView contents={contents} height={36} GetValue={() => { - let field = props.Document.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); - } - return field || ""; - }} - SetValue={(value: string) => { - let script = CompileScript(value, { addReturn: true }); - if (!script.compiled) { - return false; + <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> + <EditableView contents={contents} height={36} GetValue={() => { + let field = props.Document.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); } - let res = script.run(); - if (!res.success) return false; - const field = res.result; - if (field instanceof Field) { - props.Document.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.Document.Set(props.fieldKey, dataField); + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value, { addReturn: true }); + if (!script.compiled) { + return false; + } + let res = script.run(); + if (!res.success) return false; + const field = res.result; + if (field instanceof Field) { + props.Document.Set(props.fieldKey, field); return true; + } else { + let dataField = ToField(field); + if (dataField) { + props.Document.Set(props.fieldKey, dataField); + return true; + } } - } - return false; - }}></EditableView></td> + return false; + }}> + </EditableView></td> </tr> ); } diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index 809b0bf5f..639f83b38 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -1,5 +1,4 @@ -@import "../global_variables"; - +@import "../globalCssVariables"; .link-container { width: 100%; height: 50px; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 396f5f577..89c06e7b3 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,24 +1,16 @@ -import { observable, computed, action } from "mobx"; -import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { observer } from "mobx-react"; -import './LinkBox.scss'; -import { KeyStore } from '../../../fields/KeyStore'; -import { props } from "bluebird"; -import { DocumentView } from "./DocumentView"; import { Document } from "../../../fields/Document"; +import { KeyStore } from '../../../fields/KeyStore'; import { ListField } from "../../../fields/ListField"; +import { NumberField } from "../../../fields/NumberField"; 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"; -import { FieldWaiting } from "../../../fields/Field"; -import { NumberField } from "../../../fields/NumberField"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import './LinkBox.scss'; +import React = require("react"); library.add(faEye); diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss index f01213542..9629585d7 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,5 +1,4 @@ -@import "../global_variables"; - +@import "../globalCssVariables"; .edit-container { width: 100%; height: auto; diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 830dfe6c6..3760e378a 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -4,6 +4,9 @@ top: 0; left:0; } +.react-pdf__Page__textContent span { + user-select: text; +} .react-pdf__Document { position: absolute; } @@ -12,6 +15,21 @@ top: 0; left:0; z-index: 25; + pointer-events: all; +} +.pdfButton { + pointer-events: all; + width: 100px; + height:100px; +} +.pdfBox-cont { + pointer-events: none ; + span { + pointer-events: none !important; + } +} +.pdfBox-cont-interactive { + pointer-events: all; } .pdfBox-contentContainer { position: absolute; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 81ceb37f6..226dfba11 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, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; @@ -14,11 +14,10 @@ import { RouteStore } from "../../../server/RouteStore"; import { Utils } from '../../../Utils'; import { Annotation } from './Annotation'; import { FieldView, FieldViewProps } from './FieldView'; -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 { SelectionManager } from "../../util/SelectionManager"; +import { InkingControl } from "../InkingControl"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -40,15 +39,6 @@ import { SelectionManager } from "../../util/SelectionManager"; * 4) another method: click on highlight first and then drag on your desired text * 5) To make another highlight, you need to reclick on the button * - * Draw: - * 1) click draw and select color. then just draw like there's no tomorrow. - * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. - * - * Pagination: - * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. - * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. - * - * * written by: Andrew Kim */ @observer @@ -56,30 +46,9 @@ export class PDFBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(PDFBox); } private _mainDiv = React.createRef<HTMLDivElement>(); - private _pdf = React.createRef<HTMLCanvasElement>(); @observable private _renderAsSvg = true; - //very useful for keeping track of X and y position throughout the PDF Canvas - private initX: number = 0; - private initY: number = 0; - - //checks if tool is on - private _toolOn: boolean = false; //checks if tool is on - private _pdfContext: any = null; //gets pdf context - private bool: Boolean = false; //general boolean debounce - private currSpan: any;//keeps track of current span (for highlighting) - - private _currTool: any; //keeps track of current tool button reference - private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool - private _drawTool = React.createRef<HTMLButtonElement>();//drawing tool button reference - - private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference - private _currColor: string = "black"; //current color that user selected (for ink/pen) - - private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference - private _highlightToolOn: boolean = false; - private _pdfCanvas: any; private _reactionDisposer: Opt<IReactionDisposer>; @observable private _perPageInfo: Object[] = []; //stores pageInfo @@ -112,43 +81,6 @@ export class PDFBox extends React.Component<FieldViewProps> { } /** - * selection tool used for area highlighting (stickies). Kinda temporary - */ - selectionTool = () => { - this._toolOn = true; - } - /** - * when user draws on the canvas. When mouse pointer is down - */ - drawDown = (e: PointerEvent) => { - this.initX = e.offsetX; - this.initY = e.offsetY; - this._pdfContext.beginPath(); - this._pdfContext.lineTo(this.initX, this.initY); - this._pdfContext.strokeStyle = this._currColor; - this._pdfCanvas.addEventListener("pointermove", this.drawMove); - this._pdfCanvas.addEventListener("pointerup", this.drawUp); - - } - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = this.initX += e.movementX, - y = this.initY += e.movementY; - //connects the point - this._pdfContext.lineTo(x, y); - this._pdfContext.stroke(); - } - - drawUp = (e: PointerEvent) => { - this._pdfContext.closePath(); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - } - - - /** * highlighting helper function */ makeEditableAndHighlight = (colour: string) => { @@ -183,7 +115,7 @@ export class PDFBox extends React.Component<FieldViewProps> { child.id = "highlighted"; //@ts-ignore obj.spans.push(child); - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } }); } @@ -206,7 +138,7 @@ export class PDFBox extends React.Component<FieldViewProps> { child.id = "highlighted"; //@ts-ignore temp.spans.push(child); - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } }); @@ -272,11 +204,20 @@ export class PDFBox extends React.Component<FieldViewProps> { * controls the area highlighting (stickies) Kinda temporary */ onPointerDown = (e: React.PointerEvent) => { - if (this._toolOn) { - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - + if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) { + if (e.altKey) { + this._alt = true; + } else { + if (e.metaKey) + e.stopPropagation(); + } + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + if (this.props.isSelected() && e.buttons === 2) { + this._alt = true; + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); } } @@ -284,96 +225,15 @@ export class PDFBox extends React.Component<FieldViewProps> { * controls area highlighting and partially highlighting. Kinda temporary */ @action - onPointerUp = (e: React.PointerEvent) => { - if (this._highlightToolOn) { + onPointerUp = (e: PointerEvent) => { + this._alt = false; + document.removeEventListener("pointerup", this.onPointerUp); + if (this.props.isSelected()) { this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. - this._highlightToolOn = false; - } - if (this._toolOn) { - let mouse = e.nativeEvent; - let finalX = mouse.offsetX; - let finalY = mouse.offsetY; - let width = Math.abs(finalX - this.initX); //width - let height = Math.abs(finalY - this.initY); //height - - //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky - if (finalX < this.initX) { - this.initX = finalX; - } - if (finalY < this.initY) { - this.initY = finalY; - } - - if (this._mainDiv.current) { - let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} />; - this._pageInfo.area.push(sticky); - } - this._toolOn = false; } this._interactive = true; } - /** - * starts drawing the line when user presses down. - */ - onDraw = () => { - if (this._currTool !== null) { - this._currTool.style.backgroundColor = "grey"; - } - - if (this._drawTool.current) { - this._currTool = this._drawTool.current; - if (this._drawToolOn) { - this._drawToolOn = false; - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.removeEventListener("pointerup", this.drawUp); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._drawTool.current.style.backgroundColor = "grey"; - } else { - this._drawToolOn = true; - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - this._drawTool.current.style.backgroundColor = "cyan"; - } - } - } - - - /** - * for changing color (for ink/pen) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML === "Red") { - this._currColor = "red"; - } else if (e.currentTarget.innerHTML === "Blue") { - this._currColor = "blue"; - } else if (e.currentTarget.innerHTML === "Green") { - this._currColor = "green"; - } else if (e.currentTarget.innerHTML === "Black") { - this._currColor = "black"; - } - - } - - - /** - * For highlighting (text drag highlighting) - */ - onHighlight = () => { - this._drawToolOn = false; - if (this._currTool !== null) { - this._currTool.style.backgroundColor = "grey"; - } - if (this._highlightTool.current) { - this._currTool = this._drawTool.current; - if (this._highlightToolOn) { - this._highlightToolOn = false; - this._highlightTool.current.style.backgroundColor = "grey"; - } else { - this._highlightToolOn = true; - this._highlightTool.current.style.backgroundColor = "orange"; - } - } - } @action @@ -397,22 +257,6 @@ export class PDFBox extends React.Component<FieldViewProps> { @action onLoaded = (page: any) => { - if (this._mainDiv.current) { - this._mainDiv.current.childNodes.forEach((element) => { - if (element.nodeName === "DIV") { - element.childNodes[0].childNodes.forEach((e) => { - - if (e instanceof HTMLCanvasElement) { - this._pdfCanvas = e; - this._pdfContext = e.getContext("2d"); - - } - - }); - } - }); - } - // bcz: the number of pages should really be set when the document is imported. this.props.Document.SetNumber(KeyStore.NumPages, page._transport.numPages); if (this._perPageInfo.length === 0) { //Makes sure it only runs once @@ -424,7 +268,7 @@ export class PDFBox extends React.Component<FieldViewProps> { @action setScaling = (r: any) => { // 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 + // also, the native dimensions could be different for different pages of the canvas // so this design is flawed. var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); if (!this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) { @@ -433,22 +277,27 @@ export class PDFBox extends React.Component<FieldViewProps> { this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight); } } - + renderHeight = 2400; + @computed + get pdfPage() { + return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} /> + } @computed get pdfContent() { - let page = this.curPage; - const renderHeight = 2400; let pdfUrl = this.props.Document.GetT(this.props.fieldKey, PDFField); - let xf = this.props.Document.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; + let xf = this.props.Document.GetNumber(KeyStore.NativeHeight, 0) / this.renderHeight; + let body = (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ? + this.pdfPage : + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + {this.pdfPage} + </div> + } + </Measure>; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : ""}> - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="pdfBox-page" ref={measureRef}> - <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> - </div> - } - </Measure> + <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}> + {body} </Document> </div >; } @@ -478,10 +327,24 @@ export class PDFBox extends React.Component<FieldViewProps> { } return (null); } - + @observable _alt = false; + @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Alt") { + this._alt = true; + } + } + @action + onKeyUp = (e: React.KeyboardEvent) => { + if (e.key === "Alt") { + this._alt = false; + } + } render() { + trace(); + let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( - <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} > {this.pdfRenderer} </div > ); diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx deleted file mode 100644 index 11719831b..000000000 --- a/src/client/views/nodes/Sticky.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import "react-image-lightbox/style.css"; // This only needs to be imported once in your app -import React = require("react"); -import { observer } from "mobx-react"; -import "react-pdf/dist/Page/AnnotationLayer.css"; - -interface IProps { - Height: number; - Width: number; - X: number; - Y: number; -} - -/** - * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. - * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. - * - * Written By: Andrew Kim - */ -@observer -export class Sticky extends React.Component<IProps> { - private initX: number = 0; - private initY: number = 0; - - private _ref = React.createRef<HTMLCanvasElement>(); - private ctx: any; //context that keeps track of sticky canvas - - /** - * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas - */ - drawDown = (e: React.PointerEvent) => { - if (this._ref.current) { - this.ctx = this._ref.current.getContext("2d"); - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - this.ctx.beginPath(); - this.ctx.lineTo(this.initX, this.initY); - this.ctx.strokeStyle = "black"; - document.addEventListener("pointermove", this.drawMove); - document.addEventListener("pointerup", this.drawUp); - } - } - - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = (this.initX += e.movementX), - y = (this.initY += e.movementY); - //connects the point - this.ctx.lineTo(x, y); - this.ctx.stroke(); - } - - /** - * when user lifts the mouse, the drawing ends - */ - drawUp = (e: PointerEvent) => { - this.ctx.closePath(); - console.log(this.ctx); - document.removeEventListener("pointermove", this.drawMove); - } - - render() { - return ( - <div onPointerDown={this.drawDown}> - <canvas - ref={this._ref} - height={this.props.Height} - width={this.props.Width} - style={{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - background: "yellow", - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - opacity: 0.4 - }} - /> - </div> - ); - } -} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 314af64c9..3bb930ba7 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,18 +1,16 @@ import React = require("react"); +import { action, computed, IReactionDisposer, trace } from "mobx"; import { observer } from "mobx-react"; +import Measure from "react-measure"; import { FieldWaiting, Opt } from '../../../fields/Field'; +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, IReactionDisposer, computed, reaction } from "mobx"; -import { KeyStore } from "../../../fields/KeyStore"; -import { number } from "prop-types"; @observer export class VideoBox extends React.Component<FieldViewProps> { - private _reactionDisposer: Opt<IReactionDisposer>; private _videoRef = React.createRef<HTMLVideoElement>(); public static LayoutString() { return FieldView.LayoutString(VideoBox); } @@ -54,25 +52,26 @@ export class VideoBox extends React.Component<FieldViewProps> { (vref as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage; } } + videoContent(path: string) { + return <video className="videobox-cont" ref={this.setVideoRef}> + <source src={path} type="video/mp4" /> + Not supported. + </video>; + } render() { let field = this.props.Document.GetT(this.props.fieldKey, VideoField); if (!field || field === FieldWaiting) { return <div>Loading</div>; } - let path = field.Data.href; - trace(); - return ( + return (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ? + this.videoContent(field.Data.href) : <Measure onResize={this.setScaling}> {({ measureRef }) => <div style={{ width: "100%", height: "auto" }} ref={measureRef}> - <video className="videobox-cont" onClick={() => { }} ref={this.setVideoRef}> - <source src={path} type="video/mp4" /> - Not supported. - </video> + {this.videoContent(field!.Data.href)} </div> } - </Measure> - ); + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index c73bc0c47..eb09b0693 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,12 +1,31 @@ -.webBox-cont { +.webBox-cont, .webBox-cont-interactive{ padding: 0vw; position: absolute; top: 0; left:0; width: 100%; height: 100%; - overflow: scroll; + overflow: auto; + pointer-events: none ; +} +.webBox-cont-interactive { + pointer-events: all; + span { + user-select: text !important; + } +} + +#webBox-htmlSpan { + position: absolute; + top:0; + left:0; +} + +.webBox-overlay { + width: 100%; + height: 100%; + position: absolute; } .webBox-button { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 90ce72c41..a7c6fda8b 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,37 +2,57 @@ import "./WebBox.scss"; import React = require("react"); import { WebField } from '../../../fields/WebField'; import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting } from '../../../fields/Field'; +import { FieldWaiting, Opt } from '../../../fields/Field'; import { observer } from "mobx-react"; -import { computed } from 'mobx'; +import { computed, reaction, IReactionDisposer } from 'mobx'; import { KeyStore } from '../../../fields/KeyStore'; +import { DocumentDecorations } from "../DocumentDecorations"; +import { InkingControl } from "../InkingControl"; @observer export class WebBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(WebBox); } - constructor(props: FieldViewProps) { - super(props); - } - @computed get html(): string { return this.props.Document.GetHtml(KeyStore.Data, ""); } + _ignore = 0; + onPreWheel = (e: React.WheelEvent) => { + this._ignore = e.timeStamp; + } + onPrePointer = (e: React.PointerEvent) => { + this._ignore = e.timeStamp; + } + onPostPointer = (e: React.PointerEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } + } + onPostWheel = (e: React.WheelEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } + } render() { let field = this.props.Document.Get(this.props.fieldKey); let path = field === FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; - let content = this.html ? - <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : - <div style={{ width: "100%", height: "100%", position: "absolute" }}> - <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> - {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + let content = + <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + {this.html ? <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: this.html }} /> : + <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }} />} </div>; + let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + + let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( - <div className="webBox-cont" > - {content} - </div>); + <> + <div className={classname} > + {content} + </div> + {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + </>); } }
\ No newline at end of file diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts index 996d2556d..87e47a715 100644 --- a/src/fields/AudioField.ts +++ b/src/fields/AudioField.ts @@ -20,11 +20,11 @@ export class AudioField extends BasicField<URL> { return new AudioField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Audio, data: this.Data.href, - _id: this.Id + id: this.Id }; } diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts index d319b4021..d49bfe82b 100644 --- a/src/fields/BooleanField.ts +++ b/src/fields/BooleanField.ts @@ -15,11 +15,11 @@ export class BooleanField extends BasicField<boolean> { return new BooleanField(this.Data); } - ToJson(): { type: Types; data: boolean; _id: string } { + ToJson() { return { type: Types.Boolean, data: this.Data, - _id: this.Id + id: this.Id }; } } diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 60eaf5b51..55ac731dd 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -13,6 +13,7 @@ import { BooleanField } from "./BooleanField"; import { allLimit } from "async"; import { prototype } from "nodemailer/lib/smtp-pool"; import { HistogramField } from "../client/northstar/dash-fields/HistogramField"; +import { Documents } from "../client/documents/Documents"; 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 @@ -26,6 +27,12 @@ export class Document extends Field { Server.UpdateField(this); } } + static FromJson(data: any, id: string, save: boolean): Document { + let doc = new Document(id, save); + let fields = data as [string, string][]; + fields.forEach(pair => doc._proxies.set(pair[0], pair[1])); + return doc; + } UpdateFromServer(data: [string, string][]) { for (const key in data) { @@ -41,14 +48,14 @@ export class Document extends Field { @computed public get Title(): string { let title = this.Get(KeyStore.Title, true); - if (title) { + if (title || title === FieldWaiting) { if (title !== FieldWaiting && title instanceof TextField) { return title.Data; } else return "-waiting-"; } let parTitle = this.GetT(KeyStore.Title, TextField); - if (parTitle) { + if (parTitle || parTitle === FieldWaiting) { if (parTitle !== FieldWaiting) return parTitle.Data + ".alias"; else return "-waiting-.alias"; } @@ -197,7 +204,7 @@ export class Document extends Field { callback: (field: T) => void ): void { //This currently doesn't deal with prototypes - if (this._proxies.has(key.Id)) { + if (this._proxies.has(key.Id) || this.fields.has(key.Id)) { Server.GetDocumentField(this, key, field => { if (field && field instanceof ctor) { callback(field); @@ -365,6 +372,36 @@ export class Document extends Field { return alias; } + @action + CreateLink(dstTarg: Document) { + let batch = UndoManager.StartBatch("document view drop"); + let linkDoc: Document = Documents.TextDocument({ width: 100, height: 25, title: "-link-" }); + linkDoc.SetText(KeyStore.LinkDescription, ""); + linkDoc.SetText(KeyStore.LinkTags, "Default"); + + let srcTarg = this; + linkDoc.Set(KeyStore.LinkedToDocs, dstTarg); + linkDoc.Set(KeyStore.LinkedFromDocs, srcTarg); + const prom1 = new Promise(resolve => dstTarg.GetOrCreateAsync( + KeyStore.LinkedFromDocs, + ListField, + field => { + (field as ListField<Document>).Data.push(linkDoc); + resolve(); + } + )); + const prom2 = new Promise(resolve => srcTarg.GetOrCreateAsync( + KeyStore.LinkedToDocs, + ListField, + field => { + (field as ListField<Document>).Data.push(linkDoc); + resolve(); + } + )); + Promise.all([prom1, prom2]).finally(() => batch.end()); + return linkDoc; + } + MakeDelegate(id?: string): Document { let delegate = new Document(id); @@ -410,18 +447,15 @@ export class Document extends Field { return copy; } - ToJson(): { type: Types; data: [string, string][]; _id: string } { + ToJson() { let fields: [string, string][] = []; - this._proxies.forEach((field, key) => { - if (field) { - fields.push([key, field]); - } - }); + this._proxies.forEach((field, key) => + field && fields.push([key, field])); return { type: Types.Document, data: fields, - _id: this.Id + id: this.Id }; } } diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts index 6c0c1ef82..303754177 100644 --- a/src/fields/DocumentReference.ts +++ b/src/fields/DocumentReference.ts @@ -47,11 +47,11 @@ export class DocumentReference extends Field { return ""; } - ToJson(): { type: Types, data: FieldId, _id: string } { + ToJson() { return { type: Types.DocumentReference, data: this.document.Id, - _id: this.Id + id: this.Id }; } }
\ No newline at end of file diff --git a/src/fields/Field.ts b/src/fields/Field.ts index d9db23b9e..3b3e95c2b 100644 --- a/src/fields/Field.ts +++ b/src/fields/Field.ts @@ -1,6 +1,6 @@ import { Utils } from "../Utils"; -import { Types } from "../server/Message"; +import { Types, Transferable } from "../server/Message"; import { computed } from "mobx"; export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T }): Opt<T> { @@ -65,5 +65,5 @@ export abstract class Field { abstract Copy(): Field; - abstract ToJson(): { _id: string, type: Types, data: any }; + abstract ToJson(): Transferable; }
\ No newline at end of file diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts index 65665cf7a..a1d880070 100644 --- a/src/fields/HtmlField.ts +++ b/src/fields/HtmlField.ts @@ -15,11 +15,11 @@ export class HtmlField extends BasicField<string> { return new HtmlField(this.Data); } - ToJson(): { _id: string; type: Types; data: string; } { + ToJson() { return { type: Types.Html, data: this.Data, - _id: this.Id, + id: this.Id, }; } }
\ No newline at end of file diff --git a/src/fields/IconFIeld.ts b/src/fields/IconFIeld.ts new file mode 100644 index 000000000..a6694cc49 --- /dev/null +++ b/src/fields/IconFIeld.ts @@ -0,0 +1,25 @@ +import { BasicField } from "./BasicField"; +import { FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class IconField extends BasicField<string> { + constructor(data: string = "", id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new IconField("${this.Data}")`; + } + + Copy() { + return new IconField(this.Data); + } + + ToJson() { + return { + type: Types.Icon, + data: this.Data, + id: this.Id + }; + } +}
\ No newline at end of file diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts index dd843026f..bce20f242 100644 --- a/src/fields/ImageField.ts +++ b/src/fields/ImageField.ts @@ -19,11 +19,11 @@ export class ImageField extends BasicField<URL> { return new ImageField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Image, data: this.Data.href, - _id: this.Id + id: this.Id }; } }
\ No newline at end of file diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index ab706ee30..2eacd7d0c 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -31,11 +31,11 @@ export class InkField extends BasicField<StrokeMap> { return new InkField(this.Data); } - ToJson(): { _id: string; type: Types; data: any; } { + ToJson() { return { type: Types.Ink, data: this.Data, - _id: this.Id, + id: this.Id, }; } diff --git a/src/fields/Key.ts b/src/fields/Key.ts index c7f806b88..57e2dadf0 100644 --- a/src/fields/Key.ts +++ b/src/fields/Key.ts @@ -40,11 +40,11 @@ export class Key extends Field { return name; } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Key, data: this.name, - _id: this.Id + id: this.Id }; } }
\ No newline at end of file diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 3a57d0f0a..e50041f1a 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -1,5 +1,4 @@ import { Key } from "./Key"; -import { KeyTransfer } from "../server/Message"; export namespace KeyStore { export const Prototype = new Key("Prototype"); @@ -16,10 +15,13 @@ export namespace KeyStore { export const Width = new Key("Width"); export const Height = new Key("Height"); export const ZIndex = new Key("ZIndex"); + export const ZoomBasis = new Key("ZoomBasis"); export const Data = new Key("Data"); export const Annotations = new Key("Annotations"); export const ViewType = new Key("ViewType"); + export const BaseLayout = new Key("BaseLayout"); export const Layout = new Key("Layout"); + export const Templates = new Key("Templates"); export const BackgroundColor = new Key("BackgroundColor"); export const BackgroundLayout = new Key("BackgroundLayout"); export const OverlayLayout = new Key("OverlayLayout"); @@ -45,15 +47,18 @@ export namespace KeyStore { export const OptionalRightCollection = new Key("OptionalRightCollection"); export const Archives = new Key("Archives"); export const Workspaces = new Key("Workspaces"); - export const Minimized = new Key("Minimized"); + export const IsMinimized = new Key("IsMinimized"); + export const MinimizedDoc = new Key("MinimizedDoc"); + export const MaximizedDoc = new Key("MaximizedDoc"); export const CopyDraggedItems = new Key("CopyDraggedItems"); export const PanTransformType = new Key("PanTransformType"); + export const BorderRounding = new Key("BorderRounding"); export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight, - Width, Height, ZIndex, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys, + Width, Height, ZIndex, ZoomBasis, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys, LayoutFields, ColumnsKey, SchemaSplitPercentage, Caption, ActiveWorkspace, DocumentText, BrushingDocs, LinkedToDocs, LinkedFromDocs, LinkDescription, LinkTags, Thumbnail, ThumbnailPage, CurPage, AnnotationOn, NumPages, Ink, Cursors, OptionalRightCollection, - Archives, Workspaces, Minimized, CopyDraggedItems + Archives, Workspaces, IsMinimized, MinimizedDoc, MaximizedDoc, CopyDraggedItems, BorderRounding ]; export function KeyLookup(keyid: string) { for (const key of KeyList) { diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index 8311e737b..e24099126 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -176,14 +176,14 @@ export class ListField<T extends Field> extends BasicField<T[]> { this._proxies = data.fields; this._scriptIds = data.scripts; } - ToJson(): { type: Types, data: { fields: string[], scripts: string[] }, _id: string } { + ToJson() { return { type: Types.List, data: { fields: this._proxies, scripts: this._scriptIds, }, - _id: this.Id + id: this.Id }; } diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts index 45b920e31..7eea360c0 100644 --- a/src/fields/NumberField.ts +++ b/src/fields/NumberField.ts @@ -15,9 +15,9 @@ export class NumberField extends BasicField<number> { return new NumberField(this.Data); } - ToJson(): { _id: string, type: Types, data: number } { + ToJson() { return { - _id: this.Id, + id: this.Id, type: Types.Number, data: this.Data }; diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts index 65e179894..718a1a4c0 100644 --- a/src/fields/PDFField.ts +++ b/src/fields/PDFField.ts @@ -22,11 +22,11 @@ export class PDFField extends BasicField<URL> { return `new PDFField("${this.Data}")`; } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.PDF, data: this.Data.href, - _id: this.Id + id: this.Id }; } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 6f7b3074a..f53f48ca6 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -15,11 +15,11 @@ export class RichTextField extends BasicField<string> { return new RichTextField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.RichText, data: this.Data, - _id: this.Id + id: this.Id }; } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 24c1d9b3a..7f87be45d 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -2,33 +2,34 @@ import { Field, FieldId } from "./Field"; import { Types } from "../server/Message"; import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting"; import { Server } from "../client/Server"; +import { Without } from "../Utils"; + +export interface SerializableOptions extends Without<ScriptOptions, "capturedVariables"> { + capturedIds: { [id: string]: string }; +} export interface ScriptData { script: string; - options: ScriptOptions; + options: SerializableOptions; } export class ScriptField extends Field { - readonly script: CompiledScript; + private _script?: CompiledScript; + get script(): CompiledScript { + return this._script!; + } + private options?: ScriptData; - constructor(script: CompiledScript, id?: FieldId, save: boolean = true) { + constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) { super(id); - this.script = script; + this._script = script; if (save) { Server.UpdateField(this); } } - static FromJson(id: string, data: ScriptData): ScriptField { - const script = CompileScript(data.script, data.options); - if (!script.compiled) { - throw new Error("Can't compile script"); - } - return new ScriptField(script, id, false); - } - ToScriptString() { return "new ScriptField(...)"; } @@ -45,14 +46,50 @@ export class ScriptField extends Field { throw new Error("Script fields currently can't be updated"); } - ToJson(): { _id: string, type: Types, data: ScriptData } { + static FromJson(id: string, data: ScriptData): ScriptField { + let field = new ScriptField(undefined, id, false); + field.options = data; + return field; + } + + init(callback: (res: Field) => any) { + const options = this.options!; + const keys = Object.keys(options.options.capturedIds); + Server.GetFields(keys).then(fields => { + let captured: { [name: string]: Field } = {}; + keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); + const opts: ScriptOptions = { + addReturn: options.options.addReturn, + params: options.options.params, + requiredType: options.options.requiredType, + capturedVariables: captured + }; + const script = CompileScript(options.script, opts); + if (!script.compiled) { + throw new Error("Can't compile script"); + } + this._script = script; + callback(this); + }); + } + + ToJson() { const { options, originalScript } = this.script; + let capturedIds: { [id: string]: string } = {}; + for (const capt in options.capturedVariables) { + capturedIds[options.capturedVariables[capt].Id] = capt; + } + const opts: SerializableOptions = { + ...options, + capturedIds + }; + delete (opts as any).capturedVariables; return { - _id: this.Id, + id: this.Id, type: Types.Script, data: { script: originalScript, - options + options: opts, }, }; } diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts new file mode 100644 index 000000000..72ae13c2e --- /dev/null +++ b/src/fields/TemplateField.ts @@ -0,0 +1,43 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; +import { Template, TemplatePosition } from "../client/views/Templates"; + + +export class TemplateField extends BasicField<Array<Template>> { + constructor(data: Array<Template> = [], id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new TemplateField("${this.Data}")`; + } + + Copy() { + return new TemplateField(this.Data); + } + + ToJson() { + let templates: Array<{ name: string, position: TemplatePosition, layout: string }> = []; + this.Data.forEach(template => { + templates.push({ name: template.Name, layout: template.Layout, position: template.Position }); + }); + return { + type: Types.Templates, + data: templates, + id: this.Id, + }; + } + + UpdateFromServer(data: any) { + this.data = new Array(data); + } + + static FromJson(id: string, data: any): TemplateField { + let templates: Array<Template> = []; + data.forEach((template: { name: string, position: number, layout: string }) => { + templates.push(new Template(template.name, template.position, template.layout)); + }); + return new TemplateField(templates, id, false); + } +}
\ No newline at end of file diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts index 69d26f42f..ddedec9b1 100644 --- a/src/fields/TextField.ts +++ b/src/fields/TextField.ts @@ -15,11 +15,11 @@ export class TextField extends BasicField<string> { return new TextField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Text, data: this.Data, - _id: this.Id + id: this.Id }; } }
\ No newline at end of file diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts index ad0f6f350..347f1fa05 100644 --- a/src/fields/TupleField.ts +++ b/src/fields/TupleField.ts @@ -49,11 +49,11 @@ export class TupleField<T, U> extends BasicField<[T, U]> { return new TupleField<T, U>(this.Data); } - ToJson(): { type: Types, data: [T, U], _id: string } { + ToJson() { return { type: Types.Tuple, data: this.Data, - _id: this.Id + id: this.Id }; } }
\ No newline at end of file diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts index d7cd7e968..838b811b1 100644 --- a/src/fields/VideoField.ts +++ b/src/fields/VideoField.ts @@ -19,11 +19,11 @@ export class VideoField extends BasicField<URL> { return new VideoField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Video, data: this.Data.href, - _id: this.Id + id: this.Id }; } diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts index 6023e9e6b..8b276a552 100644 --- a/src/fields/WebField.ts +++ b/src/fields/WebField.ts @@ -19,11 +19,11 @@ export class WebField extends BasicField<URL> { return new WebField(this.Data); } - ToJson(): { type: Types, data: string, _id: string } { + ToJson() { return { type: Types.Web, data: this.Data.href, - _id: this.Id + id: this.Id }; } diff --git a/src/server/Client.ts b/src/server/Client.ts index 02402a5a0..e6f953712 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -1,15 +1,11 @@ import { computed } from "mobx"; export class Client { - constructor(guid: string) { - this.guid = guid; - } + private _guid: string; - private guid: string; - - @computed - public get GUID(): string { - return this.guid; + constructor(guid: string) { + this._guid = guid; } + @computed public get GUID(): string { return this._guid; } }
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index d22e5c17c..854ae0168 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,146 +1,35 @@ import { Utils } from "../Utils"; export class Message<T> { - private name: string; - private guid: string; - - get Name(): string { - return this.name; - } - - get Message(): string { - return this.guid; - } + private _name: string; + private _guid: string; constructor(name: string) { - this.name = name; - this.guid = Utils.GenerateDeterministicGuid(name); - } - - GetValue() { - return this.Name; - } -} - -class TestMessageArgs { - hello: string = ""; -} - -export class SetFieldArgs { - field: string; - value: any; - - constructor(f: string, v: any) { - this.field = f; - this.value = v; + this._name = name; + this._guid = Utils.GenerateDeterministicGuid(name); } -} -export class GetFieldArgs { - field: string; - - constructor(f: string) { - this.field = f; - } + get Name(): string { return this._name; } + get Message(): string { return this._guid; } } export enum Types { - Number, - List, - Key, - Image, - Web, - Document, - Text, - RichText, - DocumentReference, - Html, - Video, - Audio, - Ink, - PDF, - Tuple, - HistogramOp, - Boolean, - Script, -} - -export class DocumentTransfer implements Transferable { - readonly type = Types.Document; - _id: string; - - constructor( - readonly obj: { type: Types; data: [string, string][]; _id: string } - ) { - this._id = obj._id; - } -} - -export class ImageTransfer implements Transferable { - readonly type = Types.Image; - - constructor(readonly _id: string) { } -} - -export class KeyTransfer implements Transferable { - name: string; - readonly _id: string; - readonly type = Types.Key; - - constructor(i: string, n: string) { - this.name = n; - this._id = i; - } -} - -export class ListTransfer implements Transferable { - type = Types.List; - - constructor(readonly _id: string) { } -} - -export class NumberTransfer implements Transferable { - readonly type = Types.Number; - - constructor(readonly value: number, readonly _id: string) { } -} - -export class TextTransfer implements Transferable { - value: string; - readonly _id: string; - readonly type = Types.Text; - - constructor(t: string, i: string) { - this.value = t; - this._id = i; - } -} - -export class RichTextTransfer implements Transferable { - value: string; - readonly _id: string; - readonly type = Types.Text; - - constructor(t: string, i: string) { - this.value = t; - this._id = i; - } + Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference, + Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, Templates } export interface Transferable { - readonly _id: string; + readonly id: string; readonly type: Types; + readonly data?: any; } export namespace MessageStore { export const Foo = new Message<string>("Foo"); export const Bar = new Message<string>("Bar"); - export const AddDocument = new Message<DocumentTransfer>("Add Document"); - export const SetField = new Message<{ _id: string; data: any; type: Types }>( - "Set Field" - ); - export const GetField = new Message<string>("Get Field"); - export const GetFields = new Message<string[]>("Get Fields"); + export const SetField = new Message<Transferable>("Set Field"); // send Transferable (no reply) + export const GetField = new Message<string>("Get Field"); // send string 'id' get Transferable back + export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back export const GetDocument = new Message<string>("Get Document"); export const DeleteAll = new Message<any>("Delete All"); } diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 0973f82b1..eb5749dff 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -1,83 +1,60 @@ -import { Field } from "./../fields/Field"; -import { TextField } from "./../fields/TextField"; -import { NumberField } from "./../fields/NumberField"; -import { RichTextField } from "./../fields/RichTextField"; -import { Key } from "./../fields/Key"; -import { ImageField } from "./../fields/ImageField"; -import { ListField } from "./../fields/ListField"; -import { Document } from "./../fields/Document"; -import { Server } from "./../client/Server"; -import { Types } from "./Message"; -import { Utils } from "../Utils"; -import { HtmlField } from "../fields/HtmlField"; -import { WebField } from "../fields/WebField"; +import { HistogramField } from "../client/northstar/dash-fields/HistogramField"; import { AudioField } from "../fields/AudioField"; -import { VideoField } from "../fields/VideoField"; +import { BooleanField } from "../fields/BooleanField"; +import { HtmlField } from "../fields/HtmlField"; import { InkField } from "../fields/InkField"; import { PDFField } from "../fields/PDFField"; -import { TupleField } from "../fields/TupleField"; -import { BooleanField } from "../fields/BooleanField"; -import { HistogramField } from "../client/northstar/dash-fields/HistogramField"; import { ScriptField } from "../fields/ScriptField"; +import { TupleField } from "../fields/TupleField"; +import { VideoField } from "../fields/VideoField"; +import { WebField } from "../fields/WebField"; +import { Utils } from "../Utils"; +import { Document } from "./../fields/Document"; +import { Field } from "./../fields/Field"; +import { ImageField } from "./../fields/ImageField"; +import { Key } from "./../fields/Key"; +import { ListField } from "./../fields/ListField"; +import { NumberField } from "./../fields/NumberField"; +import { RichTextField } from "./../fields/RichTextField"; +import { TextField } from "./../fields/TextField"; +import { Transferable, Types } from "./Message"; +import { TemplateField } from "../fields/TemplateField"; +import { IconField } from "../fields/IconFIeld"; 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; - let id: string = obj._id; - let type: Types = obj.type; + public static FromJson(json: Transferable): Field { - if (!(data !== undefined && id && type !== undefined)) { + if (!(json.data !== undefined && json.id && json.type !== undefined)) { console.log( "how did you manage to get an object that doesn't have a data or an id?" ); return new TextField("Something to fill the space", Utils.GenerateGuid()); } - switch (type) { - case Types.Boolean: - return new BooleanField(data, id, false); - case Types.Number: - return new NumberField(data, id, false); - case Types.Text: - return new TextField(data, id, false); - case Types.Html: - return new HtmlField(data, id, false); - case Types.Web: - return new WebField(new URL(data), id, false); - case Types.RichText: - return new RichTextField(data, id, false); - case Types.Key: - return new Key(data, id, false); - case Types.Image: - return new ImageField(new URL(data), id, false); - case Types.HistogramOp: - return HistogramField.FromJson(id, data); - case Types.PDF: - return new PDFField(new URL(data), id, false); - case Types.List: - return ListField.FromJson(id, data); - case Types.Script: - return ScriptField.FromJson(id, data); - case Types.Audio: - 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: - let doc: Document = new Document(id, false); - let fields: [string, string][] = data as [string, string][]; - fields.forEach(element => { - doc._proxies.set(element[0], element[1]); - }); - return doc; + switch (json.type) { + case Types.Boolean: return new BooleanField(json.data, json.id, false); + case Types.Number: return new NumberField(json.data, json.id, false); + case Types.Text: return new TextField(json.data, json.id, false); + case Types.Icon: return new IconField(json.data, json.id, false); + case Types.Html: return new HtmlField(json.data, json.id, false); + case Types.Web: return new WebField(new URL(json.data), json.id, false); + case Types.RichText: return new RichTextField(json.data, json.id, false); + case Types.Key: return new Key(json.data, json.id, false); + case Types.Image: return new ImageField(new URL(json.data), json.id, false); + case Types.HistogramOp: return HistogramField.FromJson(json.id, json.data); + case Types.PDF: return new PDFField(new URL(json.data), json.id, false); + case Types.List: return ListField.FromJson(json.id, json.data); + case Types.Script: return ScriptField.FromJson(json.id, json.data); + case Types.Audio: return new AudioField(new URL(json.data), json.id, false); + case Types.Video: return new VideoField(new URL(json.data), json.id, false); + case Types.Tuple: return new TupleField(json.data, json.id, false); + case Types.Ink: return InkField.FromJson(json.id, json.data); + case Types.Templates: return TemplateField.FromJson(json.id, json.data); + case Types.Document: return Document.FromJson(json.data, json.id, false); default: throw Error( "Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here" diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 13eddafbf..5d4479c88 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,81 +1,37 @@ -import { DashUserModel } from "./user_model"; +import { computed, observable, action, runInAction } from "mobx"; import * as rp from 'request-promise'; -import { RouteStore } from "../../RouteStore"; -import { ServerUtils } from "../../ServerUtil"; +import { Documents } from "../../../client/documents/Documents"; +import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea"; +import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; import { Server } from "../../../client/Server"; import { Document } from "../../../fields/Document"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { Documents } from "../../../client/documents/Documents"; -import { Schema, Attribute, AttributeGroup, Catalog } from "../../../client/northstar/model/idea/idea"; -import { observable, computed, action } from "mobx"; -import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; +import { RouteStore } from "../../RouteStore"; +import { ServerUtils } from "../../ServerUtil"; export class CurrentUserUtils { private static curr_email: string; private static curr_id: string; - private static user_document: Document; + @observable private static user_document: Document; //TODO tfs: these should be temporary... private static mainDocId: string | undefined; - @observable private static catalog?: Catalog; - - public static get email(): string { - return this.curr_email; - } - - public static get id(): string { - return this.curr_id; - } - - public static get UserDocument(): Document { - return this.user_document; - } - public static get MainDocId(): string | undefined { - return this.mainDocId; - } - - public static set MainDocId(id: string | undefined) { - this.mainDocId = id; - } - - @computed public static get NorthstarDBCatalog(): Catalog | undefined { - return this.catalog; - } - public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { - this.catalog = ctlog; - } - public static GetNorthstarSchema(name: string): Schema | undefined { - return !this.catalog || !this.catalog.schemas ? undefined : - ArrayUtil.FirstOrDefault<Schema>(this.catalog.schemas, (s: Schema) => s.displayName === name); - } - public static GetAllNorthstarColumnAttributes(schema: Schema) { - if (!schema || !schema.rootAttributeGroup) { - return []; - } - const recurs = (attrs: Attribute[], g: AttributeGroup) => { - if (g.attributes) { - attrs.push.apply(attrs, g.attributes); - if (g.attributeGroups) { - g.attributeGroups.forEach(ng => recurs(attrs, ng)); - } - } - }; - const allAttributes: Attribute[] = new Array<Attribute>(); - recurs(allAttributes, schema.rootAttributeGroup); - return allAttributes; - } + public static get email() { return this.curr_email; } + public static get id() { return this.curr_id; } + @computed public static get UserDocument() { return this.user_document; } + public static get MainDocId() { return this.mainDocId; } + public static set MainDocId(id: string | undefined) { this.mainDocId = id; } private static createUserDocument(id: string): Document { let doc = new Document(id); - doc.Set(KeyStore.Workspaces, new ListField<Document>()); doc.Set(KeyStore.OptionalRightCollection, Documents.SchemaDocument([], { title: "Pending documents" })); return doc; } public static loadCurrentUser(): Promise<any> { - let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then((response) => { + let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then(response => { if (response) { let obj = JSON.parse(response); CurrentUserUtils.curr_id = obj.id as string; @@ -86,17 +42,34 @@ export class CurrentUserUtils { }); let userDocPromise = rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id) { - return Server.GetField(id).then(field => { - if (field instanceof Document) { - this.user_document = field; - } else { - this.user_document = this.createUserDocument(id); - } - }); + return Server.GetField(id).then(field => + runInAction(() => this.user_document = field instanceof Document ? field : this.createUserDocument(id))); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } }); return Promise.all([userPromise, userDocPromise]); } + + /* Northstar catalog ... really just for testing so this should eventually go away */ + @observable private static _northstarCatalog?: Catalog; + @computed public static get NorthstarDBCatalog() { return this._northstarCatalog; } + public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { this._northstarCatalog = ctlog; } + + public static GetNorthstarSchema(name: string): Schema | undefined { + return !this._northstarCatalog || !this._northstarCatalog.schemas ? undefined : + ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas, (s: Schema) => s.displayName === name); + } + public static GetAllNorthstarColumnAttributes(schema: Schema) { + const recurs = (attrs: Attribute[], g?: AttributeGroup) => { + if (g && g.attributes) { + attrs.push.apply(attrs, g.attributes); + if (g.attributeGroups) { + g.attributeGroups.forEach(ng => recurs(attrs, ng)); + } + } + return attrs; + }; + return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined); + } }
\ No newline at end of file diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts index 1c6926517..ee85e1c05 100644 --- a/src/server/authentication/models/user_model.ts +++ b/src/server/authentication/models/user_model.ts @@ -18,8 +18,8 @@ mongoose.connection.on('disconnected', function () { export type DashUserModel = mongoose.Document & { email: string, password: string, - passwordResetToken: string | undefined, - passwordResetExpires: Date | undefined, + passwordResetToken?: string, + passwordResetExpires?: Date, userDocumentId: string; @@ -67,11 +67,17 @@ const userSchema = new mongoose.Schema({ */ userSchema.pre("save", function save(next) { const user = this as DashUserModel; - if (!user.isModified("password")) { return next(); } + if (!user.isModified("password")) { + return next(); + } bcrypt.genSalt(10, (err, salt) => { - if (err) { return next(err); } + if (err) { + return next(err); + } bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => { - if (err) { return next(err); } + if (err) { + return next(err); + } user.password = hash; next(); }); @@ -79,9 +85,7 @@ userSchema.pre("save", function save(next) { }); const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { - bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => { - cb(err, isMatch); - }); + bcrypt.compare(candidatePassword, this.password, cb); }; userSchema.methods.comparePassword = comparePassword; diff --git a/src/server/database.ts b/src/server/database.ts index 0bc806253..5457e4dd5 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -1,107 +1,75 @@ import * as mongodb from 'mongodb'; +import { Transferable } from './Message'; export class Database { + public static DocumentsCollection = 'documents'; public static Instance = new Database(); private MongoClient = mongodb.MongoClient; private url = 'mongodb://localhost:27017/Dash'; + private currentWrites: { [id: string]: Promise<void> } = {}; private db?: mongodb.Db; constructor() { - this.MongoClient.connect(this.url, (err, client) => { - this.db = client.db(); - }); + this.MongoClient.connect(this.url, (err, client) => this.db = client.db()); } - private currentWrites: { [_id: string]: Promise<void> } = {}; - public update(id: string, value: any, callback: () => void) { if (this.db) { let collection = this.db.collection('documents'); const prom = this.currentWrites[id]; - const run = (promise: Promise<void>, resolve?: () => void) => { - collection.updateOne({ _id: id }, { $set: value }, { - upsert: true - }, (err, res) => { - if (err) { - console.log(err.message); - console.log(err.errmsg); - } - // if (res) { - // console.log(JSON.stringify(res.result)); - // } - if (this.currentWrites[id] === promise) { - delete this.currentWrites[id]; - } - if (resolve) { - resolve(); - } - callback(); + let newProm: Promise<void>; + const run = (): Promise<void> => { + return new Promise<void>(resolve => { + collection.updateOne({ _id: id }, { $set: value }, { upsert: true } + , (err, res) => { + if (err) { + console.log(err.message); + console.log(err.errmsg); + } + // if (res) { + // console.log(JSON.stringify(res.result)); + // } + if (this.currentWrites[id] === newProm) { + delete this.currentWrites[id]; + } + resolve(); + callback(); + }); }); }; - if (prom) { - const newProm: Promise<void> = prom.then(() => run(newProm)); - this.currentWrites[id] = newProm; - } else { - const newProm: Promise<void> = new Promise<void>(res => run(newProm, res)); - this.currentWrites[id] = newProm; - } + newProm = prom ? prom.then(run) : run(); + this.currentWrites[id] = newProm; } } - public delete(id: string) { - if (this.db) { - let collection = this.db.collection('documents'); - collection.remove({ _id: id }); - } + public delete(id: string, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).remove({ id: id }); } - public deleteAll(collectionName: string = 'documents'): Promise<any> { - return new Promise(res => { - if (this.db) { - let collection = this.db.collection(collectionName); - collection.deleteMany({}, res); - } - }); + public deleteAll(collectionName = Database.DocumentsCollection): Promise<any> { + return new Promise(res => + this.db && this.db.collection(collectionName).deleteMany({}, res)); } - public insert(kvpairs: any) { - if (this.db) { - let collection = this.db.collection('documents'); - collection.insertOne(kvpairs, (err: any, res: any) => { - if (err) { - // console.log(err) - return; - } - }); - } + public insert(kvpairs: any, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) => + err // && console.log(err) + ); } - public getDocument(id: string, fn: (res: any) => void) { - var result: JSON; - if (this.db) { - let collection = this.db.collection('documents'); - collection.findOne({ _id: id }, (err: any, res: any) => { - result = res; - if (!result) { - fn(undefined); - } - fn(result); - }); - } + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).findOne({ id: id }, (err, result) => + fn(result ? ({ id: result._id, type: result.type, data: result.data }) : undefined)); } - public getDocuments(ids: string[], fn: (res: any) => void) { - if (this.db) { - 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); - }); - } + public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).find({ id: { "$in": ids } }).toArray((err, docs) => { + if (err) { + console.log(err.message); + console.log(err.errmsg); + } + fn(docs.map(doc => ({ id: doc._id, type: doc.type, data: doc.data }))); + }); } public print() { diff --git a/src/server/index.ts b/src/server/index.ts index b9c7448b4..70a7d266c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,49 +1,45 @@ +import * as bodyParser from 'body-parser'; +import { exec } from 'child_process'; +import * as cookieParser from 'cookie-parser'; import * as express from 'express'; -const app = express(); -import * as webpack from 'webpack'; -import * as wdm from 'webpack-dev-middleware'; -import * as whm from 'webpack-hot-middleware'; -import * as path from 'path'; +import * as session from 'express-session'; +import * as expressValidator from 'express-validator'; import * as formidable from 'formidable'; +import * as fs from 'fs'; +import * as mobileDetect from 'mobile-detect'; +import { ObservableMap } from 'mobx'; import * as passport from 'passport'; -import { MessageStore, Transferable } from "./Message"; -import { Client } from './Client'; +import * as path from 'path'; +import * as request from 'request'; +import * as io from 'socket.io'; import { Socket } from 'socket.io'; +import * as webpack from 'webpack'; +import * as wdm from 'webpack-dev-middleware'; +import * as whm from 'webpack-hot-middleware'; +import { Field, FieldId } from '../fields/Field'; import { Utils } from '../Utils'; -import { ObservableMap } from 'mobx'; -import { FieldId, Field } from '../fields/Field'; +import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller'; +import { DashUserModel } from './authentication/models/user_model'; +import { Client } from './Client'; import { Database } from './database'; -import * as io from 'socket.io'; -import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; +import { MessageStore, Transferable } from "./Message"; +import { RouteStore } from './RouteStore'; +const app = express(); const config = require('../../webpack.config'); const compiler = webpack(config); const port = 1050; // default port to listen const serverPort = 4321; -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'); -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 download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); const mongoUrl = 'mongodb://localhost:27017/Dash'; mongoose.connect(mongoUrl); -mongoose.connection.on('connected', function () { - console.log("connected"); -}); +mongoose.connection.on('connected', () => console.log("connected")); // SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE // ORDER OF IMPORTS MATTERS @@ -54,9 +50,7 @@ app.use(session({ resave: true, cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, saveUninitialized: true, - store: new MongoStore({ - url: 'mongodb://localhost:27017/Dash' - }) + store: new MongoStore({ url: 'mongodb://localhost:27017/Dash' }) })); app.use(flash()); @@ -71,9 +65,7 @@ app.use((req, res, next) => { next(); }); -app.get("/hello", (req, res) => { - res.send("<p>Hello</p>"); -}); +app.get("/hello", (req, res) => res.send("<p>Hello</p>")); enum Method { GET, @@ -95,9 +87,11 @@ function addSecureRoute(method: Method, ...subscribers: string[] ) { let abstracted = (req: express.Request, res: express.Response) => { - const dashUser: DashUserModel = req.user; - if (!dashUser) return onRejection(res); - handler(dashUser, res, req); + if (req.user) { + handler(req.user, res, req); + } else { + onRejection(res); + } }; subscribers.forEach(route => { switch (method) { @@ -112,21 +106,17 @@ function addSecureRoute(method: Method, } // STATIC FILE SERVING - -let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); - app.use(express.static(__dirname + RouteStore.public)); app.use(RouteStore.images, express.static(__dirname + RouteStore.public)); -app.get("/pull", (req, res) => { +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("/"); - }); -}); + })); // GETTERS @@ -143,11 +133,8 @@ 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')); - } + let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; + res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }, undefined, RouteStore.home, @@ -163,12 +150,7 @@ addSecureRoute( addSecureRoute( Method.GET, - (user, res) => { - res.send(JSON.stringify({ - id: user.id, - email: user.email - })); - }, + (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })), undefined, RouteStore.getCurrUser ); @@ -185,10 +167,9 @@ addSecureRoute( console.log("upload"); form.parse(req, (err, fields, files) => { console.log("parsing"); - let names: any[] = []; + let names: string[] = []; for (const name in files) { - let file = files[name]; - names.push(`/files/` + path.basename(file.path)); + names.push(`/files/` + path.basename(files[name].path)); } res.send(names); }); @@ -218,28 +199,22 @@ app.post(RouteStore.forgot, postForgot); 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.use(RouteStore.corsProxy, (req, res) => + req.pipe(request(req.url.substring(1))).pipe(res)); -app.get(RouteStore.delete, (req, res) => { - deleteFields().then(() => res.redirect(RouteStore.home)); -}); +app.get(RouteStore.delete, (req, res) => + deleteFields().then(() => res.redirect(RouteStore.home))); -app.get(RouteStore.deleteAll, (req, res) => { - deleteAll().then(() => res.redirect(RouteStore.home)); -}); +app.get(RouteStore.deleteAll, (req, res) => + deleteAll().then(() => res.redirect(RouteStore.home))); -app.use(wdm(compiler, { - publicPath: config.output.publicPath -})); +app.use(wdm(compiler, { publicPath: config.output.publicPath })); app.use(whm(compiler)); // start the Express server -app.listen(port, () => { - console.log(`server started at http://localhost:${port}`); -}); +app.listen(port, () => + console.log(`server started at http://localhost:${port}`)); const server = io(); interface Map { @@ -273,25 +248,18 @@ function barReceived(guid: String) { clients[guid.toString()] = new Client(guid.toString()); } -function getField([id, callback]: [string, (result: any) => void]) { - Database.Instance.getDocument(id, (result: any) => { - if (result) { - callback(result); - } - else { - callback(undefined); - } - }); +function getField([id, callback]: [string, (result?: Transferable) => void]) { + Database.Instance.getDocument(id, (result?: Transferable) => + callback(result ? result : undefined)); } -function getFields([ids, callback]: [string[], (result: any) => void]) { +function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) { Database.Instance.getDocuments(ids, callback); } function setField(socket: Socket, newValue: Transferable) { - Database.Instance.update(newValue._id, newValue, () => { - socket.broadcast.emit(MessageStore.SetField.Message, newValue); - }); + Database.Instance.update(newValue.id, newValue, () => + socket.broadcast.emit(MessageStore.SetField.Message, newValue)); } server.listen(serverPort); diff --git a/tsconfig.json b/tsconfig.json index 41db1d0a7..0d4d77002 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,23 @@ { - "compilerOptions": { - "target": "es5", - "removeComments": true, - "experimentalDecorators": true, - "strict": true, - "jsx": "react", - "sourceMap": true, - "outDir": "dist", - "lib": [ - "dom", - "es2015" - ], - }, - // "exclude": [ - // "node_modules", - // "static" - // ], - "typeRoots": [ - "./node_modules/@types", - "./src/typings" - ] + "compilerOptions": { + "target": "es5", + "removeComments": true, + "experimentalDecorators": true, + "strict": true, + "jsx": "react", + "sourceMap": true, + "outDir": "dist", + "lib": [ + "dom", + "es2015" + ], + }, + // "exclude": [ + // "node_modules", + // "static" + // ], + "typeRoots": [ + "./node_modules/@types", + "./src/typings" + ] }
\ No newline at end of file diff --git a/tslint.json b/tslint.json index aa4dee4e5..76d28b375 100644 --- a/tslint.json +++ b/tslint.json @@ -52,5 +52,6 @@ // } // ], // "ordered-imports": true - } + }, + "defaultSeverity": "warning" }
\ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 50079255f..c08742272 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,97 +1,96 @@ var path = require('path'); var webpack = require('webpack'); const CopyWebpackPlugin = require("copy-webpack-plugin"); +const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); module.exports = { - mode: 'development', - entry: { - 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: { - fs: 'empty', - module: 'empty', - dns: 'mock', - tls: 'mock', - net: 'mock' - }, - output: { - filename: "[name].js", - path: path.resolve(__dirname, "build"), - publicPath: "/" - }, - resolve: { - extensions: ['.js', '.ts', '.tsx'] - }, - module: { - rules: [ - { - test: [/\.tsx?$/, /\.ts?$/,], - enforce: 'pre', - use: [ - { - loader: "tslint-loader", - } - ] - }, { - test: [/\.tsx?$/, /\.ts?$/,], - loader: "awesome-typescript-loader", - include: path.join(__dirname, 'src') - }, - { - test: /\.scss|css$/, - use: [ - { - loader: "style-loader" - }, - { - loader: "css-loader" - }, - { - loader: "sass-loader" - } - ] - }, - { - test: /\.(jpg|png|pdf)$/, - use: [ - { - loader: 'file-loader' - } - ] - }, - { - test: /\.(png|jpg|gif)$/i, - use: [ - { - loader: 'url-loader', - options: { - limit: 8192 - } - } - ] - }] - }, - plugins: [ - new CopyWebpackPlugin([{ from: "deploy", to: path.join(__dirname, "build") }]), - new webpack.optimize.OccurrenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoEmitOnErrorsPlugin() - ], - devServer: { - compress: false, - host: "localhost", - contentBase: path.join(__dirname, 'deploy'), - port: 4321, - hot: true, - https: false, - overlay: { - warnings: true, - errors: true + mode: 'development', + entry: { + 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'], + }, + optimization: { + noEmitOnErrors: true + }, + devtool: "source-map", + node: { + fs: 'empty', + module: 'empty', + dns: 'mock', + tls: 'mock', + net: 'mock' + }, + output: { + filename: "[name].js", + path: path.resolve(__dirname, "build"), + publicPath: "/" + }, + resolve: { + extensions: ['.js', '.ts', '.tsx'] + }, + module: { + rules: [ + { + test: [/\.tsx?$/], + use: [ + { loader: 'ts-loader', options: { transpileOnly: true } } + ] + }, + { + test: /\.scss|css$/, + use: [ + { + loader: "style-loader" + }, + { + loader: "css-loader" + }, + { + loader: "sass-loader" + } + ] + }, + { + test: /\.(jpg|png|pdf)$/, + use: [ + { + loader: 'file-loader' + } + ] + }, + { + test: /\.(png|jpg|gif)$/i, + use: [ + { + loader: 'url-loader', + options: { + limit: 8192 + } + } + ] + }] + }, + plugins: [ + new CopyWebpackPlugin([{ from: "deploy", to: path.join(__dirname, "build") }]), + new ForkTsCheckerWebpackPlugin({ + tslint: true, useTypescriptIncrementalApi: true + }), + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.HotModuleReplacementPlugin(), + ], + devServer: { + compress: false, + host: "localhost", + contentBase: path.join(__dirname, 'deploy'), + port: 4321, + hot: true, + https: false, + overlay: { + warnings: true, + errors: true + } } - } };
\ No newline at end of file |
