aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts40
-rw-r--r--src/client/DocServer.ts130
-rw-r--r--src/client/documents/Documents.ts286
-rw-r--r--src/client/goldenLayout.js5359
-rw-r--r--src/client/northstar/core/brusher/IBaseBrushable.ts4
-rw-r--r--src/client/northstar/core/filter/FilterModel.ts17
-rw-r--r--src/client/northstar/core/filter/IBaseFilterConsumer.ts4
-rw-r--r--src/client/northstar/core/filter/ValueComparision.ts6
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts95
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.scss6
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx104
-rw-r--r--src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx8
-rw-r--r--src/client/northstar/operations/HistogramOperation.ts52
-rw-r--r--src/client/util/DocumentManager.ts66
-rw-r--r--src/client/util/DragManager.ts123
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts1
-rw-r--r--src/client/util/RichTextSchema.tsx87
-rw-r--r--src/client/util/Scripting.ts34
-rw-r--r--src/client/util/SelectionManager.ts30
-rw-r--r--src/client/util/SerializationHelper.ts130
-rw-r--r--src/client/util/TooltipTextMenu.tsx189
-rw-r--r--src/client/util/UndoManager.ts12
-rw-r--r--src/client/views/.DS_Storebin8196 -> 6148 bytes
-rw-r--r--src/client/views/DocComponent.tsx14
-rw-r--r--src/client/views/DocumentDecorations.scss128
-rw-r--r--src/client/views/DocumentDecorations.tsx390
-rw-r--r--src/client/views/EditableView.tsx2
-rw-r--r--src/client/views/InkingCanvas.tsx47
-rw-r--r--src/client/views/InkingControl.tsx7
-rw-r--r--src/client/views/InkingStroke.scss3
-rw-r--r--src/client/views/InkingStroke.tsx10
-rw-r--r--src/client/views/Main.scss48
-rw-r--r--src/client/views/Main.tsx190
-rw-r--r--src/client/views/MainOverlayTextBox.scss1
-rw-r--r--src/client/views/MainOverlayTextBox.tsx63
-rw-r--r--src/client/views/PresentationView.scss68
-rw-r--r--src/client/views/PresentationView.tsx190
-rw-r--r--src/client/views/PreviewCursor.tsx36
-rw-r--r--src/client/views/TemplateMenu.tsx91
-rw-r--r--src/client/views/Templates.tsx84
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx115
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx224
-rw-r--r--src/client/views/collections/CollectionPDFView.scss26
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx49
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss12
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx184
-rw-r--r--src/client/views/collections/CollectionSubView.tsx363
-rw-r--r--src/client/views/collections/CollectionTreeView.scss56
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx186
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx88
-rw-r--r--src/client/views/collections/CollectionView.tsx15
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss10
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx51
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx117
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx65
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss151
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx242
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx258
-rw-r--r--src/client/views/globalCssVariables.scss6
-rw-r--r--src/client/views/globalCssVariables.scss.d.ts4
-rw-r--r--src/client/views/nodes/AudioBox.tsx27
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx260
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx76
-rw-r--r--src/client/views/nodes/DocumentView.scss17
-rw-r--r--src/client/views/nodes/DocumentView.tsx365
-rw-r--r--src/client/views/nodes/FieldView.tsx84
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss18
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx266
-rw-r--r--src/client/views/nodes/IconBox.scss22
-rw-r--r--src/client/views/nodes/IconBox.tsx69
-rw-r--r--src/client/views/nodes/ImageBox.scss6
-rw-r--r--src/client/views/nodes/ImageBox.tsx78
-rw-r--r--src/client/views/nodes/KeyValueBox.scss1
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx37
-rw-r--r--src/client/views/nodes/KeyValuePair.scss1
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx62
-rw-r--r--src/client/views/nodes/LinkBox.scss23
-rw-r--r--src/client/views/nodes/LinkBox.tsx84
-rw-r--r--src/client/views/nodes/LinkEditor.scss3
-rw-r--r--src/client/views/nodes/LinkEditor.tsx17
-rw-r--r--src/client/views/nodes/LinkMenu.tsx27
-rw-r--r--src/client/views/nodes/PDFBox.scss18
-rw-r--r--src/client/views/nodes/PDFBox.tsx302
-rw-r--r--src/client/views/nodes/Sticky.tsx83
-rw-r--r--src/client/views/nodes/VideoBox.tsx80
-rw-r--r--src/client/views/nodes/WebBox.scss17
-rw-r--r--src/client/views/nodes/WebBox.tsx38
-rw-r--r--src/debug/Test.tsx87
-rw-r--r--src/debug/Viewer.tsx368
-rw-r--r--src/fields/AudioField.ts31
-rw-r--r--src/fields/BasicField.ts59
-rw-r--r--src/fields/BooleanField.ts25
-rw-r--r--src/fields/Document.ts430
-rw-r--r--src/fields/DocumentReference.ts57
-rw-r--r--src/fields/Field.ts69
-rw-r--r--src/fields/FieldUpdatedArgs.ts27
-rw-r--r--src/fields/HtmlField.ts25
-rw-r--r--src/fields/ImageField.ts29
-rw-r--r--src/fields/InkField.ts53
-rw-r--r--src/fields/Key.ts50
-rw-r--r--src/fields/KeyStore.ts65
-rw-r--r--src/fields/ListField.ts196
-rw-r--r--src/fields/NumberField.ts25
-rw-r--r--src/fields/PDFField.ts36
-rw-r--r--src/fields/RichTextField.ts26
-rw-r--r--src/fields/ScriptField.ts174
-rw-r--r--src/fields/TextField.ts25
-rw-r--r--src/fields/TupleField.ts59
-rw-r--r--src/fields/VideoField.ts30
-rw-r--r--src/fields/WebField.ts30
-rw-r--r--src/mobile/ImageUpload.tsx34
-rw-r--r--src/new_fields/CursorField.ts55
-rw-r--r--src/new_fields/DateField.ts18
-rw-r--r--src/new_fields/Doc.ts228
-rw-r--r--src/new_fields/HtmlField.ts18
-rw-r--r--src/new_fields/IconField.ts18
-rw-r--r--src/new_fields/InkField.ts43
-rw-r--r--src/new_fields/List.ts279
-rw-r--r--src/new_fields/ObjectField.ts18
-rw-r--r--src/new_fields/Proxy.ts65
-rw-r--r--src/new_fields/RefField.ts18
-rw-r--r--src/new_fields/RichTextField.ts18
-rw-r--r--src/new_fields/Schema.ts82
-rw-r--r--src/new_fields/Types.ts88
-rw-r--r--src/new_fields/URLField.ts34
-rw-r--r--src/new_fields/util.ts104
-rw-r--r--src/server/Message.ts17
-rw-r--r--src/server/ServerUtil.ts60
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.css3
-rw-r--r--src/server/authentication/controllers/WorkspacesMenu.tsx89
-rw-r--r--src/server/authentication/models/current_user_utils.ts37
-rw-r--r--src/server/database.ts58
-rw-r--r--src/server/index.ts31
134 files changed, 11343 insertions, 4508 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index dec6245ef..d4b6f5377 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,11 +1,12 @@
import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
-import { Message, Types, Transferable } from './server/Message';
-import { Document } from './fields/Document';
+import { Message } from './server/Message';
export class Utils {
+ public static DRAG_THRESHOLD = 4;
+
public static GenerateGuid(): string {
return v4();
}
@@ -87,13 +88,20 @@ export class Utils {
}
}
-export function OmitKeys(obj: any, keys: any, addKeyFunc?: (dup: any) => void) {
+export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } {
+ const omit: any = { ...obj };
+ const extract: any = {};
+ keys.forEach(key => {
+ extract[key] = omit[key];
+ delete omit[key];
+ });
+ addKeyFunc && addKeyFunc(omit);
+ return { omit, extract };
+}
+
+export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) {
var dup: any = {};
- for (var key in obj) {
- if (keys.indexOf(key) === -1) {
- dup[key] = obj[key];
- }
- }
+ keys.forEach(key => dup[key] = obj[key]);
addKeyFunc && addKeyFunc(dup);
return dup;
}
@@ -108,6 +116,18 @@ 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>>;
+
+export type Predicate<K, V> = (entry: [K, V]) => boolean;
-export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; \ No newline at end of file
+export function deepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
+ let deepCopy = new Map<K, V>();
+ let entries = source.entries(), next = entries.next();
+ while (!next.done) {
+ let entry = next.value;
+ if (!predicate || predicate(entry)) {
+ deepCopy.set(entry[0], entry[1]);
+ }
+ }
+ return deepCopy;
+} \ No newline at end of file
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
new file mode 100644
index 000000000..a288d394a
--- /dev/null
+++ b/src/client/DocServer.ts
@@ -0,0 +1,130 @@
+import * as OpenSocket from 'socket.io-client';
+import { MessageStore } from "./../server/Message";
+import { Opt } from '../new_fields/Doc';
+import { Utils } from '../Utils';
+import { SerializationHelper } from './util/SerializationHelper';
+import { RefField, HandleUpdate, Id } from '../new_fields/RefField';
+
+export namespace DocServer {
+ const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);
+ const GUID: string = Utils.GenerateGuid();
+
+ export function prepend(extension: string): string {
+ return window.location.origin + extension;
+ }
+
+ export function DeleteDatabase() {
+ Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ }
+
+ export async function GetRefField(id: string): Promise<Opt<RefField>> {
+ let cached = _cache[id];
+ if (cached === undefined) {
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => {
+ const field = SerializationHelper.Deserialize(fieldJson);
+ if (_cache[id] !== undefined && !(_cache[id] instanceof Promise)) {
+ id;
+ }
+ if (field !== undefined) {
+ _cache[id] = field;
+ } else {
+ delete _cache[id];
+ }
+ return field;
+ });
+ _cache[id] = prom;
+ return prom;
+ } else if (cached instanceof Promise) {
+ return cached;
+ } else {
+ return cached;
+ }
+ }
+
+ export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> {
+ const requestedIds: string[] = [];
+ const waitingIds: string[] = [];
+ const promises: Promise<Opt<RefField>>[] = [];
+ const map: { [id: string]: Opt<RefField> } = {};
+ for (const id of ids) {
+ const cached = _cache[id];
+ if (cached === undefined) {
+ requestedIds.push(id);
+ } else if (cached instanceof Promise) {
+ promises.push(cached);
+ waitingIds.push(id);
+ } else {
+ map[id] = cached;
+ }
+ }
+ const prom = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds).then(fields => {
+ const fieldMap: { [id: string]: RefField } = {};
+ for (const field of fields) {
+ if (field !== undefined) {
+ fieldMap[field.id] = SerializationHelper.Deserialize(field);
+ }
+ }
+ return fieldMap;
+ });
+ requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id]));
+ const fields = await prom;
+ requestedIds.forEach(id => {
+ const field = fields[id];
+ if (field !== undefined) {
+ _cache[id] = field;
+ } else {
+ delete _cache[id];
+ }
+ map[id] = field;
+ });
+ const otherFields = await Promise.all(promises);
+ waitingIds.forEach((id, index) => map[id] = otherFields[index]);
+ return map;
+ }
+
+ export function UpdateField(id: string, diff: any) {
+ if (id === updatingId) {
+ return;
+ }
+ Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ }
+
+ export function CreateField(field: RefField) {
+ _cache[field[Id]] = field;
+ const initialState = SerializationHelper.Serialize(field);
+ Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ }
+
+ let updatingId: string | undefined;
+ function respondToUpdate(diff: any) {
+ const id = diff.id;
+ if (id === undefined) {
+ return;
+ }
+ const field = _cache[id];
+ const update = (f: Opt<RefField>) => {
+ if (f === undefined) {
+ return;
+ }
+ const handler = f[HandleUpdate];
+ if (handler) {
+ updatingId = id;
+ handler.call(f, diff.diff);
+ updatingId = undefined;
+ }
+ };
+ if (field instanceof Promise) {
+ field.then(update);
+ } else {
+ update(field);
+ }
+ }
+
+ function connected() {
+ _socket.emit(MessageStore.Bar.Message, GUID);
+ }
+
+ Utils.AddServerHandler(_socket, MessageStore.Foo, connected);
+ Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4febfa7eb..63ba01b6a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,20 +1,6 @@
-import { AudioField } from "../../fields/AudioField";
-import { Document } from "../../fields/Document";
-import { Field, Opt } from "../../fields/Field";
-import { HtmlField } from "../../fields/HtmlField";
-import { ImageField } from "../../fields/ImageField";
-import { InkField, StrokeData } from "../../fields/InkField";
-import { Key } from "../../fields/Key";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
-import { PDFField } from "../../fields/PDFField";
-import { TextField } from "../../fields/TextField";
-import { VideoField } from "../../fields/VideoField";
-import { WebField } from "../../fields/WebField";
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
import { HistogramOperation } from "../northstar/operations/HistogramOperation";
-import { Server } from "../Server";
import { CollectionPDFView } from "../views/collections/CollectionPDFView";
import { CollectionVideoView } from "../views/collections/CollectionVideoView";
import { CollectionView } from "../views/collections/CollectionView";
@@ -32,37 +18,62 @@ 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 { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
+import { IconBox } from "../views/nodes/IconBox";
+import { Field, Doc, Opt } from "../../new_fields/Doc";
+import { OmitKeys } from "../../Utils";
+import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
+import { HtmlField } from "../../new_fields/HtmlField";
+import { List } from "../../new_fields/List";
+import { Cast } from "../../new_fields/Types";
+import { IconField } from "../../new_fields/IconField";
+import { listSpec } from "../../new_fields/Schema";
+import { DocServer } from "../DocServer";
+import { StrokeData, InkField } from "../../new_fields/InkField";
+import { dropActionType } from "../util/DragManager";
+import { DateField } from "../../new_fields/DateField";
export interface DocumentOptions {
x?: number;
y?: number;
- ink?: Map<string, StrokeData>;
+ ink?: InkField;
width?: number;
height?: number;
nativeWidth?: number;
nativeHeight?: number;
title?: string;
- panx?: number;
- pany?: number;
+ panX?: number;
+ panY?: number;
page?: number;
scale?: number;
+ baseLayout?: string;
layout?: string;
- layoutKeys?: Key[];
+ templates?: List<string>;
viewType?: number;
backgroundColor?: string;
- copyDraggedItems?: boolean;
+ dropAction?: dropActionType;
+ backgroundLayout?: string;
+ curPage?: number;
+ documentText?: string;
+ borderRounding?: number;
+ schemaColumns?: List<string>;
+ dockingConfig?: string;
+ // [key: string]: Opt<Field>;
}
+const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
-export namespace Documents {
- let textProto: Document;
- let histoProto: Document;
- let imageProto: Document;
- let webProto: Document;
- let collProto: Document;
- let kvpProto: Document;
- let videoProto: Document;
- let audioProto: Document;
- let pdfProto: Document;
+export namespace Docs {
+ let textProto: Doc;
+ let histoProto: Doc;
+ let imageProto: Doc;
+ let webProto: Doc;
+ let collProto: Doc;
+ let kvpProto: Doc;
+ let videoProto: Doc;
+ let audioProto: Doc;
+ let pdfProto: Doc;
+ let iconProto: Doc;
const textProtoId = "textProto";
const histoProtoId = "histoProto";
const pdfProtoId = "pdfProto";
@@ -72,117 +83,103 @@ 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 => {
- textProto = fields[textProtoId] as Document || CreateTextPrototype();
- histoProto = fields[histoProtoId] as Document || CreateHistogramPrototype();
- collProto = fields[collProtoId] as Document || CreateCollectionPrototype();
- imageProto = fields[imageProtoId] as Document || CreateImagePrototype();
- webProto = fields[webProtoId] as Document || CreateWebPrototype();
- kvpProto = fields[kvpProtoId] as Document || CreateKVPPrototype();
- videoProto = fields[videoProtoId] as Document || CreateVideoPrototype();
- audioProto = fields[audioProtoId] as Document || CreateAudioPrototype();
- pdfProto = fields[pdfProtoId] as Document || CreatePdfPrototype();
+ return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
+ textProto = fields[textProtoId] as Doc || CreateTextPrototype();
+ histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype();
+ collProto = fields[collProtoId] as Doc || CreateCollectionPrototype();
+ imageProto = fields[imageProtoId] as Doc || CreateImagePrototype();
+ webProto = fields[webProtoId] as Doc || CreateWebPrototype();
+ kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype();
+ videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype();
+ audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
+ pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
+ iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
});
}
- function assignOptions(doc: Document, options: DocumentOptions): Document {
- if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); }
- if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); }
- if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); }
- if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); }
- if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); }
- if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); }
- if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); }
- if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); }
- if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); }
- if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); }
- if (options.copyDraggedItems !== undefined) { doc.SetBoolean(KeyStore.CopyDraggedItems, options.copyDraggedItems); }
- return doc;
- }
- function assignToDelegate(doc: Document, options: DocumentOptions): Document {
- if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); }
- if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); }
- if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); }
- if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }
- if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); }
- if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); }
- return doc;
+ function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc {
+ return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout });
}
-
- function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document {
- return assignOptions(new Document(protoId), { ...options, title: title, layout: layout });
+ function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) {
+ const deleg = Doc.MakeDelegate(doc);
+ deleg.data = value;
+ return Doc.assign(deleg, options);
}
- function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: [T, { new(): U }] | Document, id?: string) {
- var deleg = doc.MakeDelegate(id);
- if (value instanceof Document) {
- deleg.Set(KeyStore.Data, value);
- }
- else {
- deleg.SetData(KeyStore.Data, value[0], value[1]);
- }
- return assignOptions(deleg, options);
+ function SetDelegateOptions<U extends Field>(doc: Doc, options: DocumentOptions) {
+ const deleg = Doc.MakeDelegate(doc);
+ return Doc.assign(deleg, options);
}
- function CreateImagePrototype(): Document {
- let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"),
- { 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);
+ function CreateImagePrototype(): Doc {
+ let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0 });
return imageProto;
}
- function CreateHistogramPrototype(): Document {
- let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
- histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString());
+ function CreateHistogramPrototype(): Doc {
+ let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
+ { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString() });
return histoProto;
}
- function CreateTextPrototype(): Document {
+ function CreateIconPrototype(): Doc {
+ let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(),
+ { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
+ return iconProto;
+ }
+ function CreateTextPrototype(): Doc {
let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150, backgroundColor: "#f1efeb" });
return textProto;
}
- function CreatePdfPrototype(): Document {
- let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 1200, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] });
- pdfProto.SetNumber(KeyStore.CurPage, 1);
- pdfProto.SetText(KeyStore.BackgroundLayout, PDFBox.LayoutString());
+ function CreatePdfPrototype(): Doc {
+ let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 1200, width: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 });
return pdfProto;
}
- function CreateWebPrototype(): Document {
+ function CreateWebPrototype(): Doc {
let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 300 });
return webProto;
}
- function CreateCollectionPrototype(): Document {
- let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"),
- { panx: 0, pany: 0, scale: 1, width: 500, height: 500, layoutKeys: [KeyStore.Data] });
+ function CreateCollectionPrototype(): Doc {
+ let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"),
+ { panX: 0, panY: 0, scale: 1, width: 500, height: 500 });
return collProto;
}
- function CreateKVPPrototype(): Document {
+ function CreateKVPPrototype(): Doc {
let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150 });
return kvpProto;
}
- function CreateVideoPrototype(): Document {
- let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("AnnotationsKey"),
- { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
- videoProto.SetNumber(KeyStore.CurPage, 0);
- videoProto.SetText(KeyStore.BackgroundLayout, VideoBox.LayoutString());
+ function CreateVideoPrototype(): Doc {
+ let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"),
+ { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0 });
return videoProto;
}
- function CreateAudioPrototype(): Document {
+ function CreateAudioPrototype(): Doc {
let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(),
- { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] });
+ { x: 0, y: 0, width: 300, height: 150 });
return audioProto;
}
+ function CreateInstance(proto: Doc, data: Field, options: DocumentOptions) {
+ const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
+ if (!("author" in protoProps)) {
+ protoProps.author = CurrentUserUtils.email;
+ }
+ if (!("creationDate" in protoProps)) {
+ protoProps.creationDate = new DateField;
+ }
+
+ return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps);
+ }
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(imageProto, options, [new URL(url), ImageField]).MakeDelegate(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
+ return CreateInstance(imageProto, new ImageField(new URL(url)), options);
// let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] },
// [new URL(url), ImageField]);
// doc.SetText(KeyStore.Caption, "my caption...");
@@ -191,76 +188,83 @@ export namespace Documents {
// return doc;
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(videoProto, options, [new URL(url), VideoField]), options);
+ return CreateInstance(videoProto, new VideoField(new URL(url)), options);
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(audioProto, options, [new URL(url), AudioField]), options);
+ return CreateInstance(audioProto, new AudioField(new URL(url)), options);
}
- export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}, id?: string, delegId?: string) {
- return assignToDelegate(SetInstanceOptions(histoProto, options, [histoOp, HistogramField], id).MakeDelegate(delegId), options);
+ export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
+ return CreateInstance(histoProto, new HistogramField(histoOp), options);
}
export function TextDocument(options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(textProto, options, ["", TextField]).MakeDelegate(), options);
+ return CreateInstance(textProto, "", options);
+ }
+ export function IconDocument(icon: string, options: DocumentOptions = {}) {
+ return CreateInstance(iconProto, new IconField(icon), options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(pdfProto, options, [new URL(url), PDFField]).MakeDelegate(), options);
+ return CreateInstance(pdfProto, new PdfField(new URL(url)), 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[]);
+ let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+ let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc));
+ if (!schemaDocuments) {
+ return;
+ }
+ const docs = schemaDocuments;
CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => {
- if (field instanceof Document) {
- schemaDocuments.push(field);
+ DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
+ docs.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"));
+ docs.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
}
}));
});
return schemaDoc;
}
- return Documents.TreeDocument([], { width: 50, height: 100, title: schemaName });
+ return Docs.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);
+ return CreateInstance(webProto, new WebField(new URL(url)), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
- return assignToDelegate(SetInstanceOptions(webProto, options, [html, HtmlField]).MakeDelegate(), options);
+ return CreateInstance(webProto, new HtmlField(html), options);
}
- export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) {
- return assignToDelegate(SetInstanceOptions(kvpProto, options, document, id), options);
+ export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+ return CreateInstance(kvpProto, document, options);
}
- export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string, makePrototype: boolean = true) {
+ export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) {
if (!makePrototype) {
- return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id);
+ return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents));
}
- return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, [documents, ListField], id).MakeDelegate(), options);
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
}
- export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Schema }, [documents, ListField], id), options);
+ export function SchemaDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Schema });
}
- export function TreeDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Tree }, [documents, ListField], id), options);
+ export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
}
- export function DockDocument(config: string, options: DocumentOptions, id?: string) {
- return assignToDelegate(SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Docking }, [config, TextField], id), options);
+ export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions) {
+ return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config });
}
- export function CaptionDocument(doc: Document) {
- const captionDoc = doc.CreateAlias();
- captionDoc.SetText(KeyStore.OverlayLayout, FixedCaption());
- captionDoc.SetNumber(KeyStore.Width, doc.GetNumber(KeyStore.Width, 0));
- captionDoc.SetNumber(KeyStore.Height, doc.GetNumber(KeyStore.Height, 0));
+ export function CaptionDocument(doc: Doc) {
+ const captionDoc = Doc.MakeAlias(doc);
+ captionDoc.overlayLayout = FixedCaption();
+ captionDoc.width = Cast(doc.width, "number", 0);
+ captionDoc.height = Cast(doc.height, "number", 0);
return captionDoc;
}
@@ -271,14 +275,14 @@ export namespace Documents {
+ ImageBox.LayoutString() +
`</div>
<div style="position:relative; height:15%; text-align:center; ">`
- + FormattedTextBox.LayoutString("CaptionKey") +
+ + FormattedTextBox.LayoutString("caption") +
`</div>
</div>`;
}
- export function FixedCaption(fieldName: string = "Caption") {
+ export function FixedCaption(fieldName: string = "caption") {
return `<div style="position:absolute; height:30px; bottom:0; width:100%">
<div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`
- + FormattedTextBox.LayoutString(fieldName + "Key") +
+ + FormattedTextBox.LayoutString(fieldName) +
`</div>
</div>`;
}
@@ -290,7 +294,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:(100% + 25px); width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
@@ -302,7 +306,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:25px; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
@@ -325,7 +329,7 @@ export namespace Documents {
{layout}
</div>
<div style="height:15%; width:100%; position:absolute">
- <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"CaptionKey"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+ <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
</div>
</div>
`);
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
new file mode 100644
index 000000000..56a71f1ac
--- /dev/null
+++ b/src/client/goldenLayout.js
@@ -0,0 +1,5359 @@
+(function ($) {
+ var lm = { "config": {}, "container": {}, "controls": {}, "errors": {}, "items": {}, "utils": {} };
+ lm.utils.F = function () {
+ };
+
+ lm.utils.extend = function (subClass, superClass) {
+ subClass.prototype = lm.utils.createObject(superClass.prototype);
+ subClass.prototype.contructor = subClass;
+ };
+
+ lm.utils.createObject = function (prototype) {
+ if (typeof Object.create === 'function') {
+ return Object.create(prototype);
+ } else {
+ lm.utils.F.prototype = prototype;
+ return new lm.utils.F();
+ }
+ };
+
+ lm.utils.objectKeys = function (object) {
+ var keys, key;
+
+ if (typeof Object.keys === 'function') {
+ return Object.keys(object);
+ } else {
+ keys = [];
+ for (key in object) {
+ keys.push(key);
+ }
+ return keys;
+ }
+ };
+
+ lm.utils.getHashValue = function (key) {
+ var matches = location.hash.match(new RegExp(key + '=([^&]*)'));
+ return matches ? matches[1] : null;
+ };
+
+ lm.utils.getQueryStringParam = function (param) {
+ if (window.location.hash) {
+ return lm.utils.getHashValue(param);
+ } else if (!window.location.search) {
+ return null;
+ }
+
+ var keyValuePairs = window.location.search.substr(1).split('&'),
+ params = {},
+ pair,
+ i;
+
+ for (i = 0; i < keyValuePairs.length; i++) {
+ pair = keyValuePairs[i].split('=');
+ params[pair[0]] = pair[1];
+ }
+
+ return params[param] || null;
+ };
+
+ lm.utils.copy = function (target, source) {
+ for (var key in source) {
+ target[key] = source[key];
+ }
+ return target;
+ };
+
+ /**
+ * This is based on Paul Irish's shim, but looks quite odd in comparison. Why?
+ * Because
+ * a) it shouldn't affect the global requestAnimationFrame function
+ * b) it shouldn't pass on the time that has passed
+ *
+ * @param {Function} fn
+ *
+ * @returns {void}
+ */
+ lm.utils.animFrame = function (fn) {
+ return (window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ function (callback) {
+ window.setTimeout(callback, 1000 / 60);
+ })(function () {
+ fn();
+ });
+ };
+
+ lm.utils.indexOf = function (needle, haystack) {
+ if (!(haystack instanceof Array)) {
+ throw new Error('Haystack is not an Array');
+ }
+
+ if (haystack.indexOf) {
+ return haystack.indexOf(needle);
+ } else {
+ for (var i = 0; i < haystack.length; i++) {
+ if (haystack[i] === needle) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ };
+
+ if (typeof /./ != 'function' && typeof Int8Array != 'object') {
+ lm.utils.isFunction = function (obj) {
+ return typeof obj == 'function' || false;
+ };
+ } else {
+ lm.utils.isFunction = function (obj) {
+ return toString.call(obj) === '[object Function]';
+ };
+ }
+
+ lm.utils.fnBind = function (fn, context, boundArgs) {
+
+ if (Function.prototype.bind !== undefined) {
+ return Function.prototype.bind.apply(fn, [context].concat(boundArgs || []));
+ }
+
+ var bound = function () {
+
+ // Join the already applied arguments to the now called ones (after converting to an array again).
+ var args = (boundArgs || []).concat(Array.prototype.slice.call(arguments, 0));
+
+ // If not being called as a constructor
+ if (!(this instanceof bound)) {
+ // return the result of the function called bound to target and partially applied.
+ return fn.apply(context, args);
+ }
+ // If being called as a constructor, apply the function bound to self.
+ fn.apply(this, args);
+ };
+ // Attach the prototype of the function to our newly created function.
+ bound.prototype = fn.prototype;
+ return bound;
+ };
+
+ lm.utils.removeFromArray = function (item, array) {
+ var index = lm.utils.indexOf(item, array);
+
+ if (index === -1) {
+ throw new Error('Can\'t remove item from array. Item is not in the array');
+ }
+
+ array.splice(index, 1);
+ };
+
+ lm.utils.now = function () {
+ if (typeof Date.now === 'function') {
+ return Date.now();
+ } else {
+ return (new Date()).getTime();
+ }
+ };
+
+ lm.utils.getUniqueId = function () {
+ return (Math.random() * 1000000000000000)
+ .toString(36)
+ .replace('.', '');
+ };
+
+ /**
+ * A basic XSS filter. It is ultimately up to the
+ * implementing developer to make sure their particular
+ * applications and usecases are save from cross site scripting attacks
+ *
+ * @param {String} input
+ * @param {Boolean} keepTags
+ *
+ * @returns {String} filtered input
+ */
+ lm.utils.filterXss = function (input, keepTags) {
+
+ var output = input
+ .replace(/javascript/gi, 'j&#97;vascript')
+ .replace(/expression/gi, 'expr&#101;ssion')
+ .replace(/onload/gi, 'onlo&#97;d')
+ .replace(/script/gi, '&#115;cript')
+ .replace(/onerror/gi, 'on&#101;rror');
+
+ if (keepTags === true) {
+ return output;
+ } else {
+ return output
+ .replace(/>/g, '&gt;')
+ .replace(/</g, '&lt;');
+ }
+ };
+
+ /**
+ * Removes html tags from a string
+ *
+ * @param {String} input
+ *
+ * @returns {String} input without tags
+ */
+ lm.utils.stripTags = function (input) {
+ return $.trim(input.replace(/(<([^>]+)>)/ig, ''));
+ };
+ /**
+ * A generic and very fast EventEmitter
+ * implementation. On top of emitting the
+ * actual event it emits an
+ *
+ * lm.utils.EventEmitter.ALL_EVENT
+ *
+ * event for every event triggered. This allows
+ * to hook into it and proxy events forwards
+ *
+ * @constructor
+ */
+ lm.utils.EventEmitter = function () {
+ this._mSubscriptions = {};
+ this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT] = [];
+
+ /**
+ * Listen for events
+ *
+ * @param {String} sEvent The name of the event to listen to
+ * @param {Function} fCallback The callback to execute when the event occurs
+ * @param {[Object]} oContext The value of the this pointer within the callback function
+ *
+ * @returns {void}
+ */
+ this.on = function (sEvent, fCallback, oContext) {
+ if (!lm.utils.isFunction(fCallback)) {
+ throw new Error('Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback);
+ }
+
+ if (!this._mSubscriptions[sEvent]) {
+ this._mSubscriptions[sEvent] = [];
+ }
+
+ this._mSubscriptions[sEvent].push({ fn: fCallback, ctx: oContext });
+ };
+
+ /**
+ * Emit an event and notify listeners
+ *
+ * @param {String} sEvent The name of the event
+ * @param {Mixed} various additional arguments that will be passed to the listener
+ *
+ * @returns {void}
+ */
+ this.emit = function (sEvent) {
+ var i, ctx, args;
+
+ args = Array.prototype.slice.call(arguments, 1);
+
+ var subs = this._mSubscriptions[sEvent];
+
+ if (subs) {
+ subs = subs.slice();
+ for (i = 0; i < subs.length; i++) {
+ ctx = subs[i].ctx || {};
+ subs[i].fn.apply(ctx, args);
+ }
+ }
+
+ args.unshift(sEvent);
+
+ var allEventSubs = this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT].slice()
+
+ for (i = 0; i < allEventSubs.length; i++) {
+ ctx = allEventSubs[i].ctx || {};
+ allEventSubs[i].fn.apply(ctx, args);
+ }
+ };
+
+ /**
+ * Removes a listener for an event, or all listeners if no callback and context is provided.
+ *
+ * @param {String} sEvent The name of the event
+ * @param {Function} fCallback The previously registered callback method (optional)
+ * @param {Object} oContext The previously registered context (optional)
+ *
+ * @returns {void}
+ */
+ this.unbind = function (sEvent, fCallback, oContext) {
+ if (!this._mSubscriptions[sEvent]) {
+ throw new Error('No subscribtions to unsubscribe for event ' + sEvent);
+ }
+
+ var i, bUnbound = false;
+
+ for (i = 0; i < this._mSubscriptions[sEvent].length; i++) {
+ if
+ (
+ (!fCallback || this._mSubscriptions[sEvent][i].fn === fCallback) &&
+ (!oContext || oContext === this._mSubscriptions[sEvent][i].ctx)
+ ) {
+ this._mSubscriptions[sEvent].splice(i, 1);
+ bUnbound = true;
+ }
+ }
+
+ if (bUnbound === false) {
+ throw new Error('Nothing to unbind for ' + sEvent);
+ }
+ };
+
+ /**
+ * Alias for unbind
+ */
+ this.off = this.unbind;
+
+ /**
+ * Alias for emit
+ */
+ this.trigger = this.emit;
+ };
+
+ /**
+ * The name of the event that's triggered for every other event
+ *
+ * usage
+ *
+ * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){
+ * //do stuff
+ * });
+ *
+ * @type {String}
+ */
+ lm.utils.EventEmitter.ALL_EVENT = '__all';
+ lm.utils.DragListener = function (eElement, nButtonCode) {
+ lm.utils.EventEmitter.call(this);
+
+ this._eElement = $(eElement);
+ this._oDocument = $(document);
+ this._eBody = $(document.body);
+ this._nButtonCode = nButtonCode || 0;
+
+ /**
+ * The delay after which to start the drag in milliseconds
+ */
+ this._nDelay = 200;
+
+ /**
+ * The distance the mouse needs to be moved to qualify as a drag
+ */
+ this._nDistance = 10;//TODO - works better with delay only
+
+ this._nX = 0;
+ this._nY = 0;
+
+ this._nOriginalX = 0;
+ this._nOriginalY = 0;
+
+ this._bDragging = false;
+
+ this._fMove = lm.utils.fnBind(this.onMouseMove, this);
+ this._fUp = lm.utils.fnBind(this.onMouseUp, this);
+ this._fDown = lm.utils.fnBind(this.onMouseDown, this);
+
+
+ this._eElement.on('mousedown touchstart', this._fDown);
+ };
+
+ lm.utils.DragListener.timeout = null;
+
+ lm.utils.copy(lm.utils.DragListener.prototype, {
+ destroy: function () {
+ this._eElement.unbind('mousedown touchstart', this._fDown);
+ this._oDocument.unbind('mouseup touchend', this._fUp);
+ this._eElement = null;
+ this._oDocument = null;
+ this._eBody = null;
+ },
+
+ onMouseDown: function (oEvent) {
+ oEvent.preventDefault();
+
+ if (oEvent.button == 0 || oEvent.type === "touchstart") {
+ var coordinates = this._getCoordinates(oEvent);
+
+ this._nOriginalX = coordinates.x;
+ this._nOriginalY = coordinates.y;
+
+ this._oDocument.on('mousemove touchmove', this._fMove);
+ this._oDocument.one('mouseup touchend', this._fUp);
+
+ this._timeout = setTimeout(lm.utils.fnBind(this._startDrag, this), this._nDelay);
+ }
+ },
+
+ onMouseMove: function (oEvent) {
+ if (this._timeout != null) {
+ oEvent.preventDefault();
+
+ var coordinates = this._getCoordinates(oEvent);
+
+ this._nX = coordinates.x - this._nOriginalX;
+ this._nY = coordinates.y - this._nOriginalY;
+
+ if (this._bDragging === false) {
+ if (
+ Math.abs(this._nX) > this._nDistance ||
+ Math.abs(this._nY) > this._nDistance
+ ) {
+ clearTimeout(this._timeout);
+ this._startDrag();
+ }
+ }
+
+ if (this._bDragging) {
+ this.emit('drag', this._nX, this._nY, oEvent);
+ }
+ }
+ },
+
+ onMouseUp: function (oEvent) {
+ if (this._timeout != null) {
+ clearTimeout(this._timeout);
+ this._eBody.removeClass('lm_dragging');
+ this._eElement.removeClass('lm_dragging');
+ this._oDocument.find('iframe').css('pointer-events', '');
+ this._oDocument.unbind('mousemove touchmove', this._fMove);
+ this._oDocument.unbind('mouseup touchend', this._fUp);
+
+ if (this._bDragging === true) {
+ this._bDragging = false;
+ this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
+ }
+ }
+ },
+
+ _startDrag: function () {
+ this._bDragging = true;
+ this._eBody.addClass('lm_dragging');
+ this._eElement.addClass('lm_dragging');
+ this._oDocument.find('iframe').css('pointer-events', 'none');
+ this.emit('dragStart', this._nOriginalX, this._nOriginalY);
+ },
+
+ _getCoordinates: function (event) {
+ event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+ return {
+ x: event.pageX,
+ y: event.pageY
+ };
+ }
+ });
+ /**
+ * The main class that will be exposed as GoldenLayout.
+ *
+ * @public
+ * @constructor
+ * @param {GoldenLayout config} config
+ * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body
+ *
+ * @returns {VOID}
+ */
+ lm.LayoutManager = function (config, container) {
+
+ if (!$ || typeof $.noConflict !== 'function') {
+ var errorMsg = 'jQuery is missing as dependency for GoldenLayout. ';
+ errorMsg += 'Please either expose $ on GoldenLayout\'s scope (e.g. window) or add "jquery" to ';
+ errorMsg += 'your paths when using RequireJS/AMD';
+ throw new Error(errorMsg);
+ }
+ lm.utils.EventEmitter.call(this);
+
+ this.isInitialised = false;
+ this._isFullPage = false;
+ this._resizeTimeoutId = null;
+ this._components = { 'lm-react-component': lm.utils.ReactComponentHandler };
+ this._itemAreas = [];
+ this._resizeFunction = lm.utils.fnBind(this._onResize, this);
+ this._unloadFunction = lm.utils.fnBind(this._onUnload, this);
+ this._maximisedItem = null;
+ this._maximisePlaceholder = $('<div class="lm_maximise_place"></div>');
+ this._creationTimeoutPassed = false;
+ this._subWindowsCreated = false;
+ this._dragSources = [];
+ this._updatingColumnsResponsive = false;
+ this._firstLoad = true;
+
+ this.width = null;
+ this.height = null;
+ this.root = null;
+ this.openPopouts = [];
+ this.selectedItem = null;
+ this.isSubWindow = false;
+ this.eventHub = new lm.utils.EventHub(this);
+ this.config = this._createConfig(config);
+ this.container = container;
+ this.dropTargetIndicator = null;
+ this.transitionIndicator = null;
+ this.tabDropPlaceholder = $('<div class="lm_drop_tab_placeholder"></div>');
+
+ if (this.isSubWindow === true) {
+ $('body').css('visibility', 'hidden');
+ }
+
+ this._typeToItem = {
+ 'column': lm.utils.fnBind(lm.items.RowOrColumn, this, [true]),
+ 'row': lm.utils.fnBind(lm.items.RowOrColumn, this, [false]),
+ 'stack': lm.items.Stack,
+ 'component': lm.items.Component
+ };
+ };
+
+ /**
+ * Hook that allows to access private classes
+ */
+ lm.LayoutManager.__lm = lm;
+
+ /**
+ * Takes a GoldenLayout configuration object and
+ * replaces its keys and values recursively with
+ * one letter codes
+ *
+ * @static
+ * @public
+ * @param {Object} config A GoldenLayout config object
+ *
+ * @returns {Object} minified config
+ */
+ lm.LayoutManager.minifyConfig = function (config) {
+ return (new lm.utils.ConfigMinifier()).minifyConfig(config);
+ };
+
+ /**
+ * Takes a configuration Object that was previously minified
+ * using minifyConfig and returns its original version
+ *
+ * @static
+ * @public
+ * @param {Object} minifiedConfig
+ *
+ * @returns {Object} the original configuration
+ */
+ lm.LayoutManager.unminifyConfig = function (config) {
+ return (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+ };
+
+ lm.utils.copy(lm.LayoutManager.prototype, {
+
+ /**
+ * Register a component with the layout manager. If a configuration node
+ * of type component is reached it will look up componentName and create the
+ * associated component
+ *
+ * {
+ * type: "component",
+ * componentName: "EquityNewsFeed",
+ * componentState: { "feedTopic": "us-bluechips" }
+ * }
+ *
+ * @public
+ * @param {String} name
+ * @param {Function} constructor
+ *
+ * @returns {void}
+ */
+ registerComponent: function (name, constructor) {
+ if (typeof constructor !== 'function') {
+ throw new Error('Please register a constructor function');
+ }
+
+ if (this._components[name] !== undefined) {
+ throw new Error('Component ' + name + ' is already registered');
+ }
+
+ this._components[name] = constructor;
+ },
+
+ /**
+ * Creates a layout configuration object based on the the current state
+ *
+ * @public
+ * @returns {Object} GoldenLayout configuration
+ */
+ toConfig: function (root) {
+ var config, next, i;
+
+ if (this.isInitialised === false) {
+ throw new Error('Can\'t create config, layout not yet initialised');
+ }
+
+ if (root && !(root instanceof lm.items.AbstractContentItem)) {
+ throw new Error('Root must be a ContentItem');
+ }
+
+ /*
+ * settings & labels
+ */
+ config = {
+ settings: lm.utils.copy({}, this.config.settings),
+ dimensions: lm.utils.copy({}, this.config.dimensions),
+ labels: lm.utils.copy({}, this.config.labels)
+ };
+
+ /*
+ * Content
+ */
+ config.content = [];
+ next = function (configNode, item) {
+ var key, i;
+
+ for (key in item.config) {
+ if (key !== 'content') {
+ configNode[key] = item.config[key];
+ }
+ }
+
+ if (item.contentItems.length) {
+ configNode.content = [];
+
+ for (i = 0; i < item.contentItems.length; i++) {
+ configNode.content[i] = {};
+ next(configNode.content[i], item.contentItems[i]);
+ }
+ }
+ };
+
+ if (root) {
+ next(config, { contentItems: [root] });
+ } else {
+ next(config, this.root);
+ }
+
+ /*
+ * Retrieve config for subwindows
+ */
+ this._$reconcilePopoutWindows();
+ config.openPopouts = [];
+ for (i = 0; i < this.openPopouts.length; i++) {
+ config.openPopouts.push(this.openPopouts[i].toConfig());
+ }
+
+ /*
+ * Add maximised item
+ */
+ config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null;
+ return config;
+ },
+
+ /**
+ * Returns a previously registered component
+ *
+ * @public
+ * @param {String} name The name used
+ *
+ * @returns {Function}
+ */
+ getComponent: function (name) {
+ if (this._components[name] === undefined) {
+ throw new lm.errors.ConfigurationError('Unknown component "' + name + '"');
+ }
+
+ return this._components[name];
+ },
+
+ /**
+ * Creates the actual layout. Must be called after all initial components
+ * are registered. Recurses through the configuration and sets up
+ * the item tree.
+ *
+ * If called before the document is ready it adds itself as a listener
+ * to the document.ready event
+ *
+ * @public
+ *
+ * @returns {void}
+ */
+ init: function () {
+
+ /**
+ * Create the popout windows straight away. If popouts are blocked
+ * an error is thrown on the same 'thread' rather than a timeout and can
+ * be caught. This also prevents any further initilisation from taking place.
+ */
+ if (this._subWindowsCreated === false) {
+ this._createSubWindows();
+ this._subWindowsCreated = true;
+ }
+
+
+ /**
+ * If the document isn't ready yet, wait for it.
+ */
+ if (document.readyState === 'loading' || document.body === null) {
+ $(document).ready(lm.utils.fnBind(this.init, this));
+ return;
+ }
+
+ /**
+ * If this is a subwindow, wait a few milliseconds for the original
+ * page's js calls to be executed, then replace the bodies content
+ * with GoldenLayout
+ */
+ if (this.isSubWindow === true && this._creationTimeoutPassed === false) {
+ setTimeout(lm.utils.fnBind(this.init, this), 7);
+ this._creationTimeoutPassed = true;
+ return;
+ }
+
+ if (this.isSubWindow === true) {
+ this._adjustToWindowMode();
+ }
+
+ this._setContainer();
+ this.dropTargetIndicator = new lm.controls.DropTargetIndicator(this.container);
+ this.transitionIndicator = new lm.controls.TransitionIndicator();
+ this.updateSize();
+ this._create(this.config);
+ this._bindEvents();
+ this.isInitialised = true;
+ this._adjustColumnsResponsive();
+ this.emit('initialised');
+ },
+
+ /**
+ * Updates the layout managers size
+ *
+ * @public
+ * @param {[int]} width height in pixels
+ * @param {[int]} height width in pixels
+ *
+ * @returns {void}
+ */
+ updateSize: function (width, height) {
+ if (arguments.length === 2) {
+ this.width = width;
+ this.height = height;
+ } else {
+ this.width = this.container.width();
+ this.height = this.container.height();
+ }
+
+ if (this.isInitialised === true) {
+ this.root.callDownwards('setSize', [this.width, this.height]);
+
+ if (this._maximisedItem) {
+ this._maximisedItem.element.width(this.container.width());
+ this._maximisedItem.element.height(this.container.height());
+ this._maximisedItem.callDownwards('setSize');
+ }
+
+ this._adjustColumnsResponsive();
+ }
+ },
+
+ /**
+ * Destroys the LayoutManager instance itself as well as every ContentItem
+ * within it. After this is called nothing should be left of the LayoutManager.
+ *
+ * @public
+ * @returns {void}
+ */
+ destroy: function () {
+ if (this.isInitialised === false) {
+ return;
+ }
+ this._onUnload();
+ $(window).off('resize', this._resizeFunction);
+ $(window).off('unload beforeunload', this._unloadFunction);
+ this.root.callDownwards('_$destroy', [], true);
+ this.root.contentItems = [];
+ this.tabDropPlaceholder.remove();
+ this.dropTargetIndicator.destroy();
+ this.transitionIndicator.destroy();
+ this.eventHub.destroy();
+
+ this._dragSources.forEach(function (dragSource) {
+ dragSource._dragListener.destroy();
+ dragSource._element = null;
+ dragSource._itemConfig = null;
+ dragSource._dragListener = null;
+ });
+ this._dragSources = [];
+ },
+
+ /**
+ * Recursively creates new item tree structures based on a provided
+ * ItemConfiguration object
+ *
+ * @public
+ * @param {Object} config ItemConfig
+ * @param {[ContentItem]} parent The item the newly created item should be a child of
+ *
+ * @returns {lm.items.ContentItem}
+ */
+ createContentItem: function (config, parent) {
+ var typeErrorMsg, contentItem;
+
+ if (typeof config.type !== 'string') {
+ throw new lm.errors.ConfigurationError('Missing parameter \'type\'', config);
+ }
+
+ if (config.type === 'react-component') {
+ config.type = 'component';
+ config.componentName = 'lm-react-component';
+ }
+
+ if (!this._typeToItem[config.type]) {
+ typeErrorMsg = 'Unknown type \'' + config.type + '\'. ' +
+ 'Valid types are ' + lm.utils.objectKeys(this._typeToItem).join(',');
+
+ throw new lm.errors.ConfigurationError(typeErrorMsg);
+ }
+
+
+ /**
+ * We add an additional stack around every component that's not within a stack anyways.
+ */
+ if (
+ // If this is a component
+ config.type === 'component' &&
+
+ // and it's not already within a stack
+ !(parent instanceof lm.items.Stack) &&
+
+ // and we have a parent
+ !!parent &&
+
+ // and it's not the topmost item in a new window
+ !(this.isSubWindow === true && parent instanceof lm.items.Root)
+ ) {
+ config = {
+ type: 'stack',
+ width: config.width,
+ height: config.height,
+ content: [config]
+ };
+ }
+
+ contentItem = new this._typeToItem[config.type](this, config, parent);
+ return contentItem;
+ },
+
+ /**
+ * Creates a popout window with the specified content and dimensions
+ *
+ * @param {Object|lm.itemsAbstractContentItem} configOrContentItem
+ * @param {[Object]} dimensions A map with width, height, left and top
+ * @param {[String]} parentId the id of the element this item will be appended to
+ * when popIn is called
+ * @param {[Number]} indexInParent The position of this item within its parent element
+
+ * @returns {lm.controls.BrowserPopout}
+ */
+ createPopout: function (configOrContentItem, dimensions, parentId, indexInParent) {
+ var config = configOrContentItem,
+ isItem = configOrContentItem instanceof lm.items.AbstractContentItem,
+ self = this,
+ windowLeft,
+ windowTop,
+ offset,
+ parent,
+ child,
+ browserPopout;
+
+ parentId = parentId || null;
+
+ if (isItem) {
+ config = this.toConfig(configOrContentItem).content;
+ parentId = lm.utils.getUniqueId();
+
+ /**
+ * If the item is the only component within a stack or for some
+ * other reason the only child of its parent the parent will be destroyed
+ * when the child is removed.
+ *
+ * In order to support this we move up the tree until we find something
+ * that will remain after the item is being popped out
+ */
+ parent = configOrContentItem.parent;
+ child = configOrContentItem;
+ while (parent.contentItems.length === 1 && !parent.isRoot) {
+ parent = parent.parent;
+ child = child.parent;
+ }
+
+ parent.addId(parentId);
+ if (isNaN(indexInParent)) {
+ indexInParent = lm.utils.indexOf(child, parent.contentItems);
+ }
+ } else {
+ if (!(config instanceof Array)) {
+ config = [config];
+ }
+ }
+
+
+ if (!dimensions && isItem) {
+ windowLeft = window.screenX || window.screenLeft;
+ windowTop = window.screenY || window.screenTop;
+ offset = configOrContentItem.element.offset();
+
+ dimensions = {
+ left: windowLeft + offset.left,
+ top: windowTop + offset.top,
+ width: configOrContentItem.element.width(),
+ height: configOrContentItem.element.height()
+ };
+ }
+
+ if (!dimensions && !isItem) {
+ dimensions = {
+ left: window.screenX || window.screenLeft + 20,
+ top: window.screenY || window.screenTop + 20,
+ width: 500,
+ height: 309
+ };
+ }
+
+ if (isItem) {
+ configOrContentItem.remove();
+ }
+
+ browserPopout = new lm.controls.BrowserPopout(config, dimensions, parentId, indexInParent, this);
+
+ browserPopout.on('initialised', function () {
+ self.emit('windowOpened', browserPopout);
+ });
+
+ browserPopout.on('closed', function () {
+ self._$reconcilePopoutWindows();
+ });
+
+ this.openPopouts.push(browserPopout);
+
+ return browserPopout;
+ },
+
+ /**
+ * Attaches DragListener to any given DOM element
+ * and turns it into a way of creating new ContentItems
+ * by 'dragging' the DOM element into the layout
+ *
+ * @param {jQuery DOM element} element
+ * @param {Object|Function} itemConfig for the new item to be created, or a function which will provide it
+ *
+ * @returns {void}
+ */
+ createDragSource: function (element, itemConfig) {
+ this.config.settings.constrainDragToContainer = false;
+ var dragSource = new lm.controls.DragSource($(element), itemConfig, this);
+ this._dragSources.push(dragSource);
+
+ return dragSource;
+ },
+
+ /**
+ * Programmatically selects an item. This deselects
+ * the currently selected item, selects the specified item
+ * and emits a selectionChanged event
+ *
+ * @param {lm.item.AbstractContentItem} item#
+ * @param {[Boolean]} _$silent Wheather to notify the item of its selection
+ * @event selectionChanged
+ *
+ * @returns {VOID}
+ */
+ selectItem: function (item, _$silent) {
+
+ if (this.config.settings.selectionEnabled !== true) {
+ throw new Error('Please set selectionEnabled to true to use this feature');
+ }
+
+ if (item === this.selectedItem) {
+ return;
+ }
+
+ if (this.selectedItem !== null) {
+ this.selectedItem.deselect();
+ }
+
+ if (item && _$silent !== true) {
+ item.select();
+ }
+
+ this.selectedItem = item;
+
+ this.emit('selectionChanged', item);
+ },
+
+ /*************************
+ * PACKAGE PRIVATE
+ *************************/
+ _$maximiseItem: function (contentItem) {
+ if (this._maximisedItem !== null) {
+ this._$minimiseItem(this._maximisedItem);
+ }
+ this._maximisedItem = contentItem;
+ this._maximisedItem.addId('__glMaximised');
+ contentItem.element.addClass('lm_maximised');
+ contentItem.element.after(this._maximisePlaceholder);
+ this.root.element.prepend(contentItem.element);
+ contentItem.element.width(this.container.width());
+ contentItem.element.height(this.container.height());
+ contentItem.callDownwards('setSize');
+ this._maximisedItem.emit('maximised');
+ this.emit('stateChanged');
+ },
+
+ _$minimiseItem: function (contentItem) {
+ contentItem.element.removeClass('lm_maximised');
+ contentItem.removeId('__glMaximised');
+ this._maximisePlaceholder.after(contentItem.element);
+ this._maximisePlaceholder.remove();
+ contentItem.parent.callDownwards('setSize');
+ this._maximisedItem = null;
+ contentItem.emit('minimised');
+ this.emit('stateChanged');
+ },
+
+ /**
+ * This method is used to get around sandboxed iframe restrictions.
+ * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute
+ * (as is the case with codepens) the parent window is forbidden from calling certain
+ * methods on the child, such as window.close() or setting document.location.href.
+ *
+ * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call
+ * _$closeWindow on the child window's gl instance which (after a timeout to disconnect
+ * the invoking method from the close call) closes itself.
+ *
+ * @packagePrivate
+ *
+ * @returns {void}
+ */
+ _$closeWindow: function () {
+ window.setTimeout(function () {
+ window.close();
+ }, 1);
+ },
+
+ _$getArea: function (x, y) {
+ var i, area, smallestSurface = Infinity, mathingArea = null;
+
+ for (i = 0; i < this._itemAreas.length; i++) {
+ area = this._itemAreas[i];
+
+ if (
+ x > area.x1 &&
+ x < area.x2 &&
+ y > area.y1 &&
+ y < area.y2 &&
+ smallestSurface > area.surface
+ ) {
+ smallestSurface = area.surface;
+ mathingArea = area;
+ }
+ }
+
+ return mathingArea;
+ },
+
+ _$createRootItemAreas: function () {
+ var areaSize = 50;
+ var sides = { y2: 0, x2: 0, y1: 'y2', x1: 'x2' };
+ for (var side in sides) {
+ var area = this.root._$getArea();
+ area.side = side;
+ if (sides[side])
+ area[side] = area[sides[side]] - areaSize;
+ else
+ area[side] = areaSize;
+ area.surface = (area.x2 - area.x1) * (area.y2 - area.y1);
+ this._itemAreas.push(area);
+ }
+ },
+
+ _$calculateItemAreas: function () {
+ var i, area, allContentItems = this._getAllContentItems();
+ this._itemAreas = [];
+
+ /**
+ * If the last item is dragged out, highlight the entire container size to
+ * allow to re-drop it. allContentItems[ 0 ] === this.root at this point
+ *
+ * Don't include root into the possible drop areas though otherwise since it
+ * will used for every gap in the layout, e.g. splitters
+ */
+ if (allContentItems.length === 1) {
+ this._itemAreas.push(this.root._$getArea());
+ return;
+ }
+ this._$createRootItemAreas();
+
+ for (i = 0; i < allContentItems.length; i++) {
+
+ if (!(allContentItems[i].isStack)) {
+ continue;
+ }
+
+ area = allContentItems[i]._$getArea();
+
+ if (area === null) {
+ continue;
+ } else if (area instanceof Array) {
+ this._itemAreas = this._itemAreas.concat(area);
+ } else {
+ this._itemAreas.push(area);
+ var header = {};
+ lm.utils.copy(header, area);
+ lm.utils.copy(header, area.contentItem._contentAreaDimensions.header.highlightArea);
+ header.surface = (header.x2 - header.x1) * (header.y2 - header.y1);
+ this._itemAreas.push(header);
+ }
+ }
+ },
+
+ /**
+ * Takes a contentItem or a configuration and optionally a parent
+ * item and returns an initialised instance of the contentItem.
+ * If the contentItem is a function, it is first called
+ *
+ * @packagePrivate
+ *
+ * @param {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig
+ * @param {lm.items.AbtractContentItem} parent Only necessary when passing in config
+ *
+ * @returns {lm.items.AbtractContentItem}
+ */
+ _$normalizeContentItem: function (contentItemOrConfig, parent) {
+ if (!contentItemOrConfig) {
+ throw new Error('No content item defined');
+ }
+
+ if (lm.utils.isFunction(contentItemOrConfig)) {
+ contentItemOrConfig = contentItemOrConfig();
+ }
+
+ if (contentItemOrConfig instanceof lm.items.AbstractContentItem) {
+ return contentItemOrConfig;
+ }
+
+ if ($.isPlainObject(contentItemOrConfig) && contentItemOrConfig.type) {
+ var newContentItem = this.createContentItem(contentItemOrConfig, parent);
+ newContentItem.callDownwards('_$init');
+ return newContentItem;
+ } else {
+ throw new Error('Invalid contentItem');
+ }
+ },
+
+ /**
+ * Iterates through the array of open popout windows and removes the ones
+ * that are effectively closed. This is necessary due to the lack of reliably
+ * listening for window.close / unload events in a cross browser compatible fashion.
+ *
+ * @packagePrivate
+ *
+ * @returns {void}
+ */
+ _$reconcilePopoutWindows: function () {
+ var openPopouts = [], i;
+
+ for (i = 0; i < this.openPopouts.length; i++) {
+ if (this.openPopouts[i].getWindow().closed === false) {
+ openPopouts.push(this.openPopouts[i]);
+ } else {
+ this.emit('windowClosed', this.openPopouts[i]);
+ }
+ }
+
+ if (this.openPopouts.length !== openPopouts.length) {
+ this.emit('stateChanged');
+ this.openPopouts = openPopouts;
+ }
+
+ },
+
+ /***************************
+ * PRIVATE
+ ***************************/
+ /**
+ * Returns a flattened array of all content items,
+ * regardles of level or type
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _getAllContentItems: function () {
+ var allContentItems = [];
+
+ var addChildren = function (contentItem) {
+ allContentItems.push(contentItem);
+
+ if (contentItem.contentItems instanceof Array) {
+ for (var i = 0; i < contentItem.contentItems.length; i++) {
+ addChildren(contentItem.contentItems[i]);
+ }
+ }
+ };
+
+ addChildren(this.root);
+
+ return allContentItems;
+ },
+
+ /**
+ * Binds to DOM/BOM events on init
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _bindEvents: function () {
+ if (this._isFullPage) {
+ $(window).resize(this._resizeFunction);
+ }
+ $(window).on('unload beforeunload', this._unloadFunction);
+ },
+
+ /**
+ * Debounces resize events
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onResize: function () {
+ clearTimeout(this._resizeTimeoutId);
+ this._resizeTimeoutId = setTimeout(lm.utils.fnBind(this.updateSize, this), 100);
+ },
+
+ /**
+ * Extends the default config with the user specific settings and applies
+ * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode)
+ * that deals with the extension of item configs
+ *
+ * @param {Object} config
+ * @static
+ * @returns {Object} config
+ */
+ _createConfig: function (config) {
+ var windowConfigKey = lm.utils.getQueryStringParam('gl-window');
+
+ if (windowConfigKey) {
+ this.isSubWindow = true;
+ config = localStorage.getItem(windowConfigKey);
+ config = JSON.parse(config);
+ config = (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+ localStorage.removeItem(windowConfigKey);
+ }
+
+ config = $.extend(true, {}, lm.config.defaultConfig, config);
+
+ var nextNode = function (node) {
+ for (var key in node) {
+ if (key !== 'props' && typeof node[key] === 'object') {
+ nextNode(node[key]);
+ }
+ else if (key === 'type' && node[key] === 'react-component') {
+ node.type = 'component';
+ node.componentName = 'lm-react-component';
+ }
+ }
+ }
+
+ nextNode(config);
+
+ if (config.settings.hasHeaders === false) {
+ config.dimensions.headerHeight = 0;
+ }
+
+ return config;
+ },
+
+ /**
+ * This is executed when GoldenLayout detects that it is run
+ * within a previously opened popout window.
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _adjustToWindowMode: function () {
+ var popInButton = $('<div class="lm_popin" title="' + this.config.labels.popin + '">' +
+ '<div class="lm_icon"></div>' +
+ '<div class="lm_bg"></div>' +
+ '</div>');
+
+ popInButton.click(lm.utils.fnBind(function () {
+ this.emit('popIn');
+ }, this));
+
+ document.title = lm.utils.stripTags(this.config.content[0].title);
+
+ $('head').append($('body link, body style, template, .gl_keep'));
+
+ this.container = $('body')
+ .html('')
+ .css('visibility', 'visible')
+ .append(popInButton);
+
+ /*
+ * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around
+ * slickgrid's "Cannot find stylesheet." bug in chrome
+ */
+ var x = document.body.offsetHeight; // jshint ignore:line
+
+ /*
+ * Expose this instance on the window object
+ * to allow the opening window to interact with
+ * it
+ */
+ window.__glInstance = this;
+ },
+
+ /**
+ * Creates Subwindows (if there are any). Throws an error
+ * if popouts are blocked.
+ *
+ * @returns {void}
+ */
+ _createSubWindows: function () {
+ var i, popout;
+
+ for (i = 0; i < this.config.openPopouts.length; i++) {
+ popout = this.config.openPopouts[i];
+
+ this.createPopout(
+ popout.content,
+ popout.dimensions,
+ popout.parentId,
+ popout.indexInParent
+ );
+ }
+ },
+
+ /**
+ * Determines what element the layout will be created in
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setContainer: function () {
+ var container = $(this.container || 'body');
+
+ if (container.length === 0) {
+ throw new Error('GoldenLayout container not found');
+ }
+
+ if (container.length > 1) {
+ throw new Error('GoldenLayout more than one container element specified');
+ }
+
+ if (container[0] === document.body) {
+ this._isFullPage = true;
+
+ $('html, body').css({
+ height: '100%',
+ margin: 0,
+ padding: 0,
+ overflow: 'hidden'
+ });
+ }
+
+ this.container = container;
+ },
+
+ /**
+ * Kicks of the initial, recursive creation chain
+ *
+ * @param {Object} config GoldenLayout Config
+ *
+ * @returns {void}
+ */
+ _create: function (config) {
+ var errorMsg;
+
+ if (!(config.content instanceof Array)) {
+ if (config.content === undefined) {
+ errorMsg = 'Missing setting \'content\' on top level of configuration';
+ } else {
+ errorMsg = 'Configuration parameter \'content\' must be an array';
+ }
+
+ throw new lm.errors.ConfigurationError(errorMsg, config);
+ }
+
+ if (config.content.length > 1) {
+ errorMsg = 'Top level content can\'t contain more then one element.';
+ throw new lm.errors.ConfigurationError(errorMsg, config);
+ }
+
+ this.root = new lm.items.Root(this, { content: config.content }, this.container);
+ this.root.callDownwards('_$init');
+
+ if (config.maximisedItemId === '__glMaximised') {
+ this.root.getItemsById(config.maximisedItemId)[0].toggleMaximise();
+ }
+ },
+
+ /**
+ * Called when the window is closed or the user navigates away
+ * from the page
+ *
+ * @returns {void}
+ */
+ _onUnload: function () {
+ if (this.config.settings.closePopoutsOnUnload === true) {
+ for (var i = 0; i < this.openPopouts.length; i++) {
+ this.openPopouts[i].close();
+ }
+ }
+ },
+
+ /**
+ * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth.
+ *
+ * @returns {void}
+ */
+ _adjustColumnsResponsive: function () {
+
+ // If there is no min width set, or not content items, do nothing.
+ if (!this._useResponsiveLayout() || this._updatingColumnsResponsive || !this.config.dimensions || !this.config.dimensions.minItemWidth || this.root.contentItems.length === 0 || !this.root.contentItems[0].isRow) {
+ this._firstLoad = false;
+ return;
+ }
+
+ this._firstLoad = false;
+
+ // If there is only one column, do nothing.
+ var columnCount = this.root.contentItems[0].contentItems.length;
+ if (columnCount <= 1) {
+ return;
+ }
+
+ // If they all still fit, do nothing.
+ var minItemWidth = this.config.dimensions.minItemWidth;
+ var totalMinWidth = columnCount * minItemWidth;
+ if (totalMinWidth <= this.width) {
+ return;
+ }
+
+ // Prevent updates while it is already happening.
+ this._updatingColumnsResponsive = true;
+
+ // Figure out how many columns to stack, and put them all in the first stack container.
+ var finalColumnCount = Math.max(Math.floor(this.width / minItemWidth), 1);
+ var stackColumnCount = columnCount - finalColumnCount;
+
+ var rootContentItem = this.root.contentItems[0];
+ var firstStackContainer = this._findAllStackContainers()[0];
+ for (var i = 0; i < stackColumnCount; i++) {
+ // Stack from right.
+ var column = rootContentItem.contentItems[rootContentItem.contentItems.length - 1];
+ this._addChildContentItemsToContainer(firstStackContainer, column);
+ }
+
+ this._updatingColumnsResponsive = false;
+ },
+
+ /**
+ * Determines if responsive layout should be used.
+ *
+ * @returns {bool} - True if responsive layout should be used; otherwise false.
+ */
+ _useResponsiveLayout: function () {
+ return this.config.settings && (this.config.settings.responsiveMode == 'always' || (this.config.settings.responsiveMode == 'onload' && this._firstLoad));
+ },
+
+ /**
+ * Adds all children of a node to another container recursively.
+ * @param {object} container - Container to add child content items to.
+ * @param {object} node - Node to search for content items.
+ * @returns {void}
+ */
+ _addChildContentItemsToContainer: function (container, node) {
+ if (node.type === 'stack') {
+ node.contentItems.forEach(function (item) {
+ container.addChild(item);
+ node.removeChild(item, true);
+ });
+ }
+ else {
+ node.contentItems.forEach(lm.utils.fnBind(function (item) {
+ this._addChildContentItemsToContainer(container, item);
+ }, this));
+ }
+ },
+
+ /**
+ * Finds all the stack containers.
+ * @returns {array} - The found stack containers.
+ */
+ _findAllStackContainers: function () {
+ var stackContainers = [];
+ this._findAllStackContainersRecursive(stackContainers, this.root);
+
+ return stackContainers;
+ },
+
+ /**
+ * Finds all the stack containers.
+ *
+ * @param {array} - Set of containers to populate.
+ * @param {object} - Current node to process.
+ *
+ * @returns {void}
+ */
+ _findAllStackContainersRecursive: function (stackContainers, node) {
+ node.contentItems.forEach(lm.utils.fnBind(function (item) {
+ if (item.type == 'stack') {
+ stackContainers.push(item);
+ }
+ else if (!item.isComponent) {
+ this._findAllStackContainersRecursive(stackContainers, item);
+ }
+ }, this));
+ }
+ });
+
+ /**
+ * Expose the Layoutmanager as the single entrypoint using UMD
+ */
+ (function () {
+ /* global define */
+ if (typeof define === 'function' && define.amd) {
+ define(['jquery'], function (jquery) {
+ $ = jquery;
+ return lm.LayoutManager;
+ }); // jshint ignore:line
+ } else if (typeof exports === 'object') {
+ module.exports = lm.LayoutManager;
+ } else {
+ window.GoldenLayout = lm.LayoutManager;
+ }
+ })();
+
+ lm.config.itemDefaultConfig = {
+ isClosable: true,
+ reorderEnabled: true,
+ title: ''
+ };
+ lm.config.defaultConfig = {
+ openPopouts: [],
+ settings: {
+ hasHeaders: true,
+ constrainDragToContainer: true,
+ reorderEnabled: true,
+ selectionEnabled: false,
+ popoutWholeStack: false,
+ blockedPopoutsThrowError: true,
+ closePopoutsOnUnload: true,
+ showPopoutIcon: true,
+ showMaximiseIcon: true,
+ showCloseIcon: true,
+ responsiveMode: 'onload', // Can be onload, always, or none.
+ tabOverlapAllowance: 0, // maximum pixel overlap per tab
+ reorderOnTabMenuClick: true,
+ tabControlOffset: 10
+ },
+ dimensions: {
+ borderWidth: 5,
+ borderGrabWidth: 15,
+ minItemHeight: 10,
+ minItemWidth: 10,
+ headerHeight: 20,
+ dragProxyWidth: 300,
+ dragProxyHeight: 200
+ },
+ labels: {
+ close: 'close',
+ maximise: 'maximise',
+ minimise: 'minimise',
+ popout: 'open in new window',
+ popin: 'pop in',
+ tabDropdown: 'additional tabs'
+ }
+ };
+
+ lm.container.ItemContainer = function (config, parent, layoutManager) {
+ lm.utils.EventEmitter.call(this);
+
+ this.width = null;
+ this.height = null;
+ this.title = config.componentName;
+ this.parent = parent;
+ this.layoutManager = layoutManager;
+ this.isHidden = false;
+
+ this._config = config;
+ this._element = $([
+ '<div class="lm_item_container">',
+ '<div class="lm_content"></div>',
+ '</div>'
+ ].join(''));
+
+ this._contentElement = this._element.find('.lm_content');
+ };
+
+ lm.utils.copy(lm.container.ItemContainer.prototype, {
+
+ /**
+ * Get the inner DOM element the container's content
+ * is intended to live in
+ *
+ * @returns {DOM element}
+ */
+ getElement: function () {
+ return this._contentElement;
+ },
+
+ /**
+ * Hide the container. Notifies the containers content first
+ * and then hides the DOM node. If the container is already hidden
+ * this should have no effect
+ *
+ * @returns {void}
+ */
+ hide: function () {
+ this.emit('hide');
+ this.isHidden = true;
+ this._element.hide();
+ },
+
+ /**
+ * Shows a previously hidden container. Notifies the
+ * containers content first and then shows the DOM element.
+ * If the container is already visible this has no effect.
+ *
+ * @returns {void}
+ */
+ show: function () {
+ this.emit('show');
+ this.isHidden = false;
+ this._element.show();
+ // call shown only if the container has a valid size
+ if (this.height != 0 || this.width != 0) {
+ this.emit('shown');
+ }
+ },
+
+ /**
+ * Set the size from within the container. Traverses up
+ * the item tree until it finds a row or column element
+ * and resizes its items accordingly.
+ *
+ * If this container isn't a descendant of a row or column
+ * it returns false
+ * @todo Rework!!!
+ * @param {Number} width The new width in pixel
+ * @param {Number} height The new height in pixel
+ *
+ * @returns {Boolean} resizeSuccesful
+ */
+ setSize: function (width, height) {
+ var rowOrColumn = this.parent,
+ rowOrColumnChild = this,
+ totalPixel,
+ percentage,
+ direction,
+ newSize,
+ delta,
+ i;
+
+ while (!rowOrColumn.isColumn && !rowOrColumn.isRow) {
+ rowOrColumnChild = rowOrColumn;
+ rowOrColumn = rowOrColumn.parent;
+
+
+ /**
+ * No row or column has been found
+ */
+ if (rowOrColumn.isRoot) {
+ return false;
+ }
+ }
+
+ direction = rowOrColumn.isColumn ? "height" : "width";
+ newSize = direction === "height" ? height : width;
+
+ totalPixel = this[direction] * (1 / (rowOrColumnChild.config[direction] / 100));
+ percentage = (newSize / totalPixel) * 100;
+ delta = (rowOrColumnChild.config[direction] - percentage) / (rowOrColumn.contentItems.length - 1);
+
+ for (i = 0; i < rowOrColumn.contentItems.length; i++) {
+ if (rowOrColumn.contentItems[i] === rowOrColumnChild) {
+ rowOrColumn.contentItems[i].config[direction] = percentage;
+ } else {
+ rowOrColumn.contentItems[i].config[direction] += delta;
+ }
+ }
+
+ rowOrColumn.callDownwards('setSize');
+
+ return true;
+ },
+
+ /**
+ * Closes the container if it is closable. Can be called by
+ * both the component within at as well as the contentItem containing
+ * it. Emits a close event before the container itself is closed.
+ *
+ * @returns {void}
+ */
+ close: function () {
+ if (this._config.isClosable) {
+ this.emit('close');
+ this.parent.close();
+ }
+ },
+
+ /**
+ * Returns the current state object
+ *
+ * @returns {Object} state
+ */
+ getState: function () {
+ return this._config.componentState;
+ },
+
+ /**
+ * Merges the provided state into the current one
+ *
+ * @param {Object} state
+ *
+ * @returns {void}
+ */
+ extendState: function (state) {
+ this.setState($.extend(true, this.getState(), state));
+ },
+
+ /**
+ * Notifies the layout manager of a stateupdate
+ *
+ * @param {serialisable} state
+ */
+ setState: function (state) {
+ this._config.componentState = state;
+ this.parent.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Set's the components title
+ *
+ * @param {String} title
+ */
+ setTitle: function (title) {
+ this.parent.setTitle(title);
+ },
+
+ /**
+ * Set's the containers size. Called by the container's component.
+ * To set the size programmatically from within the container please
+ * use the public setSize method
+ *
+ * @param {[Int]} width in px
+ * @param {[Int]} height in px
+ *
+ * @returns {void}
+ */
+ _$setSize: function (width, height) {
+ if (width !== this.width || height !== this.height) {
+ this.width = width;
+ this.height = height;
+ var cl = this._contentElement[0];
+ var hdelta = cl.offsetWidth - cl.clientWidth;
+ var vdelta = cl.offsetHeight - cl.clientHeight;
+ this._contentElement.width(this.width - hdelta)
+ .height(this.height - vdelta);
+ this.emit('resize');
+ }
+ }
+ });
+
+ /**
+ * Pops a content item out into a new browser window.
+ * This is achieved by
+ *
+ * - Creating a new configuration with the content item as root element
+ * - Serializing and minifying the configuration
+ * - Opening the current window's URL with the configuration as a GET parameter
+ * - GoldenLayout when opened in the new window will look for the GET parameter
+ * and use it instead of the provided configuration
+ *
+ * @param {Object} config GoldenLayout item config
+ * @param {Object} dimensions A map with width, height, top and left
+ * @param {String} parentId The id of the element the item will be appended to on popIn
+ * @param {Number} indexInParent The position of this element within its parent
+ * @param {lm.LayoutManager} layoutManager
+ */
+ lm.controls.BrowserPopout = function (config, dimensions, parentId, indexInParent, layoutManager) {
+ lm.utils.EventEmitter.call(this);
+ this.isInitialised = false;
+
+ this._config = config;
+ this._dimensions = dimensions;
+ this._parentId = parentId;
+ this._indexInParent = indexInParent;
+ this._layoutManager = layoutManager;
+ this._popoutWindow = null;
+ this._id = null;
+ this._createWindow();
+ };
+
+ lm.utils.copy(lm.controls.BrowserPopout.prototype, {
+
+ toConfig: function () {
+ if (this.isInitialised === false) {
+ throw new Error('Can\'t create config, layout not yet initialised');
+ return;
+ }
+ return {
+ dimensions: {
+ width: this.getGlInstance().width,
+ height: this.getGlInstance().height,
+ left: this._popoutWindow.screenX || this._popoutWindow.screenLeft,
+ top: this._popoutWindow.screenY || this._popoutWindow.screenTop
+ },
+ content: this.getGlInstance().toConfig().content,
+ parentId: this._parentId,
+ indexInParent: this._indexInParent
+ };
+ },
+
+ getGlInstance: function () {
+ return this._popoutWindow.__glInstance;
+ },
+
+ getWindow: function () {
+ return this._popoutWindow;
+ },
+
+ close: function () {
+ if (this.getGlInstance()) {
+ this.getGlInstance()._$closeWindow();
+ } else {
+ try {
+ this.getWindow().close();
+ } catch (e) {
+ }
+ }
+ },
+
+ /**
+ * Returns the popped out item to its original position. If the original
+ * parent isn't available anymore it falls back to the layout's topmost element
+ */
+ popIn: function () {
+ var childConfig,
+ parentItem,
+ index = this._indexInParent;
+
+ if (this._parentId) {
+
+ /*
+ * The $.extend call seems a bit pointless, but it's crucial to
+ * copy the config returned by this.getGlInstance().toConfig()
+ * onto a new object. Internet Explorer keeps the references
+ * to objects on the child window, resulting in the following error
+ * once the child window is closed:
+ *
+ * The callee (server [not server application]) is not available and disappeared
+ */
+ childConfig = $.extend(true, {}, this.getGlInstance().toConfig()).content[0];
+ parentItem = this._layoutManager.root.getItemsById(this._parentId)[0];
+
+ /*
+ * Fallback if parentItem is not available. Either add it to the topmost
+ * item or make it the topmost item if the layout is empty
+ */
+ if (!parentItem) {
+ if (this._layoutManager.root.contentItems.length > 0) {
+ parentItem = this._layoutManager.root.contentItems[0];
+ } else {
+ parentItem = this._layoutManager.root;
+ }
+ index = 0;
+ }
+ }
+
+ parentItem.addChild(childConfig, this._indexInParent);
+ this.close();
+ },
+
+ /**
+ * Creates the URL and window parameter
+ * and opens a new window
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _createWindow: function () {
+ var checkReadyInterval,
+ url = this._createUrl(),
+
+ /**
+ * Bogus title to prevent re-usage of existing window with the
+ * same title. The actual title will be set by the new window's
+ * GoldenLayout instance if it detects that it is in subWindowMode
+ */
+ title = Math.floor(Math.random() * 1000000).toString(36),
+
+ /**
+ * The options as used in the window.open string
+ */
+ options = this._serializeWindowOptions({
+ width: this._dimensions.width,
+ height: this._dimensions.height,
+ innerWidth: this._dimensions.width,
+ innerHeight: this._dimensions.height,
+ menubar: 'no',
+ toolbar: 'no',
+ location: 'no',
+ personalbar: 'no',
+ resizable: 'yes',
+ scrollbars: 'no',
+ status: 'no'
+ });
+
+ this._popoutWindow = window.open(url, title, options);
+
+ if (!this._popoutWindow) {
+ if (this._layoutManager.config.settings.blockedPopoutsThrowError === true) {
+ var error = new Error('Popout blocked');
+ error.type = 'popoutBlocked';
+ throw error;
+ } else {
+ return;
+ }
+ }
+
+ $(this._popoutWindow)
+ .on('load', lm.utils.fnBind(this._positionWindow, this))
+ .on('unload beforeunload', lm.utils.fnBind(this._onClose, this));
+
+ /**
+ * Polling the childwindow to find out if GoldenLayout has been initialised
+ * doesn't seem optimal, but the alternatives - adding a callback to the parent
+ * window or raising an event on the window object - both would introduce knowledge
+ * about the parent to the child window which we'd rather avoid
+ */
+ checkReadyInterval = setInterval(lm.utils.fnBind(function () {
+ if (this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised) {
+ this._onInitialised();
+ clearInterval(checkReadyInterval);
+ }
+ }, this), 10);
+ },
+
+ /**
+ * Serialises a map of key:values to a window options string
+ *
+ * @param {Object} windowOptions
+ *
+ * @returns {String} serialised window options
+ */
+ _serializeWindowOptions: function (windowOptions) {
+ var windowOptionsString = [], key;
+
+ for (key in windowOptions) {
+ windowOptionsString.push(key + '=' + windowOptions[key]);
+ }
+
+ return windowOptionsString.join(',');
+ },
+
+ /**
+ * Creates the URL for the new window, including the
+ * config GET parameter
+ *
+ * @returns {String} URL
+ */
+ _createUrl: function () {
+ var config = { content: this._config },
+ storageKey = 'gl-window-config-' + lm.utils.getUniqueId(),
+ urlParts;
+
+ config = (new lm.utils.ConfigMinifier()).minifyConfig(config);
+
+ try {
+ localStorage.setItem(storageKey, JSON.stringify(config));
+ } catch (e) {
+ throw new Error('Error while writing to localStorage ' + e.toString());
+ }
+
+ urlParts = document.location.href.split('?');
+
+ // URL doesn't contain GET-parameters
+ if (urlParts.length === 1) {
+ return urlParts[0] + '?gl-window=' + storageKey;
+
+ // URL contains GET-parameters
+ } else {
+ return document.location.href + '&gl-window=' + storageKey;
+ }
+ },
+
+ /**
+ * Move the newly created window roughly to
+ * where the component used to be.
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _positionWindow: function () {
+ this._popoutWindow.moveTo(this._dimensions.left, this._dimensions.top);
+ this._popoutWindow.focus();
+ },
+
+ /**
+ * Callback when the new window is opened and the GoldenLayout instance
+ * within it is initialised
+ *
+ * @returns {void}
+ */
+ _onInitialised: function () {
+ this.isInitialised = true;
+ this.getGlInstance().on('popIn', this.popIn, this);
+ this.emit('initialised');
+ },
+
+ /**
+ * Invoked 50ms after the window unload event
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onClose: function () {
+ setTimeout(lm.utils.fnBind(this.emit, this, ['closed']), 50);
+ }
+ });
+ /**
+ * This class creates a temporary container
+ * for the component whilst it is being dragged
+ * and handles drag events
+ *
+ * @constructor
+ * @private
+ *
+ * @param {Number} x The initial x position
+ * @param {Number} y The initial y position
+ * @param {lm.utils.DragListener} dragListener
+ * @param {lm.LayoutManager} layoutManager
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {lm.item.AbstractContentItem} originalParent
+ */
+ lm.controls.DragProxy = function (x, y, dragListener, layoutManager, contentItem, originalParent) {
+
+ lm.utils.EventEmitter.call(this);
+
+ this._dragListener = dragListener;
+ this._layoutManager = layoutManager;
+ this._contentItem = contentItem;
+ this._originalParent = originalParent;
+
+ this._area = null;
+ this._lastValidArea = null;
+
+ this._dragListener.on('drag', this._onDrag, this);
+ this._dragListener.on('dragStop', this._onDrop, this);
+
+ this.element = $(lm.controls.DragProxy._template);
+ if (originalParent && originalParent._side) {
+ this._sided = originalParent._sided;
+ this.element.addClass('lm_' + originalParent._side);
+ if (['right', 'bottom'].indexOf(originalParent._side) >= 0)
+ this.element.find('.lm_content').after(this.element.find('.lm_header'));
+ }
+ this.element.css({ left: x, top: y });
+ this.element.find('.lm_tab').attr('title', lm.utils.stripTags(this._contentItem.config.title));
+ this.element.find('.lm_title').html(this._contentItem.config.title);
+ this.childElementContainer = this.element.find('.lm_content');
+ this.childElementContainer.append(contentItem.element);
+
+ this._updateTree();
+ this._layoutManager._$calculateItemAreas();
+ this._setDimensions();
+
+ $(document.body).append(this.element);
+
+ var offset = this._layoutManager.container.offset();
+
+ this._minX = offset.left;
+ this._minY = offset.top;
+ this._maxX = this._layoutManager.container.width() + this._minX;
+ this._maxY = this._layoutManager.container.height() + this._minY;
+ this._width = this.element.width();
+ this._height = this.element.height();
+
+ this._setDropPosition(x, y);
+ };
+
+ lm.controls.DragProxy._template = '<div class="lm_dragProxy">' +
+ '<div class="lm_header">' +
+ '<ul class="lm_tabs">' +
+ '<li class="lm_tab lm_active"><i class="lm_left"></i>' +
+ '<span class="lm_title"></span>' +
+ '<i class="lm_right"></i></li>' +
+ '</ul>' +
+ '</div>' +
+ '<div class="lm_content"></div>' +
+ '</div>';
+
+ lm.utils.copy(lm.controls.DragProxy.prototype, {
+
+ /**
+ * Callback on every mouseMove event during a drag. Determines if the drag is
+ * still within the valid drag area and calls the layoutManager to highlight the
+ * current drop area
+ *
+ * @param {Number} offsetX The difference from the original x position in px
+ * @param {Number} offsetY The difference from the original y position in px
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onDrag: function (offsetX, offsetY, event) {
+
+ event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+
+ var x = event.pageX,
+ y = event.pageY,
+ isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY;
+
+ if (!isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true) {
+ return;
+ }
+
+ this._setDropPosition(x, y);
+ },
+
+ /**
+ * Sets the target position, highlighting the appropriate area
+ *
+ * @param {Number} x The x position in px
+ * @param {Number} y The y position in px
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setDropPosition: function (x, y) {
+ this.element.css({ left: x, top: y });
+ this._area = this._layoutManager._$getArea(x, y);
+
+ if (this._area !== null) {
+ this._lastValidArea = this._area;
+ this._area.contentItem._$highlightDropZone(x, y, this._area);
+ }
+ },
+
+ /**
+ * Callback when the drag has finished. Determines the drop area
+ * and adds the child to it
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _onDrop: function () {
+ this._layoutManager.dropTargetIndicator.hide();
+
+ /*
+ * Valid drop area found
+ */
+ if (this._area !== null) {
+ this._area.contentItem._$onDrop(this._contentItem, this._area);
+
+ /**
+ * No valid drop area available at present, but one has been found before.
+ * Use it
+ */
+ } else if (this._lastValidArea !== null) {
+ this._lastValidArea.contentItem._$onDrop(this._contentItem, this._lastValidArea);
+
+ /**
+ * No valid drop area found during the duration of the drag. Return
+ * content item to its original position if a original parent is provided.
+ * (Which is not the case if the drag had been initiated by createDragSource)
+ */
+ } else if (this._originalParent) {
+ this._originalParent.addChild(this._contentItem);
+
+ /**
+ * The drag didn't ultimately end up with adding the content item to
+ * any container. In order to ensure clean up happens, destroy the
+ * content item.
+ */
+ } else {
+ this._contentItem._$destroy();
+ }
+
+ this.element.remove();
+
+ this._layoutManager.emit('itemDropped', this._contentItem);
+ },
+
+ /**
+ * Removes the item from its original position within the tree
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _updateTree: function () {
+
+ /**
+ * parent is null if the drag had been initiated by a external drag source
+ */
+ if (this._contentItem.parent) {
+ this._contentItem.parent.removeChild(this._contentItem, true);
+ }
+
+ this._contentItem._$setParent(this);
+ },
+
+ /**
+ * Updates the Drag Proxie's dimensions
+ *
+ * @private
+ *
+ * @returns {void}
+ */
+ _setDimensions: function () {
+ var dimensions = this._layoutManager.config.dimensions,
+ width = dimensions.dragProxyWidth,
+ height = dimensions.dragProxyHeight;
+
+ this.element.width(width);
+ this.element.height(height);
+ width -= (this._sided ? dimensions.headerHeight : 0);
+ height -= (!this._sided ? dimensions.headerHeight : 0);
+ this.childElementContainer.width(width);
+ this.childElementContainer.height(height);
+ this._contentItem.element.width(width);
+ this._contentItem.element.height(height);
+ this._contentItem.callDownwards('_$show');
+ this._contentItem.callDownwards('setSize');
+ }
+ });
+
+ /**
+ * Allows for any DOM item to create a component on drag
+ * start tobe dragged into the Layout
+ *
+ * @param {jQuery element} element
+ * @param {Object} itemConfig the configuration for the contentItem that will be created
+ * @param {LayoutManager} layoutManager
+ *
+ * @constructor
+ */
+ lm.controls.DragSource = function (element, itemConfig, layoutManager) {
+ this._element = element;
+ this._itemConfig = itemConfig;
+ this._layoutManager = layoutManager;
+ this._dragListener = null;
+
+ this._createDragListener();
+ };
+
+ lm.utils.copy(lm.controls.DragSource.prototype, {
+
+ /**
+ * Called initially and after every drag
+ *
+ * @returns {void}
+ */
+ _createDragListener: function () {
+ if (this._dragListener !== null) {
+ this._dragListener.destroy();
+ }
+
+ this._dragListener = new lm.utils.DragListener(this._element);
+ this._dragListener.on('dragStart', this._onDragStart, this);
+ this._dragListener.on('dragStop', this._createDragListener, this);
+ },
+
+ /**
+ * Callback for the DragListener's dragStart event
+ *
+ * @param {int} x the x position of the mouse on dragStart
+ * @param {int} y the x position of the mouse on dragStart
+ *
+ * @returns {void}
+ */
+ _onDragStart: function (x, y) {
+ var itemConfig = this._itemConfig;
+ if (lm.utils.isFunction(itemConfig)) {
+ itemConfig = itemConfig();
+ }
+ var contentItem = this._layoutManager._$normalizeContentItem($.extend(true, {}, itemConfig)),
+ dragProxy = new lm.controls.DragProxy(x, y, this._dragListener, this._layoutManager, contentItem, null);
+
+ this._layoutManager.transitionIndicator.transitionElements(this._element, dragProxy.element);
+ }
+ });
+
+ lm.controls.DropTargetIndicator = function () {
+ this.element = $(lm.controls.DropTargetIndicator._template);
+ $(document.body).append(this.element);
+ };
+
+ lm.controls.DropTargetIndicator._template = '<div class="lm_dropTargetIndicator"><div class="lm_inner"></div></div>';
+
+ lm.utils.copy(lm.controls.DropTargetIndicator.prototype, {
+ destroy: function () {
+ this.element.remove();
+ },
+
+ highlight: function (x1, y1, x2, y2) {
+ this.highlightArea({ x1: x1, y1: y1, x2: x2, y2: y2 });
+ },
+
+ highlightArea: function (area) {
+ this.element.css({
+ left: area.x1,
+ top: area.y1,
+ width: area.x2 - area.x1,
+ height: area.y2 - area.y1
+ }).show();
+ },
+
+ hide: function () {
+ this.element.hide();
+ }
+ });
+ /**
+ * This class represents a header above a Stack ContentItem.
+ *
+ * @param {lm.LayoutManager} layoutManager
+ * @param {lm.item.AbstractContentItem} parent
+ */
+ lm.controls.Header = function (layoutManager, parent) {
+ lm.utils.EventEmitter.call(this);
+
+ this.layoutManager = layoutManager;
+ this.element = $(lm.controls.Header._template);
+
+ if (this.layoutManager.config.settings.selectionEnabled === true) {
+ this.element.addClass('lm_selectable');
+ this.element.on('click touchstart', lm.utils.fnBind(this._onHeaderClick, this));
+ }
+
+ this.tabsContainer = this.element.find('.lm_tabs');
+ this.tabDropdownContainer = this.element.find('.lm_tabdropdown_list');
+ this.tabDropdownContainer.hide();
+ this.controlsContainer = this.element.find('.lm_controls');
+ this.parent = parent;
+ this.parent.on('resize', this._updateTabSizes, this);
+ this.tabs = [];
+ this.activeContentItem = null;
+ this.closeButton = null;
+ this.tabDropdownButton = null;
+ this.hideAdditionalTabsDropdown = lm.utils.fnBind(this._hideAdditionalTabsDropdown, this);
+ $(document).mouseup(this.hideAdditionalTabsDropdown);
+
+ this._lastVisibleTabIndex = -1;
+ this._tabControlOffset = this.layoutManager.config.settings.tabControlOffset;
+ this._createControls();
+ };
+
+ lm.controls.Header._template = [
+ '<div class="lm_header">',
+ '<ul class="lm_tabs"></ul>',
+ '<ul class="lm_controls"></ul>',
+ '<ul class="lm_tabdropdown_list"></ul>',
+ '</div>'
+ ].join('');
+
+ lm.utils.copy(lm.controls.Header.prototype, {
+
+ /**
+ * Creates a new tab and associates it with a contentItem
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {Integer} index The position of the tab
+ *
+ * @returns {void}
+ */
+ createTab: function (contentItem, index) {
+ var tab, i;
+
+ //If there's already a tab relating to the
+ //content item, don't do anything
+ for (i = 0; i < this.tabs.length; i++) {
+ if (this.tabs[i].contentItem === contentItem) {
+ return;
+ }
+ }
+
+ tab = new lm.controls.Tab(this, contentItem);
+
+ if (this.tabs.length === 0) {
+ this.tabs.push(tab);
+ this.tabsContainer.append(tab.element);
+ return;
+ }
+
+ if (index === undefined) {
+ index = this.tabs.length;
+ }
+
+ if (index > 0) {
+ this.tabs[index - 1].element.after(tab.element);
+ } else {
+ this.tabs[0].element.before(tab.element);
+ }
+
+ this.tabs.splice(index, 0, tab);
+ this._updateTabSizes();
+ },
+
+ /**
+ * Finds a tab based on the contentItem its associated with and removes it.
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ *
+ * @returns {void}
+ */
+ removeTab: function (contentItem) {
+ for (var i = 0; i < this.tabs.length; i++) {
+ if (this.tabs[i].contentItem === contentItem) {
+ this.tabs[i]._$destroy();
+ this.tabs.splice(i, 1);
+ return;
+ }
+ }
+
+ throw new Error('contentItem is not controlled by this header');
+ },
+
+ /**
+ * The programmatical equivalent of clicking a Tab.
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ */
+ setActiveContentItem: function (contentItem) {
+ var i, j, isActive, activeTab;
+
+ for (i = 0; i < this.tabs.length; i++) {
+ isActive = this.tabs[i].contentItem === contentItem;
+ this.tabs[i].setActive(isActive);
+ if (isActive === true) {
+ this.activeContentItem = contentItem;
+ this.parent.config.activeItemIndex = i;
+ }
+ }
+
+ if (this.layoutManager.config.settings.reorderOnTabMenuClick) {
+ /**
+ * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first.
+ * This will make sure the most used tabs stay visible.
+ */
+ if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) {
+ activeTab = this.tabs[this.parent.config.activeItemIndex];
+ for (j = this.parent.config.activeItemIndex; j > 0; j--) {
+ this.tabs[j] = this.tabs[j - 1];
+ }
+ this.tabs[0] = activeTab;
+ this.parent.config.activeItemIndex = 0;
+ }
+ }
+
+ this._updateTabSizes();
+ this.parent.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Programmatically operate with header position.
+ *
+ * @param {string} position one of ('top','left','right','bottom') to set or empty to get it.
+ *
+ * @returns {string} previous header position
+ */
+ position: function (position) {
+ var previous = this.parent._header.show;
+ if (previous && !this.parent._side)
+ previous = 'top';
+ if (position !== undefined && this.parent._header.show != position) {
+ this.parent._header.show = position;
+ this.parent._setupHeaderPosition();
+ }
+ return previous;
+ },
+
+ /**
+ * Programmatically set closability.
+ *
+ * @package private
+ * @param {Boolean} isClosable Whether to enable/disable closability.
+ *
+ * @returns {Boolean} Whether the action was successful
+ */
+ _$setClosable: function (isClosable) {
+ if (this.closeButton && this._isClosable()) {
+ this.closeButton.element[isClosable ? "show" : "hide"]();
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Destroys the entire header
+ *
+ * @package private
+ *
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this.emit('destroy', this);
+
+ for (var i = 0; i < this.tabs.length; i++) {
+ this.tabs[i]._$destroy();
+ }
+ $(document).off('mouseup', this.hideAdditionalTabsDropdown);
+ this.element.remove();
+ },
+
+ /**
+ * get settings from header
+ *
+ * @returns {string} when exists
+ */
+ _getHeaderSetting: function (name) {
+ if (name in this.parent._header)
+ return this.parent._header[name];
+ },
+ /**
+ * Creates the popout, maximise and close buttons in the header's top right corner
+ *
+ * @returns {void}
+ */
+ _createControls: function () {
+ var closeStack,
+ popout,
+ label,
+ maximiseLabel,
+ minimiseLabel,
+ maximise,
+ maximiseButton,
+ tabDropdownLabel,
+ showTabDropdown;
+
+ /**
+ * Dropdown to show additional tabs.
+ */
+ showTabDropdown = lm.utils.fnBind(this._showAdditionalTabsDropdown, this);
+ tabDropdownLabel = this.layoutManager.config.labels.tabDropdown;
+ this.tabDropdownButton = new lm.controls.HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown);
+ this.tabDropdownButton.element.hide();
+
+ /**
+ * Popout control to launch component in new window.
+ */
+ if (this._getHeaderSetting('popout')) {
+ popout = lm.utils.fnBind(this._onPopoutClick, this);
+ label = this._getHeaderSetting('popout');
+ new lm.controls.HeaderButton(this, label, 'lm_popout', popout);
+ }
+
+ /**
+ * Maximise control - set the component to the full size of the layout
+ */
+ if (this._getHeaderSetting('maximise')) {
+ maximise = lm.utils.fnBind(this.parent.toggleMaximise, this.parent);
+ maximiseLabel = this._getHeaderSetting('maximise');
+ minimiseLabel = this._getHeaderSetting('minimise');
+ maximiseButton = new lm.controls.HeaderButton(this, maximiseLabel, 'lm_maximise', maximise);
+
+ this.parent.on('maximised', function () {
+ maximiseButton.element.attr('title', minimiseLabel);
+ });
+
+ this.parent.on('minimised', function () {
+ maximiseButton.element.attr('title', maximiseLabel);
+ });
+ }
+
+ /**
+ * Close button
+ */
+ if (this._isClosable()) {
+ closeStack = lm.utils.fnBind(this.parent.remove, this.parent);
+ label = this._getHeaderSetting('close');
+ this.closeButton = new lm.controls.HeaderButton(this, label, 'lm_close', closeStack);
+ }
+ },
+
+ /**
+ * Shows drop down for additional tabs when there are too many to display.
+ *
+ * @returns {void}
+ */
+ _showAdditionalTabsDropdown: function () {
+ this.tabDropdownContainer.show();
+ },
+
+ /**
+ * Hides drop down for additional tabs when there are too many to display.
+ *
+ * @returns {void}
+ */
+ _hideAdditionalTabsDropdown: function (e) {
+ this.tabDropdownContainer.hide();
+ },
+
+ /**
+ * Checks whether the header is closable based on the parent config and
+ * the global config.
+ *
+ * @returns {Boolean} Whether the header is closable.
+ */
+ _isClosable: function () {
+ return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon;
+ },
+
+ _onPopoutClick: function () {
+ if (this.layoutManager.config.settings.popoutWholeStack === true) {
+ this.parent.popout();
+ } else {
+ this.activeContentItem.popout();
+ }
+ },
+
+
+ /**
+ * Invoked when the header's background is clicked (not it's tabs or controls)
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @returns {void}
+ */
+ _onHeaderClick: function (event) {
+ if (event.target === this.element[0]) {
+ this.parent.select();
+ }
+ },
+
+ /**
+ * Pushes the tabs to the tab dropdown if the available space is not sufficient
+ *
+ * @returns {void}
+ */
+ _updateTabSizes: function (showTabMenu) {
+ if (this.tabs.length === 0) {
+ return;
+ }
+
+ //Show the menu based on function argument
+ this.tabDropdownButton.element.toggle(showTabMenu === true);
+
+ var size = function (val) {
+ return val ? 'width' : 'height';
+ };
+ this.element.css(size(!this.parent._sided), '');
+ this.element[size(this.parent._sided)](this.layoutManager.config.dimensions.headerHeight);
+ var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth() - this._tabControlOffset,
+ cumulativeTabWidth = 0,
+ visibleTabWidth = 0,
+ tabElement,
+ i,
+ j,
+ marginLeft,
+ overlap = 0,
+ tabWidth,
+ tabOverlapAllowance = this.layoutManager.config.settings.tabOverlapAllowance,
+ tabOverlapAllowanceExceeded = false,
+ activeIndex = (this.activeContentItem ? this.tabs.indexOf(this.activeContentItem.tab) : 0),
+ activeTab = this.tabs[activeIndex];
+ if (this.parent._sided)
+ availableWidth = this.element.outerHeight() - this.controlsContainer.outerHeight() - this._tabControlOffset;
+ this._lastVisibleTabIndex = -1;
+
+ for (i = 0; i < this.tabs.length; i++) {
+ tabElement = this.tabs[i].element;
+
+ //Put the tab in the tabContainer so its true width can be checked
+ this.tabsContainer.append(tabElement);
+ tabWidth = tabElement.outerWidth() + parseInt(tabElement.css('margin-right'), 10);
+
+ cumulativeTabWidth += tabWidth;
+
+ //Include the active tab's width if it isn't already
+ //This is to ensure there is room to show the active tab
+ if (activeIndex <= i) {
+ visibleTabWidth = cumulativeTabWidth;
+ } else {
+ visibleTabWidth = cumulativeTabWidth + activeTab.element.outerWidth() + parseInt(activeTab.element.css('margin-right'), 10);
+ }
+
+ // If the tabs won't fit, check the overlap allowance.
+ if (visibleTabWidth > availableWidth) {
+
+ //Once allowance is exceeded, all remaining tabs go to menu.
+ if (!tabOverlapAllowanceExceeded) {
+
+ //No overlap for first tab or active tab
+ //Overlap spreads among non-active, non-first tabs
+ if (activeIndex > 0 && activeIndex <= i) {
+ overlap = (visibleTabWidth - availableWidth) / (i - 1);
+ } else {
+ overlap = (visibleTabWidth - availableWidth) / i;
+ }
+
+ //Check overlap against allowance.
+ if (overlap < tabOverlapAllowance) {
+ for (j = 0; j <= i; j++) {
+ marginLeft = (j !== activeIndex && j !== 0) ? '-' + overlap + 'px' : '';
+ this.tabs[j].element.css({ 'z-index': i - j, 'margin-left': marginLeft });
+ }
+ this._lastVisibleTabIndex = i;
+ this.tabsContainer.append(tabElement);
+ } else {
+ tabOverlapAllowanceExceeded = true;
+ }
+
+ } else if (i === activeIndex) {
+ //Active tab should show even if allowance exceeded. (We left room.)
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabsContainer.append(tabElement);
+ }
+
+ if (tabOverlapAllowanceExceeded && i !== activeIndex) {
+ if (showTabMenu) {
+ //Tab menu already shown, so we just add to it.
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabDropdownContainer.append(tabElement);
+ } else {
+ //We now know the tab menu must be shown, so we have to recalculate everything.
+ this._updateTabSizes(true);
+ return;
+ }
+ }
+
+ }
+ else {
+ this._lastVisibleTabIndex = i;
+ tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+ this.tabsContainer.append(tabElement);
+ }
+ }
+
+ }
+ });
+
+
+ lm.controls.HeaderButton = function (header, label, cssClass, action) {
+ this._header = header;
+ this.element = $('<li class="' + cssClass + '" title="' + label + '"></li>');
+ this._header.on('destroy', this._$destroy, this);
+ this._action = action;
+ this.element.on('click touchstart', this._action);
+ this._header.controlsContainer.append(this.element);
+ };
+
+ lm.utils.copy(lm.controls.HeaderButton.prototype, {
+ _$destroy: function () {
+ this.element.off();
+ this.element.remove();
+ }
+ });
+ lm.controls.Splitter = function (isVertical, size, grabSize) {
+ this._isVertical = isVertical;
+ this._size = size;
+ this._grabSize = grabSize < size ? size : grabSize;
+
+ this.element = this._createElement();
+ this._dragListener = new lm.utils.DragListener(this.element);
+ };
+
+ lm.utils.copy(lm.controls.Splitter.prototype, {
+ on: function (event, callback, context) {
+ this._dragListener.on(event, callback, context);
+ },
+
+ _$destroy: function () {
+ this.element.remove();
+ },
+
+ _createElement: function () {
+ var dragHandle = $('<div class="lm_drag_handle"></div>');
+ var element = $('<div class="lm_splitter"></div>');
+ element.append(dragHandle);
+
+ var handleExcessSize = this._grabSize - this._size;
+ var handleExcessPos = handleExcessSize / 2;
+
+ if (this._isVertical) {
+ dragHandle.css('top', -handleExcessPos);
+ dragHandle.css('height', this._size + handleExcessSize);
+ element.addClass('lm_vertical');
+ element['height'](this._size);
+ } else {
+ dragHandle.css('left', -handleExcessPos);
+ dragHandle.css('width', this._size + handleExcessSize);
+ element.addClass('lm_horizontal');
+ element['width'](this._size);
+ }
+
+ return element;
+ }
+ });
+
+ /**
+ * Represents an individual tab within a Stack's header
+ *
+ * @param {lm.controls.Header} header
+ * @param {lm.items.AbstractContentItem} contentItem
+ *
+ * @constructor
+ */
+ lm.controls.Tab = function (header, contentItem) {
+ this.header = header;
+ this.contentItem = contentItem;
+ this.element = $(lm.controls.Tab._template);
+ this.titleElement = this.element.find('.lm_title');
+ this.closeElement = this.element.find('.lm_close_tab');
+ this.closeElement[contentItem.config.isClosable ? 'show' : 'hide']();
+ this.isActive = false;
+
+ this.setTitle(contentItem.config.title);
+ this.contentItem.on('titleChanged', this.setTitle, this);
+
+ this._layoutManager = this.contentItem.layoutManager;
+
+ if (
+ this._layoutManager.config.settings.reorderEnabled === true &&
+ contentItem.config.reorderEnabled === true
+ ) {
+ this._dragListener = new lm.utils.DragListener(this.element);
+ this._dragListener.on('dragStart', this._onDragStart, this);
+ this.contentItem.on('destroy', this._dragListener.destroy, this._dragListener);
+ }
+
+ this._onTabClickFn = lm.utils.fnBind(this._onTabClick, this);
+ this._onCloseClickFn = lm.utils.fnBind(this._onCloseClick, this);
+
+ this.element.on('mousedown touchstart', this._onTabClickFn);
+
+ if (this.contentItem.config.isClosable) {
+ this.closeElement.on('click touchstart', this._onCloseClickFn);
+ this.closeElement.on('mousedown', this._onCloseMousedown);
+ } else {
+ this.closeElement.remove();
+ }
+
+ this.contentItem.tab = this;
+ this.contentItem.emit('tab', this);
+ this.contentItem.layoutManager.emit('tabCreated', this);
+
+ if (this.contentItem.isComponent) {
+ this.contentItem.container.tab = this;
+ this.contentItem.container.emit('tab', this);
+ }
+ };
+
+ /**
+ * The tab's html template
+ *
+ * @type {String}
+ */
+ lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
+ '<span class="lm_title"></span><div class="lm_close_tab"></div>' +
+ '<i class="lm_right"></i></li>';
+
+ lm.utils.copy(lm.controls.Tab.prototype, {
+
+ /**
+ * Sets the tab's title to the provided string and sets
+ * its title attribute to a pure text representation (without
+ * html tags) of the same string.
+ *
+ * @public
+ * @param {String} title can contain html
+ */
+ setTitle: function (title) {
+ this.element.attr('title', lm.utils.stripTags(title));
+ this.titleElement.html(title);
+ },
+
+ /**
+ * Sets this tab's active state. To programmatically
+ * switch tabs, use header.setActiveContentItem( item ) instead.
+ *
+ * @public
+ * @param {Boolean} isActive
+ */
+ setActive: function (isActive) {
+ if (isActive === this.isActive) {
+ return;
+ }
+ this.isActive = isActive;
+
+ if (isActive) {
+ this.element.addClass('lm_active');
+ } else {
+ this.element.removeClass('lm_active');
+ }
+ },
+
+ /**
+ * Destroys the tab
+ *
+ * @private
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this.element.off('mousedown touchstart', this._onTabClickFn);
+ this.closeElement.off('click touchstart', this._onCloseClickFn);
+ if (this._dragListener) {
+ this.contentItem.off('destroy', this._dragListener.destroy, this._dragListener);
+ this._dragListener.off('dragStart', this._onDragStart);
+ this._dragListener = null;
+ }
+ this.element.remove();
+ },
+
+ /**
+ * Callback for the DragListener
+ *
+ * @param {Number} x The tabs absolute x position
+ * @param {Number} y The tabs absolute y position
+ *
+ * @private
+ * @returns {void}
+ */
+ _onDragStart: function (x, y) {
+ if (this.contentItem.parent.isMaximised === true) {
+ this.contentItem.parent.toggleMaximise();
+ }
+ new lm.controls.DragProxy(
+ x,
+ y,
+ this._dragListener,
+ this._layoutManager,
+ this.contentItem,
+ this.header.parent
+ );
+ },
+
+ /**
+ * Callback when the tab is clicked
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onTabClick: function (event) {
+ // left mouse button or tap
+ if (event.button === 0 || event.type === 'touchstart') {
+ var activeContentItem = this.header.parent.getActiveContentItem();
+ if (this.contentItem !== activeContentItem) {
+ this.header.parent.setActiveContentItem(this.contentItem);
+ }
+
+ // middle mouse button
+ } else if (event.button === 1 && this.contentItem.config.isClosable) {
+ this._onCloseClick(event);
+ }
+ },
+
+ /**
+ * Callback when the tab's close button is
+ * clicked
+ *
+ * @param {jQuery DOM event} event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCloseClick: function (event) {
+ event.stopPropagation();
+ this.header.parent.removeChild(this.contentItem);
+ },
+
+
+ /**
+ * Callback to capture tab close button mousedown
+ * to prevent tab from activating.
+ *
+ * @param (jQuery DOM event) event
+ *
+ * @private
+ * @returns {void}
+ */
+ _onCloseMousedown: function (event) {
+ event.stopPropagation();
+ }
+ });
+
+ lm.controls.TransitionIndicator = function () {
+ this._element = $('<div class="lm_transition_indicator"></div>');
+ $(document.body).append(this._element);
+
+ this._toElement = null;
+ this._fromDimensions = null;
+ this._totalAnimationDuration = 200;
+ this._animationStartTime = null;
+ };
+
+ lm.utils.copy(lm.controls.TransitionIndicator.prototype, {
+ destroy: function () {
+ this._element.remove();
+ },
+
+ transitionElements: function (fromElement, toElement) {
+ /**
+ * TODO - This is not quite as cool as expected. Review.
+ */
+ return;
+ this._toElement = toElement;
+ this._animationStartTime = lm.utils.now();
+ this._fromDimensions = this._measure(fromElement);
+ this._fromDimensions.opacity = 0.8;
+ this._element.show().css(this._fromDimensions);
+ lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+ },
+
+ _nextAnimationFrame: function () {
+ var toDimensions = this._measure(this._toElement),
+ animationProgress = (lm.utils.now() - this._animationStartTime) / this._totalAnimationDuration,
+ currentFrameStyles = {},
+ cssProperty;
+
+ if (animationProgress >= 1) {
+ this._element.hide();
+ return;
+ }
+
+ toDimensions.opacity = 0;
+
+ for (cssProperty in this._fromDimensions) {
+ currentFrameStyles[cssProperty] = this._fromDimensions[cssProperty] +
+ (toDimensions[cssProperty] - this._fromDimensions[cssProperty]) *
+ animationProgress;
+ }
+
+ this._element.css(currentFrameStyles);
+ lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+ },
+
+ _measure: function (element) {
+ var offset = element.offset();
+
+ return {
+ left: offset.left,
+ top: offset.top,
+ width: element.outerWidth(),
+ height: element.outerHeight()
+ };
+ }
+ });
+ lm.errors.ConfigurationError = function (message, node) {
+ Error.call(this);
+
+ this.name = 'Configuration Error';
+ this.message = message;
+ this.node = node;
+ };
+
+ lm.errors.ConfigurationError.prototype = new Error();
+
+ /**
+ * This is the baseclass that all content items inherit from.
+ * Most methods provide a subset of what the sub-classes do.
+ *
+ * It also provides a number of functions for tree traversal
+ *
+ * @param {lm.LayoutManager} layoutManager
+ * @param {item node configuration} config
+ * @param {lm.item} parent
+ *
+ * @event stateChanged
+ * @event beforeItemDestroyed
+ * @event itemDestroyed
+ * @event itemCreated
+ * @event componentCreated
+ * @event rowCreated
+ * @event columnCreated
+ * @event stackCreated
+ *
+ * @constructor
+ */
+ lm.items.AbstractContentItem = function (layoutManager, config, parent) {
+ lm.utils.EventEmitter.call(this);
+
+ this.config = this._extendItemNode(config);
+ this.type = config.type;
+ this.contentItems = [];
+ this.parent = parent;
+
+ this.isInitialised = false;
+ this.isMaximised = false;
+ this.isRoot = false;
+ this.isRow = false;
+ this.isColumn = false;
+ this.isStack = false;
+ this.isComponent = false;
+
+ this.layoutManager = layoutManager;
+ this._pendingEventPropagations = {};
+ this._throttledEvents = ['stateChanged'];
+
+ this.on(lm.utils.EventEmitter.ALL_EVENT, this._propagateEvent, this);
+
+ if (config.content) {
+ this._createContentItems(config);
+ }
+ };
+
+ lm.utils.copy(lm.items.AbstractContentItem.prototype, {
+
+ /**
+ * Set the size of the component and its children, called recursively
+ *
+ * @abstract
+ * @returns void
+ */
+ setSize: function () {
+ throw new Error('Abstract Method');
+ },
+
+ /**
+ * Calls a method recursively downwards on the tree
+ *
+ * @param {String} functionName the name of the function to be called
+ * @param {[Array]}functionArguments optional arguments that are passed to every function
+ * @param {[bool]} bottomUp Call methods from bottom to top, defaults to false
+ * @param {[bool]} skipSelf Don't invoke the method on the class that calls it, defaults to false
+ *
+ * @returns {void}
+ */
+ callDownwards: function (functionName, functionArguments, bottomUp, skipSelf) {
+ var i;
+
+ if (bottomUp !== true && skipSelf !== true) {
+ this[functionName].apply(this, functionArguments || []);
+ }
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].callDownwards(functionName, functionArguments, bottomUp);
+ }
+ if (bottomUp === true && skipSelf !== true) {
+ this[functionName].apply(this, functionArguments || []);
+ }
+ },
+
+ /**
+ * Removes a child node (and its children) from the tree
+ *
+ * @param {lm.items.ContentItem} contentItem
+ *
+ * @returns {void}
+ */
+ removeChild: function (contentItem, keepChild) {
+
+ /*
+ * Get the position of the item that's to be removed within all content items this node contains
+ */
+ var index = lm.utils.indexOf(contentItem, this.contentItems);
+
+ /*
+ * Make sure the content item to be removed is actually a child of this item
+ */
+ if (index === -1) {
+ throw new Error('Can\'t remove child item. Unknown content item');
+ }
+
+ /**
+ * Call ._$destroy on the content item. This also calls ._$destroy on all its children
+ */
+ if (keepChild !== true) {
+ this.contentItems[index]._$destroy();
+ }
+
+ /**
+ * Remove the content item from this nodes array of children
+ */
+ this.contentItems.splice(index, 1);
+
+ /**
+ * Remove the item from the configuration
+ */
+ this.config.content.splice(index, 1);
+
+ /**
+ * If this node still contains other content items, adjust their size
+ */
+ if (this.contentItems.length > 0) {
+ this.callDownwards('setSize');
+
+ /**
+ * If this was the last content item, remove this node as well
+ */
+ } else if (!(this instanceof lm.items.Root) && this.config.isClosable === true) {
+ this.parent.removeChild(this);
+ }
+ },
+
+ /**
+ * Sets up the tree structure for the newly added child
+ * The responsibility for the actual DOM manipulations lies
+ * with the concrete item
+ *
+ * @param {lm.items.AbstractContentItem} contentItem
+ * @param {[Int]} index If omitted item will be appended
+ */
+ addChild: function (contentItem, index) {
+ if (index === undefined) {
+ index = this.contentItems.length;
+ }
+
+ this.contentItems.splice(index, 0, contentItem);
+
+ if (this.config.content === undefined) {
+ this.config.content = [];
+ }
+
+ this.config.content.splice(index, 0, contentItem.config);
+ contentItem.parent = this;
+
+ if (contentItem.parent.isInitialised === true && contentItem.isInitialised === false) {
+ contentItem._$init();
+ }
+ },
+
+ /**
+ * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
+ * some reason removes all event listeners, so isn't really an option.
+ *
+ * @param {lm.item.AbstractContentItem} oldChild
+ * @param {lm.item.AbstractContentItem} newChild
+ *
+ * @returns {void}
+ */
+ replaceChild: function (oldChild, newChild, _$destroyOldChild) {
+
+ newChild = this.layoutManager._$normalizeContentItem(newChild);
+
+ var index = lm.utils.indexOf(oldChild, this.contentItems),
+ parentNode = oldChild.element[0].parentNode;
+
+ if (index === -1) {
+ throw new Error('Can\'t replace child. oldChild is not child of this');
+ }
+
+ parentNode.replaceChild(newChild.element[0], oldChild.element[0]);
+
+ /*
+ * Optionally destroy the old content item
+ */
+ if (_$destroyOldChild === true) {
+ oldChild.parent = null;
+ oldChild._$destroy();
+ }
+
+ /*
+ * Wire the new contentItem into the tree
+ */
+ this.contentItems[index] = newChild;
+ newChild.parent = this;
+
+ /*
+ * Update tab reference
+ */
+ if (this.isStack) {
+ this.header.tabs[index].contentItem = newChild;
+ }
+
+ //TODO This doesn't update the config... refactor to leave item nodes untouched after creation
+ if (newChild.parent.isInitialised === true && newChild.isInitialised === false) {
+ newChild._$init();
+ }
+
+ this.callDownwards('setSize');
+ },
+
+ /**
+ * Convenience method.
+ * Shorthand for this.parent.removeChild( this )
+ *
+ * @returns {void}
+ */
+ remove: function () {
+ this.parent.removeChild(this);
+ },
+
+ /**
+ * Removes the component from the layout and creates a new
+ * browser window with the component and its children inside
+ *
+ * @returns {lm.controls.BrowserPopout}
+ */
+ popout: function () {
+ var browserPopout = this.layoutManager.createPopout(this);
+ this.emitBubblingEvent('stateChanged');
+ return browserPopout;
+ },
+
+ /**
+ * Maximises the Item or minimises it if it is already maximised
+ *
+ * @returns {void}
+ */
+ toggleMaximise: function (e) {
+ e && e.preventDefault();
+ if (this.isMaximised === true) {
+ this.layoutManager._$minimiseItem(this);
+ } else {
+ this.layoutManager._$maximiseItem(this);
+ }
+
+ this.isMaximised = !this.isMaximised;
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Selects the item if it is not already selected
+ *
+ * @returns {void}
+ */
+ select: function () {
+ if (this.layoutManager.selectedItem !== this) {
+ this.layoutManager.selectItem(this, true);
+ this.element.addClass('lm_selected');
+ }
+ },
+
+ /**
+ * De-selects the item if it is selected
+ *
+ * @returns {void}
+ */
+ deselect: function () {
+ if (this.layoutManager.selectedItem === this) {
+ this.layoutManager.selectedItem = null;
+ this.element.removeClass('lm_selected');
+ }
+ },
+
+ /**
+ * Set this component's title
+ *
+ * @public
+ * @param {String} title
+ *
+ * @returns {void}
+ */
+ setTitle: function (title) {
+ this.config.title = title;
+ this.emit('titleChanged', title);
+ this.emit('stateChanged');
+ },
+
+ /**
+ * Checks whether a provided id is present
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {Boolean} isPresent
+ */
+ hasId: function (id) {
+ if (!this.config.id) {
+ return false;
+ } else if (typeof this.config.id === 'string') {
+ return this.config.id === id;
+ } else if (this.config.id instanceof Array) {
+ return lm.utils.indexOf(id, this.config.id) !== -1;
+ }
+ },
+
+ /**
+ * Adds an id. Adds it as a string if the component doesn't
+ * have an id yet or creates/uses an array
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {void}
+ */
+ addId: function (id) {
+ if (this.hasId(id)) {
+ return;
+ }
+
+ if (!this.config.id) {
+ this.config.id = id;
+ } else if (typeof this.config.id === 'string') {
+ this.config.id = [this.config.id, id];
+ } else if (this.config.id instanceof Array) {
+ this.config.id.push(id);
+ }
+ },
+
+ /**
+ * Removes an existing id. Throws an error
+ * if the id is not present
+ *
+ * @public
+ * @param {String} id
+ *
+ * @returns {void}
+ */
+ removeId: function (id) {
+ if (!this.hasId(id)) {
+ throw new Error('Id not found');
+ }
+
+ if (typeof this.config.id === 'string') {
+ delete this.config.id;
+ } else if (this.config.id instanceof Array) {
+ var index = lm.utils.indexOf(id, this.config.id);
+ this.config.id.splice(index, 1);
+ }
+ },
+
+ /****************************************
+ * SELECTOR
+ ****************************************/
+ getItemsByFilter: function (filter) {
+ var result = [],
+ next = function (contentItem) {
+ for (var i = 0; i < contentItem.contentItems.length; i++) {
+
+ if (filter(contentItem.contentItems[i]) === true) {
+ result.push(contentItem.contentItems[i]);
+ }
+
+ next(contentItem.contentItems[i]);
+ }
+ };
+
+ next(this);
+ return result;
+ },
+
+ getItemsById: function (id) {
+ return this.getItemsByFilter(function (item) {
+ if (item.config.id instanceof Array) {
+ return lm.utils.indexOf(id, item.config.id) !== -1;
+ } else {
+ return item.config.id === id;
+ }
+ });
+ },
+
+ getItemsByType: function (type) {
+ return this._$getItemsByProperty('type', type);
+ },
+
+ getComponentsByName: function (componentName) {
+ var components = this._$getItemsByProperty('componentName', componentName),
+ instances = [],
+ i;
+
+ for (i = 0; i < components.length; i++) {
+ instances.push(components[i].instance);
+ }
+
+ return instances;
+ },
+
+ /****************************************
+ * PACKAGE PRIVATE
+ ****************************************/
+ _$getItemsByProperty: function (key, value) {
+ return this.getItemsByFilter(function (item) {
+ return item[key] === value;
+ });
+ },
+
+ _$setParent: function (parent) {
+ this.parent = parent;
+ },
+
+ _$highlightDropZone: function (x, y, area) {
+ this.layoutManager.dropTargetIndicator.highlightArea(area);
+ },
+
+ _$onDrop: function (contentItem) {
+ this.addChild(contentItem);
+ },
+
+ _$hide: function () {
+ this._callOnActiveComponents('hide');
+ this.element.hide();
+ this.layoutManager.updateSize();
+ },
+
+ _$show: function () {
+ this._callOnActiveComponents('show');
+ this.element.show();
+ this.layoutManager.updateSize();
+ },
+
+ _callOnActiveComponents: function (methodName) {
+ var stacks = this.getItemsByType('stack'),
+ activeContentItem,
+ i;
+
+ for (i = 0; i < stacks.length; i++) {
+ activeContentItem = stacks[i].getActiveContentItem();
+
+ if (activeContentItem && activeContentItem.isComponent) {
+ activeContentItem.container[methodName]();
+ }
+ }
+ },
+
+ /**
+ * Destroys this item ands its children
+ *
+ * @returns {void}
+ */
+ _$destroy: function () {
+ this.emitBubblingEvent('beforeItemDestroyed');
+ this.callDownwards('_$destroy', [], true, true);
+ this.element.remove();
+ this.emitBubblingEvent('itemDestroyed');
+ },
+
+ /**
+ * Returns the area the component currently occupies in the format
+ *
+ * {
+ * x1: int
+ * xy: int
+ * y1: int
+ * y2: int
+ * contentItem: contentItem
+ * }
+ */
+ _$getArea: function (element) {
+ element = element || this.element;
+
+ var offset = element.offset(),
+ width = element.width(),
+ height = element.height();
+
+ return {
+ x1: offset.left,
+ y1: offset.top,
+ x2: offset.left + width,
+ y2: offset.top + height,
+ surface: width * height,
+ contentItem: this
+ };
+ },
+
+ /**
+ * The tree of content items is created in two steps: First all content items are instantiated,
+ * then init is called recursively from top to bottem. This is the basic init function,
+ * it can be used, extended or overwritten by the content items
+ *
+ * Its behaviour depends on the content item
+ *
+ * @package private
+ *
+ * @returns {void}
+ */
+ _$init: function () {
+ var i;
+ this.setSize();
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.childElementContainer.append(this.contentItems[i].element);
+ }
+
+ this.isInitialised = true;
+ this.emitBubblingEvent('itemCreated');
+ this.emitBubblingEvent(this.type + 'Created');
+ },
+
+ /**
+ * Emit an event that bubbles up the item tree.
+ *
+ * @param {String} name The name of the event
+ *
+ * @returns {void}
+ */
+ emitBubblingEvent: function (name) {
+ var event = new lm.utils.BubblingEvent(name, this);
+ this.emit(name, event);
+ },
+
+ /**
+ * Private method, creates all content items for this node at initialisation time
+ * PLEASE NOTE, please see addChild for adding contentItems add runtime
+ * @private
+ * @param {configuration item node} config
+ *
+ * @returns {void}
+ */
+ _createContentItems: function (config) {
+ var oContentItem, i;
+
+ if (!(config.content instanceof Array)) {
+ throw new lm.errors.ConfigurationError('content must be an Array', config);
+ }
+
+ for (i = 0; i < config.content.length; i++) {
+ oContentItem = this.layoutManager.createContentItem(config.content[i], this);
+ this.contentItems.push(oContentItem);
+ }
+ },
+
+ /**
+ * Extends an item configuration node with default settings
+ * @private
+ * @param {configuration item node} config
+ *
+ * @returns {configuration item node} extended config
+ */
+ _extendItemNode: function (config) {
+
+ for (var key in lm.config.itemDefaultConfig) {
+ if (config[key] === undefined) {
+ config[key] = lm.config.itemDefaultConfig[key];
+ }
+ }
+
+ return config;
+ },
+
+ /**
+ * Called for every event on the item tree. Decides whether the event is a bubbling
+ * event and propagates it to its parent
+ *
+ * @param {String} name the name of the event
+ * @param {lm.utils.BubblingEvent} event
+ *
+ * @returns {void}
+ */
+ _propagateEvent: function (name, event) {
+ if (event instanceof lm.utils.BubblingEvent &&
+ event.isPropagationStopped === false &&
+ this.isInitialised === true) {
+
+ /**
+ * In some cases (e.g. if an element is created from a DragSource) it
+ * doesn't have a parent and is not below root. If that's the case
+ * propagate the bubbling event from the top level of the substree directly
+ * to the layoutManager
+ */
+ if (this.isRoot === false && this.parent) {
+ this.parent.emit.apply(this.parent, Array.prototype.slice.call(arguments, 0));
+ } else {
+ this._scheduleEventPropagationToLayoutManager(name, event);
+ }
+ }
+ },
+
+ /**
+ * All raw events bubble up to the root element. Some events that
+ * are propagated to - and emitted by - the layoutManager however are
+ * only string-based, batched and sanitized to make them more usable
+ *
+ * @param {String} name the name of the event
+ *
+ * @private
+ * @returns {void}
+ */
+ _scheduleEventPropagationToLayoutManager: function (name, event) {
+ if (lm.utils.indexOf(name, this._throttledEvents) === -1) {
+ this.layoutManager.emit(name, event.origin);
+ } else {
+ if (this._pendingEventPropagations[name] !== true) {
+ this._pendingEventPropagations[name] = true;
+ lm.utils.animFrame(lm.utils.fnBind(this._propagateEventToLayoutManager, this, [name, event]));
+ }
+ }
+
+ },
+
+ /**
+ * Callback for events scheduled by _scheduleEventPropagationToLayoutManager
+ *
+ * @param {String} name the name of the event
+ *
+ * @private
+ * @returns {void}
+ */
+ _propagateEventToLayoutManager: function (name, event) {
+ this._pendingEventPropagations[name] = false;
+ this.layoutManager.emit(name, event);
+ }
+ });
+
+ /**
+ * @param {[type]} layoutManager [description]
+ * @param {[type]} config [description]
+ * @param {[type]} parent [description]
+ */
+ lm.items.Component = function (layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ var ComponentConstructor = layoutManager.getComponent(this.config.componentName),
+ componentConfig = $.extend(true, {}, this.config.componentState || {});
+
+ componentConfig.componentName = this.config.componentName;
+ this.componentName = this.config.componentName;
+
+ if (this.config.title === '') {
+ this.config.title = this.config.componentName;
+ }
+
+ this.isComponent = true;
+ this.container = new lm.container.ItemContainer(this.config, this, layoutManager);
+ this.instance = new ComponentConstructor(this.container, componentConfig);
+ this.element = this.container._element;
+ };
+
+ lm.utils.extend(lm.items.Component, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Component.prototype, {
+
+ close: function () {
+ this.parent.removeChild(this);
+ },
+
+ setSize: function () {
+ if (this.element.is(':visible')) {
+ // Do not update size of hidden components to prevent unwanted reflows
+ this.container._$setSize(this.element.width(), this.element.height());
+ }
+ },
+
+ _$init: function () {
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+ this.container.emit('open');
+ },
+
+ _$hide: function () {
+ this.container.hide();
+ lm.items.AbstractContentItem.prototype._$hide.call(this);
+ },
+
+ _$show: function () {
+ this.container.show();
+ lm.items.AbstractContentItem.prototype._$show.call(this);
+ },
+
+ _$shown: function () {
+ this.container.shown();
+ lm.items.AbstractContentItem.prototype._$shown.call(this);
+ },
+
+ _$destroy: function () {
+ this.container.emit('destroy', this);
+ lm.items.AbstractContentItem.prototype._$destroy.call(this);
+ },
+
+ /**
+ * Dragging onto a component directly is not an option
+ *
+ * @returns null
+ */
+ _$getArea: function () {
+ return null;
+ }
+ });
+
+ lm.items.Root = function (layoutManager, config, containerElement) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, null);
+ this.isRoot = true;
+ this.type = 'root';
+ this.element = $('<div class="lm_goldenlayout lm_item lm_root"></div>');
+ this.childElementContainer = this.element;
+ this._containerElement = containerElement;
+ this._containerElement.append(this.element);
+ };
+
+ lm.utils.extend(lm.items.Root, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Root.prototype, {
+ addChild: function (contentItem) {
+ if (this.contentItems.length > 0) {
+ throw new Error('Root node can only have a single child');
+ }
+
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+ this.childElementContainer.append(contentItem.element);
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem);
+
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ setSize: function (width, height) {
+ width = (typeof width === 'undefined') ? this._containerElement.width() : width;
+ height = (typeof height === 'undefined') ? this._containerElement.height() : height;
+
+ this.element.width(width);
+ this.element.height(height);
+
+ /*
+ * Root can be empty
+ */
+ if (this.contentItems[0]) {
+ this.contentItems[0].element.width(width);
+ this.contentItems[0].element.height(height);
+ }
+ },
+ _$highlightDropZone: function (x, y, area) {
+ this.layoutManager.tabDropPlaceholder.remove();
+ lm.items.AbstractContentItem.prototype._$highlightDropZone.apply(this, arguments);
+ },
+
+ _$onDrop: function (contentItem, area) {
+ var stack;
+
+ if (contentItem.isComponent) {
+ stack = this.layoutManager.createContentItem({
+ type: 'stack',
+ header: contentItem.config.header || {}
+ }, this);
+ stack._$init();
+ stack.addChild(contentItem);
+ contentItem = stack;
+ }
+
+ if (!this.contentItems.length) {
+ this.addChild(contentItem);
+ } else {
+ var type = area.side[0] == 'x' ? 'row' : 'column';
+ var dimension = area.side[0] == 'x' ? 'width' : 'height';
+ var insertBefore = area.side[1] == '2';
+ var column = this.contentItems[0];
+ if (!column instanceof lm.items.RowOrColumn || column.type != type) {
+ var rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+ this.replaceChild(column, rowOrColumn);
+ rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ rowOrColumn.addChild(column, insertBefore ? undefined : 0, true);
+ column.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ rowOrColumn.callDownwards('setSize');
+ } else {
+ var sibbling = column.contentItems[insertBefore ? 0 : column.contentItems.length - 1]
+ column.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ sibbling.config[dimension] *= 0.5;
+ contentItem.config[dimension] = sibbling.config[dimension];
+ column.callDownwards('setSize');
+ }
+ }
+ }
+ });
+
+
+
+ lm.items.RowOrColumn = function (isColumn, layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ this.isRow = !isColumn;
+ this.isColumn = isColumn;
+
+ this.element = $('<div class="lm_item lm_' + (isColumn ? 'column' : 'row') + '"></div>');
+ this.childElementContainer = this.element;
+ this._splitterSize = layoutManager.config.dimensions.borderWidth;
+ this._splitterGrabSize = layoutManager.config.dimensions.borderGrabWidth;
+ this._isColumn = isColumn;
+ this._dimension = isColumn ? 'height' : 'width';
+ this._splitter = [];
+ this._splitterPosition = null;
+ this._splitterMinPosition = null;
+ this._splitterMaxPosition = null;
+ };
+
+ lm.utils.extend(lm.items.RowOrColumn, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.RowOrColumn.prototype, {
+
+ /**
+ * Add a new contentItem to the Row or Column
+ *
+ * @param {lm.item.AbstractContentItem} contentItem
+ * @param {[int]} index The position of the new item within the Row or Column.
+ * If no index is provided the item will be added to the end
+ * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in
+ * an inconsistent state and is only intended to be used if multiple
+ * children need to be added in one go and resize is called afterwards
+ *
+ * @returns {void}
+ */
+ addChild: function (contentItem, index, _$suspendResize) {
+
+ var newItemSize, itemSize, i, splitterElement;
+
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+
+ if (index === undefined) {
+ index = this.contentItems.length;
+ }
+
+ if (this.contentItems.length > 0) {
+ splitterElement = this._createSplitter(Math.max(0, index - 1)).element;
+
+ if (index > 0) {
+ this.contentItems[index - 1].element.after(splitterElement);
+ splitterElement.after(contentItem.element);
+ } else {
+ this.contentItems[0].element.before(splitterElement);
+ splitterElement.before(contentItem.element);
+ }
+ } else {
+ this.childElementContainer.append(contentItem.element);
+ }
+
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+
+ let fixedItemSize = 0;
+ let variableItemCount = 0;
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ fixedItemSize += this.contentItems[i].config[this._dimension];
+ else variableItemCount++;
+ }
+
+ newItemSize = (1 / variableItemCount) * (100 - fixedItemSize);
+
+ if (_$suspendResize === true) {
+ this.emitBubblingEvent('stateChanged');
+ return;
+ }
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ ;
+ else if (this.contentItems[i] === contentItem) {
+ contentItem.config[this._dimension] = newItemSize;
+ } else {
+ itemSize = this.contentItems[i].config[this._dimension] *= (100 - newItemSize - fixedItemSize) / (100 - fixedItemSize);
+ this.contentItems[i].config[this._dimension] = itemSize;
+ }
+ }
+
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+
+ },
+
+ /**
+ * Removes a child of this element
+ *
+ * @param {lm.items.AbstractContentItem} contentItem
+ * @param {boolean} keepChild If true the child will be removed, but not destroyed
+ *
+ * @returns {void}
+ */
+ removeChild: function (contentItem, keepChild) {
+ var removedItemSize = contentItem.config[this._dimension],
+ index = lm.utils.indexOf(contentItem, this.contentItems),
+ splitterIndex = Math.max(index - 1, 0),
+ i,
+ childItem;
+
+ if (index === -1) {
+ throw new Error('Can\'t remove child. ContentItem is not child of this Row or Column');
+ }
+
+ /**
+ * Remove the splitter before the item or after if the item happens
+ * to be the first in the row/column
+ */
+ if (this._splitter[splitterIndex]) {
+ this._splitter[splitterIndex]._$destroy();
+ this._splitter.splice(splitterIndex, 1);
+ }
+
+ let fixedItemSize = 0;
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ fixedItemSize += this.contentItems[i].config[this._dimension];
+ }
+ /**
+ * Allocate the space that the removed item occupied to the remaining items
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config.fixed)
+ ;
+ else if (this.contentItems[i] !== contentItem) {
+ this.contentItems[i].config[this._dimension] *= (100 - fixedItemSize) / (100 - removedItemSize - fixedItemSize);
+ }
+ }
+
+ lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+
+ if (this.contentItems.length === 1 && this.config.isClosable === true) {
+ childItem = this.contentItems[0];
+ this.contentItems = [];
+ this.parent.replaceChild(this, childItem, true);
+ } else {
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ }
+ },
+
+ /**
+ * Replaces a child of this Row or Column with another contentItem
+ *
+ * @param {lm.items.AbstractContentItem} oldChild
+ * @param {lm.items.AbstractContentItem} newChild
+ *
+ * @returns {void}
+ */
+ replaceChild: function (oldChild, newChild) {
+ var size = oldChild.config[this._dimension];
+ lm.items.AbstractContentItem.prototype.replaceChild.call(this, oldChild, newChild);
+ newChild.config[this._dimension] = size;
+ this.callDownwards('setSize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Called whenever the dimensions of this item or one of its parents change
+ *
+ * @returns {void}
+ */
+ setSize: function () {
+ if (this.contentItems.length > 0) {
+ this._calculateRelativeSizes();
+ this._setAbsoluteSizes();
+ }
+ this.emitBubblingEvent('stateChanged');
+ this.emit('resize');
+ },
+
+ /**
+ * Invoked recursively by the layout manager. AbstractContentItem.init appends
+ * the contentItem's DOM elements to the container, RowOrColumn init adds splitters
+ * in between them
+ *
+ * @package private
+ * @override AbstractContentItem._$init
+ * @returns {void}
+ */
+ _$init: function () {
+ if (this.isInitialised === true) return;
+
+ var i;
+
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+
+ for (i = 0; i < this.contentItems.length - 1; i++) {
+ this.contentItems[i].element.after(this._createSplitter(i).element);
+ }
+ },
+
+ /**
+ * Turns the relative sizes calculated by _calculateRelativeSizes into
+ * absolute pixel values and applies them to the children's DOM elements
+ *
+ * Assigns additional pixels to counteract Math.floor
+ *
+ * @private
+ * @returns {void}
+ */
+ _setAbsoluteSizes: function () {
+ var i,
+ sizeData = this._calculateAbsoluteSizes();
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (sizeData.additionalPixel - i > 0) {
+ sizeData.itemSizes[i]++;
+ }
+
+ if (this._isColumn) {
+ this.contentItems[i].element.width(sizeData.totalWidth);
+ this.contentItems[i].element.height(sizeData.itemSizes[i]);
+ } else {
+ this.contentItems[i].element.width(sizeData.itemSizes[i]);
+ this.contentItems[i].element.height(sizeData.totalHeight);
+ }
+ }
+ },
+
+ /**
+ * Calculates the absolute sizes of all of the children of this Item.
+ * @returns {object} - Set with absolute sizes and additional pixels.
+ */
+ _calculateAbsoluteSizes: function () {
+ var i,
+ totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize,
+ totalWidth = this.element.width(),
+ totalHeight = this.element.height(),
+ totalAssigned = 0,
+ additionalPixel,
+ itemSize,
+ itemSizes = [];
+
+ if (this._isColumn) {
+ totalHeight -= totalSplitterSize;
+ } else {
+ totalWidth -= totalSplitterSize;
+ }
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this._isColumn) {
+ itemSize = Math.floor(totalHeight * (this.contentItems[i].config.height / 100));
+ } else {
+ itemSize = Math.floor(totalWidth * (this.contentItems[i].config.width / 100));
+ }
+
+ totalAssigned += itemSize;
+ itemSizes.push(itemSize);
+ }
+
+ additionalPixel = Math.floor((this._isColumn ? totalHeight : totalWidth) - totalAssigned);
+
+ return {
+ itemSizes: itemSizes,
+ additionalPixel: additionalPixel,
+ totalWidth: totalWidth,
+ totalHeight: totalHeight
+ };
+ },
+
+ /**
+ * Calculates the relative sizes of all children of this Item. The logic
+ * is as follows:
+ *
+ * - Add up the total size of all items that have a configured size
+ *
+ * - If the total == 100 (check for floating point errors)
+ * Excellent, job done
+ *
+ * - If the total is > 100,
+ * set the size of items without set dimensions to 1/3 and add this to the total
+ * set the size off all items so that the total is hundred relative to their original size
+ *
+ * - If the total is < 100
+ * If there are items without set dimensions, distribute the remainder to 100 evenly between them
+ * If there are no items without set dimensions, increase all items sizes relative to
+ * their original size so that they add up to 100
+ *
+ * @private
+ * @returns {void}
+ */
+ _calculateRelativeSizes: function () {
+
+ var i,
+ total = 0,
+ itemsWithoutSetDimension = [],
+ dimension = this._isColumn ? 'height' : 'width';
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ if (this.contentItems[i].config[dimension] !== undefined) {
+ total += this.contentItems[i].config[dimension];
+ } else {
+ itemsWithoutSetDimension.push(this.contentItems[i]);
+ }
+ }
+
+ /**
+ * Everything adds up to hundred, all good :-)
+ */
+ if (Math.round(total) === 100) {
+ this._respectMinItemWidth();
+ return;
+ }
+
+ /**
+ * Allocate the remaining size to the items without a set dimension
+ */
+ if (Math.round(total) < 100 && itemsWithoutSetDimension.length > 0) {
+ for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+ itemsWithoutSetDimension[i].config[dimension] = (100 - total) / itemsWithoutSetDimension.length;
+ }
+ this._respectMinItemWidth();
+ return;
+ }
+
+ /**
+ * If the total is > 100, but there are also items without a set dimension left, assing 50
+ * as their dimension and add it to the total
+ *
+ * This will be reset in the next step
+ */
+ if (Math.round(total) > 100) {
+ for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+ itemsWithoutSetDimension[i].config[dimension] = 50;
+ total += 50;
+ }
+ }
+
+ /**
+ * Set every items size relative to 100 relative to its size to total
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].config[dimension] = (this.contentItems[i].config[dimension] / total) * 100;
+ }
+
+ this._respectMinItemWidth();
+ },
+
+ /**
+ * Adjusts the column widths to respect the dimensions minItemWidth if set.
+ * @returns {}
+ */
+ _respectMinItemWidth: function () {
+ var minItemWidth = this.layoutManager.config.dimensions ? (this.layoutManager.config.dimensions.minItemWidth || 0) : 0,
+ sizeData = null,
+ entriesOverMin = [],
+ totalOverMin = 0,
+ totalUnderMin = 0,
+ remainingWidth = 0,
+ itemSize = 0,
+ contentItem = null,
+ reducePercent,
+ reducedWidth,
+ allEntries = [],
+ entry;
+
+ if (this._isColumn || !minItemWidth || this.contentItems.length <= 1) {
+ return;
+ }
+
+ sizeData = this._calculateAbsoluteSizes();
+
+ /**
+ * Figure out how much we are under the min item size total and how much room we have to use.
+ */
+ for (var i = 0; i < this.contentItems.length; i++) {
+
+ contentItem = this.contentItems[i];
+ itemSize = sizeData.itemSizes[i];
+
+ if (itemSize < minItemWidth) {
+ totalUnderMin += minItemWidth - itemSize;
+ entry = { width: minItemWidth };
+
+ }
+ else {
+ totalOverMin += itemSize - minItemWidth;
+ entry = { width: itemSize };
+ entriesOverMin.push(entry);
+ }
+
+ allEntries.push(entry);
+ }
+
+ /**
+ * If there is nothing under min, or there is not enough over to make up the difference, do nothing.
+ */
+ if (totalUnderMin === 0 || totalUnderMin > totalOverMin) {
+ return;
+ }
+
+ /**
+ * Evenly reduce all columns that are over the min item width to make up the difference.
+ */
+ reducePercent = totalUnderMin / totalOverMin;
+ remainingWidth = totalUnderMin;
+ for (i = 0; i < entriesOverMin.length; i++) {
+ entry = entriesOverMin[i];
+ reducedWidth = Math.round((entry.width - minItemWidth) * reducePercent);
+ remainingWidth -= reducedWidth;
+ entry.width -= reducedWidth;
+ }
+
+ /**
+ * Take anything remaining from the last item.
+ */
+ if (remainingWidth !== 0) {
+ allEntries[allEntries.length - 1].width -= remainingWidth;
+ }
+
+ /**
+ * Set every items size relative to 100 relative to its size to total
+ */
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].config.width = (allEntries[i].width / sizeData.totalWidth) * 100;
+ }
+ },
+
+ /**
+ * Instantiates a new lm.controls.Splitter, binds events to it and adds
+ * it to the array of splitters at the position specified as the index argument
+ *
+ * What it doesn't do though is append the splitter to the DOM
+ *
+ * @param {Int} index The position of the splitter
+ *
+ * @returns {lm.controls.Splitter}
+ */
+ _createSplitter: function (index) {
+ var splitter;
+ splitter = new lm.controls.Splitter(this._isColumn, this._splitterSize, this._splitterGrabSize);
+ splitter.on('drag', lm.utils.fnBind(this._onSplitterDrag, this, [splitter]), this);
+ splitter.on('dragStop', lm.utils.fnBind(this._onSplitterDragStop, this, [splitter]), this);
+ splitter.on('dragStart', lm.utils.fnBind(this._onSplitterDragStart, this, [splitter]), this);
+ this._splitter.splice(index, 0, splitter);
+ return splitter;
+ },
+
+ /**
+ * Locates the instance of lm.controls.Splitter in the array of
+ * registered splitters and returns a map containing the contentItem
+ * before and after the splitters, both of which are affected if the
+ * splitter is moved
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {Object} A map of contentItems that the splitter affects
+ */
+ _getItemsForSplitter: function (splitter) {
+ var index = lm.utils.indexOf(splitter, this._splitter);
+
+ return {
+ before: this.contentItems[index],
+ after: this.contentItems[index + 1]
+ };
+ },
+
+ /**
+ * Gets the minimum dimensions for the given item configuration array
+ * @param item
+ * @private
+ */
+ _getMinimumDimensions: function (arr) {
+ var minWidth = 0, minHeight = 0;
+
+ for (var i = 0; i < arr.length; ++i) {
+ minWidth = Math.max(arr[i].minWidth || 0, minWidth);
+ minHeight = Math.max(arr[i].minHeight || 0, minHeight);
+ }
+
+ return { horizontal: minWidth, vertical: minHeight };
+ },
+
+ /**
+ * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
+ * movement area once (so that it doesn't need calculating on every mousemove event)
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {void}
+ */
+ _onSplitterDragStart: function (splitter) {
+ var items = this._getItemsForSplitter(splitter),
+ minSize = this.layoutManager.config.dimensions[this._isColumn ? 'minItemHeight' : 'minItemWidth'];
+
+ var beforeMinDim = this._getMinimumDimensions(items.before.config.content);
+ var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
+
+ var afterMinDim = this._getMinimumDimensions(items.after.config.content);
+ var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
+
+ this._splitterPosition = 0;
+ this._splitterMinPosition = -1 * (items.before.element[this._dimension]() - (beforeMinSize || minSize));
+ this._splitterMaxPosition = items.after.element[this._dimension]() - (afterMinSize || minSize);
+ },
+
+ /**
+ * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
+ * but not the sizes of the elements the splitter controls in order to minimize resize events
+ *
+ * @param {lm.controls.Splitter} splitter
+ * @param {Int} offsetX Relative pixel values to the splitters original position. Can be negative
+ * @param {Int} offsetY Relative pixel values to the splitters original position. Can be negative
+ *
+ * @returns {void}
+ */
+ _onSplitterDrag: function (splitter, offsetX, offsetY) {
+ var offset = this._isColumn ? offsetY : offsetX;
+
+ if (offset > this._splitterMinPosition && offset < this._splitterMaxPosition) {
+ this._splitterPosition = offset;
+ splitter.element.css(this._isColumn ? 'top' : 'left', offset);
+ }
+ },
+
+ /**
+ * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
+ * and applies the new sizes to the elements before and after the splitter and their children
+ * on the next animation frame
+ *
+ * @param {lm.controls.Splitter} splitter
+ *
+ * @returns {void}
+ */
+ _onSplitterDragStop: function (splitter) {
+
+ var items = this._getItemsForSplitter(splitter),
+ sizeBefore = items.before.element[this._dimension](),
+ sizeAfter = items.after.element[this._dimension](),
+ splitterPositionInRange = (this._splitterPosition + sizeBefore) / (sizeBefore + sizeAfter),
+ totalRelativeSize = items.before.config[this._dimension] + items.after.config[this._dimension];
+
+ items.before.config[this._dimension] = splitterPositionInRange * totalRelativeSize;
+ items.after.config[this._dimension] = (1 - splitterPositionInRange) * totalRelativeSize;
+
+ splitter.element.css({
+ 'top': 0,
+ 'left': 0
+ });
+
+ lm.utils.animFrame(lm.utils.fnBind(this.callDownwards, this, ['setSize']));
+ }
+ });
+
+ lm.items.Stack = function (layoutManager, config, parent) {
+ lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+ this.element = $('<div class="lm_item lm_stack"></div>');
+ this._activeContentItem = null;
+ var cfg = layoutManager.config;
+ this._header = { // defaults' reconstruction from old configuration style
+ show: cfg.settings.hasHeaders === true && config.hasHeaders !== false,
+ popout: cfg.settings.showPopoutIcon && cfg.labels.popout,
+ maximise: cfg.settings.showMaximiseIcon && cfg.labels.maximise,
+ close: cfg.settings.showCloseIcon && cfg.labels.close,
+ minimise: cfg.labels.minimise,
+ };
+ if (cfg.header) // load simplified version of header configuration (https://github.com/deepstreamIO/golden-layout/pull/245)
+ lm.utils.copy(this._header, cfg.header);
+ if (config.header) // load from stack
+ lm.utils.copy(this._header, config.header);
+ if (config.content && config.content[0] && config.content[0].header) // load from component if stack omitted
+ lm.utils.copy(this._header, config.content[0].header);
+
+ this._dropZones = {};
+ this._dropSegment = null;
+ this._contentAreaDimensions = null;
+ this._dropIndex = null;
+
+ this.isStack = true;
+
+ this.childElementContainer = $('<div class="lm_items"></div>');
+ this.header = new lm.controls.Header(layoutManager, this);
+
+ this.element.append(this.header.element);
+ this.element.append(this.childElementContainer);
+ this._setupHeaderPosition();
+ this._$validateClosability();
+ };
+
+ lm.utils.extend(lm.items.Stack, lm.items.AbstractContentItem);
+
+ lm.utils.copy(lm.items.Stack.prototype, {
+
+ setSize: function () {
+ var i,
+ headerSize = this._header.show ? this.layoutManager.config.dimensions.headerHeight : 0,
+ contentWidth = this.element.width() - (this._sided ? headerSize : 0),
+ contentHeight = this.element.height() - (!this._sided ? headerSize : 0);
+
+ this.childElementContainer.width(contentWidth);
+ this.childElementContainer.height(contentHeight);
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.contentItems[i].element.width(contentWidth).height(contentHeight);
+ }
+ this.emit('resize');
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ _$init: function () {
+ var i, initialItem;
+
+ if (this.isInitialised === true) return;
+
+ lm.items.AbstractContentItem.prototype._$init.call(this);
+
+ for (i = 0; i < this.contentItems.length; i++) {
+ this.header.createTab(this.contentItems[i]);
+ this.contentItems[i]._$hide();
+ }
+
+ if (this.contentItems.length > 0) {
+ initialItem = this.contentItems[this.config.activeItemIndex || 0];
+
+ if (!initialItem) {
+ throw new Error('Configured activeItemIndex out of bounds');
+ }
+
+ this.setActiveContentItem(initialItem);
+ }
+ },
+
+ setActiveContentItem: function (contentItem) {
+ if (lm.utils.indexOf(contentItem, this.contentItems) === -1) {
+ throw new Error('contentItem is not a child of this stack');
+ }
+
+ if (this._activeContentItem !== null) {
+ this._activeContentItem._$hide();
+ }
+
+ this._activeContentItem = contentItem;
+ this.header.setActiveContentItem(contentItem);
+ contentItem._$show();
+ this.emit('activeContentItemChanged', contentItem);
+ this.layoutManager.emit('activeContentItemChanged', contentItem);
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ getActiveContentItem: function () {
+ return this.header.activeContentItem;
+ },
+
+ addChild: function (contentItem, index) {
+ contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+ lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+ this.childElementContainer.append(contentItem.element);
+ this.header.createTab(contentItem, index);
+ this.setActiveContentItem(contentItem);
+ this.callDownwards('setSize');
+ this._$validateClosability();
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ removeChild: function (contentItem, keepChild) {
+ var index = lm.utils.indexOf(contentItem, this.contentItems);
+ lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+ this.header.removeTab(contentItem);
+ if (this.header.activeContentItem === contentItem) {
+ if (this.contentItems.length > 0) {
+ this.setActiveContentItem(this.contentItems[Math.max(index - 1, 0)]);
+ } else {
+ this._activeContentItem = null;
+ }
+ }
+
+ this._$validateClosability();
+ this.emitBubblingEvent('stateChanged');
+ },
+
+ /**
+ * Validates that the stack is still closable or not. If a stack is able
+ * to close, but has a non closable component added to it, the stack is no
+ * longer closable until all components are closable.
+ *
+ * @returns {void}
+ */
+ _$validateClosability: function () {
+ var contentItem,
+ isClosable,
+ len,
+ i;
+
+ isClosable = this.header._isClosable();
+
+ for (i = 0, len = this.contentItems.length; i < len; i++) {
+ if (!isClosable) {
+ break;
+ }
+
+ isClosable = this.contentItems[i].config.isClosable;
+ }
+
+ this.header._$setClosable(isClosable);
+ },
+
+ _$destroy: function () {
+ lm.items.AbstractContentItem.prototype._$destroy.call(this);
+ this.header._$destroy();
+ },
+
+
+ /**
+ * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack.
+ *
+ * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area
+ * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case
+ * is relatively clear: We add the item to the existing stack... job done (might be good to have
+ * tab reordering at some point, but lets not sweat it right now)
+ *
+ * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the
+ * top or bottom region we need to create a new column and place the items accordingly.
+ * Unless, of course if the stack is already within a column... in which case we want
+ * to add the newly created item to the existing column...
+ * either prepend or append it, depending on wether its top or bottom.
+ *
+ * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen
+ * (left, top, right, bottom) * is child of the right parent (row, column) + header drop
+ *
+ * @param {lm.item} contentItem
+ *
+ * @returns {void}
+ */
+ _$onDrop: function (contentItem) {
+
+ /*
+ * The item was dropped on the header area. Just add it as a child of this stack and
+ * get the hell out of this logic
+ */
+ if (this._dropSegment === 'header') {
+ this._resetHeaderDropZone();
+ this.addChild(contentItem, this._dropIndex);
+ return;
+ }
+
+ /*
+ * The stack is empty. Let's just add the element.
+ */
+ if (this._dropSegment === 'body') {
+ this.addChild(contentItem);
+ return;
+ }
+
+ /*
+ * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's
+ * aggregate some conditions to make the if statements later on more readable
+ */
+ var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom',
+ isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right',
+ insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left',
+ hasCorrectParent = (isVertical && this.parent.isColumn) || (isHorizontal && this.parent.isRow),
+ type = isVertical ? 'column' : 'row',
+ dimension = isVertical ? 'height' : 'width',
+ index,
+ stack,
+ rowOrColumn;
+
+ /*
+ * The content item can be either a component or a stack. If it is a component, wrap it into a stack
+ */
+ if (contentItem.isComponent) {
+ stack = this.layoutManager.createContentItem({
+ type: 'stack',
+ header: contentItem.config.header || {}
+ }, this);
+ stack._$init();
+ stack.addChild(contentItem);
+ contentItem = stack;
+ }
+
+ /*
+ * If the item is dropped on top or bottom of a column or left and right of a row, it's already
+ * layd out in the correct way. Just add it as a child
+ */
+ if (hasCorrectParent) {
+ index = lm.utils.indexOf(this, this.parent.contentItems);
+ this.parent.addChild(contentItem, insertBefore ? index : index + 1, true);
+ this.config[dimension] *= 0.5;
+ contentItem.config[dimension] = this.config[dimension];
+ this.parent.callDownwards('setSize');
+ /*
+ * This handles items that are dropped on top or bottom of a row or left / right of a column. We need
+ * to create the appropriate contentItem for them to live in
+ */
+ } else {
+ type = isVertical ? 'column' : 'row';
+ rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+ this.parent.replaceChild(this, rowOrColumn);
+
+ rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
+
+ this.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ rowOrColumn.callDownwards('setSize');
+ }
+ },
+
+ /**
+ * If the user hovers above the header part of the stack, indicate drop positions for tabs.
+ * otherwise indicate which segment of the body the dragged item would be dropped on
+ *
+ * @param {Int} x Absolute Screen X
+ * @param {Int} y Absolute Screen Y
+ *
+ * @returns {void}
+ */
+ _$highlightDropZone: function (x, y) {
+ var segment, area;
+
+ for (segment in this._contentAreaDimensions) {
+ area = this._contentAreaDimensions[segment].hoverArea;
+
+ if (area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y) {
+
+ if (segment === 'header') {
+ this._dropSegment = 'header';
+ this._highlightHeaderDropZone(this._sided ? y : x);
+ } else {
+ this._resetHeaderDropZone();
+ this._highlightBodyDropZone(segment);
+ }
+
+ return;
+ }
+ }
+ },
+
+ _$getArea: function () {
+ if (this.element.is(':visible') === false) {
+ return null;
+ }
+
+ var getArea = lm.items.AbstractContentItem.prototype._$getArea,
+ headerArea = getArea.call(this, this.header.element),
+ contentArea = getArea.call(this, this.childElementContainer),
+ contentWidth = contentArea.x2 - contentArea.x1,
+ contentHeight = contentArea.y2 - contentArea.y1;
+
+ this._contentAreaDimensions = {
+ header: {
+ hoverArea: {
+ x1: headerArea.x1,
+ y1: headerArea.y1,
+ x2: headerArea.x2,
+ y2: headerArea.y2
+ },
+ highlightArea: {
+ x1: headerArea.x1,
+ y1: headerArea.y1,
+ x2: headerArea.x2,
+ y2: headerArea.y2
+ }
+ }
+ };
+
+ /**
+ * If this Stack is a parent to rows, columns or other stacks only its
+ * header is a valid dropzone.
+ */
+ if (this._activeContentItem && this._activeContentItem.isComponent === false) {
+ return headerArea;
+ }
+
+ /**
+ * Highlight the entire body if the stack is empty
+ */
+ if (this.contentItems.length === 0) {
+
+ this._contentAreaDimensions.body = {
+ hoverArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ return getArea.call(this, this.element);
+ }
+
+ this._contentAreaDimensions.left = {
+ hoverArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.25,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.5,
+ y2: contentArea.y2
+ }
+ };
+
+ this._contentAreaDimensions.top = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.25,
+ y1: contentArea.y1,
+ x2: contentArea.x1 + contentWidth * 0.75,
+ y2: contentArea.y1 + contentHeight * 0.5
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y1 + contentHeight * 0.5
+ }
+ };
+
+ this._contentAreaDimensions.right = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.75,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1 + contentWidth * 0.5,
+ y1: contentArea.y1,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ this._contentAreaDimensions.bottom = {
+ hoverArea: {
+ x1: contentArea.x1 + contentWidth * 0.25,
+ y1: contentArea.y1 + contentHeight * 0.5,
+ x2: contentArea.x1 + contentWidth * 0.75,
+ y2: contentArea.y2
+ },
+ highlightArea: {
+ x1: contentArea.x1,
+ y1: contentArea.y1 + contentHeight * 0.5,
+ x2: contentArea.x2,
+ y2: contentArea.y2
+ }
+ };
+
+ return getArea.call(this, this.element);
+ },
+
+ _highlightHeaderDropZone: function (x) {
+ var i,
+ tabElement,
+ tabsLength = this.header.tabs.length,
+ isAboveTab = false,
+ tabTop,
+ tabLeft,
+ offset,
+ placeHolderLeft,
+ headerOffset,
+ tabWidth,
+ halfX;
+
+ // Empty stack
+ if (tabsLength === 0) {
+ headerOffset = this.header.element.offset();
+
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: headerOffset.left,
+ x2: headerOffset.left + 100,
+ y1: headerOffset.top + this.header.element.height() - 20,
+ y2: headerOffset.top + this.header.element.height()
+ });
+
+ return;
+ }
+
+ for (i = 0; i < tabsLength; i++) {
+ tabElement = this.header.tabs[i].element;
+ offset = tabElement.offset();
+ if (this._sided) {
+ tabLeft = offset.top;
+ tabTop = offset.left;
+ tabWidth = tabElement.height();
+ } else {
+ tabLeft = offset.left;
+ tabTop = offset.top;
+ tabWidth = tabElement.width();
+ }
+
+ if (x > tabLeft && x < tabLeft + tabWidth) {
+ isAboveTab = true;
+ break;
+ }
+ }
+
+ if (isAboveTab === false && x < tabLeft) {
+ return;
+ }
+
+ halfX = tabLeft + tabWidth / 2;
+
+ if (x < halfX) {
+ this._dropIndex = i;
+ tabElement.before(this.layoutManager.tabDropPlaceholder);
+ } else {
+ this._dropIndex = Math.min(i + 1, tabsLength);
+ tabElement.after(this.layoutManager.tabDropPlaceholder);
+ }
+
+
+ if (this._sided) {
+ placeHolderTop = this.layoutManager.tabDropPlaceholder.offset().top;
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: tabTop,
+ x2: tabTop + tabElement.innerHeight(),
+ y1: placeHolderTop,
+ y2: placeHolderTop + this.layoutManager.tabDropPlaceholder.width()
+ });
+ return;
+ }
+ placeHolderLeft = this.layoutManager.tabDropPlaceholder.offset().left;
+
+ this.layoutManager.dropTargetIndicator.highlightArea({
+ x1: placeHolderLeft,
+ x2: placeHolderLeft + this.layoutManager.tabDropPlaceholder.width(),
+ y1: tabTop,
+ y2: tabTop + tabElement.innerHeight()
+ });
+ },
+
+ _resetHeaderDropZone: function () {
+ this.layoutManager.tabDropPlaceholder.remove();
+ },
+
+ _setupHeaderPosition: function () {
+ var side = ['right', 'left', 'bottom'].indexOf(this._header.show) >= 0 && this._header.show;
+ this.header.element.toggle(!!this._header.show);
+ this._side = side;
+ this._sided = ['right', 'left'].indexOf(this._side) >= 0;
+ this.element.removeClass('lm_left lm_right lm_bottom');
+ if (this._side)
+ this.element.addClass('lm_' + this._side);
+ if (this.element.find('.lm_header').length && this.childElementContainer) {
+ var headerPosition = ['right', 'bottom'].indexOf(this._side) >= 0 ? 'before' : 'after';
+ this.header.element[headerPosition](this.childElementContainer);
+ this.callDownwards('setSize');
+ }
+ },
+
+ _highlightBodyDropZone: function (segment) {
+ var highlightArea = this._contentAreaDimensions[segment].highlightArea;
+ this.layoutManager.dropTargetIndicator.highlightArea(highlightArea);
+ this._dropSegment = segment;
+ }
+ });
+
+ lm.utils.BubblingEvent = function (name, origin) {
+ this.name = name;
+ this.origin = origin;
+ this.isPropagationStopped = false;
+ };
+
+ lm.utils.BubblingEvent.prototype.stopPropagation = function () {
+ this.isPropagationStopped = true;
+ };
+ /**
+ * Minifies and unminifies configs by replacing frequent keys
+ * and values with one letter substitutes. Config options must
+ * retain array position/index, add new options at the end.
+ *
+ * @constructor
+ */
+ lm.utils.ConfigMinifier = function () {
+ this._keys = [
+ 'settings',
+ 'hasHeaders',
+ 'constrainDragToContainer',
+ 'selectionEnabled',
+ 'dimensions',
+ 'borderWidth',
+ 'minItemHeight',
+ 'minItemWidth',
+ 'headerHeight',
+ 'dragProxyWidth',
+ 'dragProxyHeight',
+ 'labels',
+ 'close',
+ 'maximise',
+ 'minimise',
+ 'popout',
+ 'content',
+ 'componentName',
+ 'componentState',
+ 'id',
+ 'width',
+ 'type',
+ 'height',
+ 'isClosable',
+ 'title',
+ 'popoutWholeStack',
+ 'openPopouts',
+ 'parentId',
+ 'activeItemIndex',
+ 'reorderEnabled',
+ 'borderGrabWidth',
+
+
+
+
+ //Maximum 36 entries, do not cross this line!
+ ];
+ if (this._keys.length > 36) {
+ throw new Error('Too many keys in config minifier map');
+ }
+
+ this._values = [
+ true,
+ false,
+ 'row',
+ 'column',
+ 'stack',
+ 'component',
+ 'close',
+ 'maximise',
+ 'minimise',
+ 'open in new window'
+ ];
+ };
+
+ lm.utils.copy(lm.utils.ConfigMinifier.prototype, {
+
+ /**
+ * Takes a GoldenLayout configuration object and
+ * replaces its keys and values recursively with
+ * one letter counterparts
+ *
+ * @param {Object} config A GoldenLayout config object
+ *
+ * @returns {Object} minified config
+ */
+ minifyConfig: function (config) {
+ var min = {};
+ this._nextLevel(config, min, '_min');
+ return min;
+ },
+
+ /**
+ * Takes a configuration Object that was previously minified
+ * using minifyConfig and returns its original version
+ *
+ * @param {Object} minifiedConfig
+ *
+ * @returns {Object} the original configuration
+ */
+ unminifyConfig: function (minifiedConfig) {
+ var orig = {};
+ this._nextLevel(minifiedConfig, orig, '_max');
+ return orig;
+ },
+
+ /**
+ * Recursive function, called for every level of the config structure
+ *
+ * @param {Array|Object} orig
+ * @param {Array|Object} min
+ * @param {String} translationFn
+ *
+ * @returns {void}
+ */
+ _nextLevel: function (from, to, translationFn) {
+ var key, minKey;
+
+ for (key in from) {
+
+ /**
+ * For in returns array indices as keys, so let's cast them to numbers
+ */
+ if (from instanceof Array) key = parseInt(key, 10);
+
+ /**
+ * In case something has extended Object prototypes
+ */
+ if (!from.hasOwnProperty(key)) continue;
+
+ /**
+ * Translate the key to a one letter substitute
+ */
+ minKey = this[translationFn](key, this._keys);
+
+ /**
+ * For Arrays and Objects, create a new Array/Object
+ * on the minified object and recurse into it
+ */
+ if (typeof from[key] === 'object') {
+ to[minKey] = from[key] instanceof Array ? [] : {};
+ this._nextLevel(from[key], to[minKey], translationFn);
+
+ /**
+ * For primitive values (Strings, Numbers, Boolean etc.)
+ * minify the value
+ */
+ } else {
+ to[minKey] = this[translationFn](from[key], this._values);
+ }
+ }
+ },
+
+ /**
+ * Minifies value based on a dictionary
+ *
+ * @param {String|Boolean} value
+ * @param {Array<String|Boolean>} dictionary
+ *
+ * @returns {String} The minified version
+ */
+ _min: function (value, dictionary) {
+ /**
+ * If a value actually is a single character, prefix it
+ * with ___ to avoid mistaking it for a minification code
+ */
+ if (typeof value === 'string' && value.length === 1) {
+ return '___' + value;
+ }
+
+ var index = lm.utils.indexOf(value, dictionary);
+
+ /**
+ * value not found in the dictionary, return it unmodified
+ */
+ if (index === -1) {
+ return value;
+
+ /**
+ * value found in dictionary, return its base36 counterpart
+ */
+ } else {
+ return index.toString(36);
+ }
+ },
+
+ _max: function (value, dictionary) {
+ /**
+ * value is a single character. Assume that it's a translation
+ * and return the original value from the dictionary
+ */
+ if (typeof value === 'string' && value.length === 1) {
+ return dictionary[parseInt(value, 36)];
+ }
+
+ /**
+ * value originally was a single character and was prefixed with ___
+ * to avoid mistaking it for a translation. Remove the prefix
+ * and return the original character
+ */
+ if (typeof value === 'string' && value.substr(0, 3) === '___') {
+ return value[3];
+ }
+ /**
+ * value was not minified
+ */
+ return value;
+ }
+ });
+
+ /**
+ * An EventEmitter singleton that propagates events
+ * across multiple windows. This is a little bit trickier since
+ * windows are allowed to open childWindows in their own right
+ *
+ * This means that we deal with a tree of windows. Hence the rules for event propagation are:
+ *
+ * - Propagate events from this layout to both parents and children
+ * - Propagate events from parent to this and children
+ * - Propagate events from children to the other children (but not the emitting one) and the parent
+ *
+ * @constructor
+ *
+ * @param {lm.LayoutManager} layoutManager
+ */
+ lm.utils.EventHub = function (layoutManager) {
+ lm.utils.EventEmitter.call(this);
+ this._layoutManager = layoutManager;
+ this._dontPropagateToParent = null;
+ this._childEventSource = null;
+ this.on(lm.utils.EventEmitter.ALL_EVENT, lm.utils.fnBind(this._onEventFromThis, this));
+ this._boundOnEventFromChild = lm.utils.fnBind(this._onEventFromChild, this);
+ $(window).on('gl_child_event', this._boundOnEventFromChild);
+ };
+
+ /**
+ * Called on every event emitted on this eventHub, regardles of origin.
+ *
+ * @private
+ *
+ * @param {Mixed}
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._onEventFromThis = function () {
+ var args = Array.prototype.slice.call(arguments);
+
+ if (this._layoutManager.isSubWindow && args[0] !== this._dontPropagateToParent) {
+ this._propagateToParent(args);
+ }
+ this._propagateToChildren(args);
+
+ //Reset
+ this._dontPropagateToParent = null;
+ this._childEventSource = null;
+ };
+
+ /**
+ * Called by the parent layout.
+ *
+ * @param {Array} args Event name + arguments
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._$onEventFromParent = function (args) {
+ this._dontPropagateToParent = args[0];
+ this.emit.apply(this, args);
+ };
+
+ /**
+ * Callback for child events raised on the window
+ *
+ * @param {DOMEvent} event
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._onEventFromChild = function (event) {
+ this._childEventSource = event.originalEvent.__gl;
+ this.emit.apply(this, event.originalEvent.__glArgs);
+ };
+
+ /**
+ * Propagates the event to the parent by emitting
+ * it on the parent's DOM window
+ *
+ * @param {Array} args Event name + arguments
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._propagateToParent = function (args) {
+ var event,
+ eventName = 'gl_child_event';
+
+ if (document.createEvent) {
+ event = window.opener.document.createEvent('HTMLEvents');
+ event.initEvent(eventName, true, true);
+ } else {
+ event = window.opener.document.createEventObject();
+ event.eventType = eventName;
+ }
+
+ event.eventName = eventName;
+ event.__glArgs = args;
+ event.__gl = this._layoutManager;
+
+ if (document.createEvent) {
+ window.opener.dispatchEvent(event);
+ } else {
+ window.opener.fireEvent('on' + event.eventType, event);
+ }
+ };
+
+ /**
+ * Propagate events to children
+ *
+ * @param {Array} args Event name + arguments
+ * @private
+ *
+ * @returns {void}
+ */
+ lm.utils.EventHub.prototype._propagateToChildren = function (args) {
+ var childGl, i;
+
+ for (i = 0; i < this._layoutManager.openPopouts.length; i++) {
+ childGl = this._layoutManager.openPopouts[i].getGlInstance();
+
+ if (childGl && childGl !== this._childEventSource) {
+ childGl.eventHub._$onEventFromParent(args);
+ }
+ }
+ };
+
+
+ /**
+ * Destroys the EventHub
+ *
+ * @public
+ * @returns {void}
+ */
+
+ lm.utils.EventHub.prototype.destroy = function () {
+ $(window).off('gl_child_event', this._boundOnEventFromChild);
+ };
+ /**
+ * A specialised GoldenLayout component that binds GoldenLayout container
+ * lifecycle events to react components
+ *
+ * @constructor
+ *
+ * @param {lm.container.ItemContainer} container
+ * @param {Object} state state is not required for react components
+ */
+ lm.utils.ReactComponentHandler = function (container, state) {
+ this._reactComponent = null;
+ this._originalComponentWillUpdate = null;
+ this._container = container;
+ this._initialState = state;
+ this._reactClass = this._getReactClass();
+ this._container.on('open', this._render, this);
+ this._container.on('destroy', this._destroy, this);
+ };
+
+ lm.utils.copy(lm.utils.ReactComponentHandler.prototype, {
+
+ /**
+ * Creates the react class and component and hydrates it with
+ * the initial state - if one is present
+ *
+ * By default, react's getInitialState will be used
+ *
+ * @private
+ * @returns {void}
+ */
+ _render: function () {
+ this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]);
+ this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () {
+ };
+ this._reactComponent.componentWillUpdate = this._onUpdate.bind(this);
+ if (this._container.getState()) {
+ this._reactComponent.setState(this._container.getState());
+ }
+ },
+
+ /**
+ * Removes the component from the DOM and thus invokes React's unmount lifecycle
+ *
+ * @private
+ * @returns {void}
+ */
+ _destroy: function () {
+ ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
+ this._container.off('open', this._render, this);
+ this._container.off('destroy', this._destroy, this);
+ },
+
+ /**
+ * Hooks into React's state management and applies the componentstate
+ * to GoldenLayout
+ *
+ * @private
+ * @returns {void}
+ */
+ _onUpdate: function (nextProps, nextState) {
+ this._container.setState(nextState);
+ this._originalComponentWillUpdate.call(this._reactComponent, nextProps, nextState);
+ },
+
+ /**
+ * Retrieves the react class from GoldenLayout's registry
+ *
+ * @private
+ * @returns {React.Class}
+ */
+ _getReactClass: function () {
+ var componentName = this._container._config.component;
+ var reactClass;
+
+ if (!componentName) {
+ throw new Error('No react component name. type: react-component needs a field `component`');
+ }
+
+ reactClass = this._container.layoutManager.getComponent(componentName);
+
+ if (!reactClass) {
+ throw new Error('React component "' + componentName + '" not found. ' +
+ 'Please register all components with GoldenLayout using `registerComponent(name, component)`');
+ }
+
+ return reactClass;
+ },
+
+ /**
+ * Copies and extends the properties array and returns the React element
+ *
+ * @private
+ * @returns {React.Element}
+ */
+ _getReactComponent: function () {
+ var defaultProps = {
+ glEventHub: this._container.layoutManager.eventHub,
+ glContainer: this._container,
+ };
+ var props = $.extend(defaultProps, this._container._config.props);
+ return React.createElement(this._reactClass, props);
+ }
+ });
+})(window.$); \ No newline at end of file
diff --git a/src/client/northstar/core/brusher/IBaseBrushable.ts b/src/client/northstar/core/brusher/IBaseBrushable.ts
index c46db4d22..87f4ba413 100644
--- a/src/client/northstar/core/brusher/IBaseBrushable.ts
+++ b/src/client/northstar/core/brusher/IBaseBrushable.ts
@@ -1,9 +1,9 @@
import { PIXIPoint } from '../../utils/MathUtil';
import { IEquatable } from '../../utils/IEquatable';
-import { Document } from '../../../../fields/Document';
+import { Doc } from '../../../../new_fields/Doc';
export interface IBaseBrushable<T> extends IEquatable {
- BrusherModels: Array<Document>;
+ BrusherModels: Array<Doc>;
BrushColors: Array<number>;
Position: PIXIPoint;
Size: PIXIPoint;
diff --git a/src/client/northstar/core/filter/FilterModel.ts b/src/client/northstar/core/filter/FilterModel.ts
index e2ba3f652..6ab96b33d 100644
--- a/src/client/northstar/core/filter/FilterModel.ts
+++ b/src/client/northstar/core/filter/FilterModel.ts
@@ -2,10 +2,9 @@ import { ValueComparison } from "./ValueComparision";
import { Utils } from "../../utils/Utils";
import { IBaseFilterProvider } from "./IBaseFilterProvider";
import { FilterOperand } from "./FilterOperand";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { FieldWaiting } from "../../../../fields/Field";
-import { Document } from "../../../../fields/Document";
import { HistogramField } from "../../dash-fields/HistogramField";
+import { Cast, FieldValue } from "../../../../new_fields/Types";
+import { Doc } from "../../../../new_fields/Doc";
export class FilterModel {
public ValueComparisons: ValueComparison[];
@@ -52,12 +51,12 @@ export class FilterModel {
let children = new Array<string>();
let linkedGraphNodes = baseOperation.Links;
linkedGraphNodes.map(linkVm => {
- let filterDoc = linkVm.Get(KeyStore.LinkedFromDocs);
- if (filterDoc && filterDoc !== FieldWaiting && filterDoc instanceof Document) {
- let filterHistogram = filterDoc.GetT(KeyStore.Data, HistogramField);
- if (filterHistogram && filterHistogram !== FieldWaiting) {
- if (!visitedFilterProviders.has(filterHistogram.Data)) {
- let child = FilterModel.GetFilterModelsRecursive(filterHistogram.Data, visitedFilterProviders, filterModels, false);
+ let filterDoc = FieldValue(Cast(linkVm.linkedFrom, Doc));
+ if (filterDoc) {
+ let filterHistogram = Cast(filterDoc.data, HistogramField);
+ if (filterHistogram) {
+ if (!visitedFilterProviders.has(filterHistogram.HistoOp)) {
+ let child = FilterModel.GetFilterModelsRecursive(filterHistogram.HistoOp, visitedFilterProviders, filterModels, false);
if (child !== "") {
// if (linkVm.IsInverted) {
// child = "! " + child;
diff --git a/src/client/northstar/core/filter/IBaseFilterConsumer.ts b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
index 59d7adf4c..e7549d113 100644
--- a/src/client/northstar/core/filter/IBaseFilterConsumer.ts
+++ b/src/client/northstar/core/filter/IBaseFilterConsumer.ts
@@ -1,10 +1,10 @@
import { FilterOperand } from '../filter/FilterOperand';
import { IEquatable } from '../../utils/IEquatable';
-import { Document } from "../../../../fields/Document";
+import { Doc } from '../../../../new_fields/Doc';
export interface IBaseFilterConsumer extends IEquatable {
FilterOperand: FilterOperand;
- Links: Document[];
+ Links: Doc[];
}
export function instanceOfIBaseFilterConsumer(object: any): object is IBaseFilterConsumer {
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 c699691a4..f01f08487 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -1,64 +1,55 @@
-import { action } from "mobx";
+import { observable } from "mobx";
+import { custom, serializable } from "serializr";
import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel";
import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation";
-import { BasicField } from "../../../fields/BasicField";
-import { Field, FieldId } from "../../../fields/Field";
+import { ObjectField, Copy } from "../../../new_fields/ObjectField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { Types } from "../../../server/Message";
import { OmitKeys } from "../../../Utils";
-
-
-export class HistogramField extends BasicField<HistogramOperation> {
- constructor(data?: HistogramOperation, id?: FieldId, save: boolean = true) {
- super(data ? data : HistogramOperation.Empty, save, id);
- }
-
- toString(): string {
- return JSON.stringify(OmitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']));
- }
-
- Copy(): Field {
- return new HistogramField(this.Data.Copy());
+import { Deserializable } from "../../util/SerializationHelper";
+
+function serialize(field: HistogramField) {
+ return OmitKeys(field.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
+}
+
+function deserialize(jp: any) {
+ let X: AttributeTransformationModel | undefined;
+ let Y: AttributeTransformationModel | undefined;
+ let V: AttributeTransformationModel | undefined;
+
+ let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
+ if (schema) {
+ CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
+ if (attr.displayName === jp.X.AttributeModel.Attribute.DisplayName) {
+ X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction);
+ }
+ if (attr.displayName === jp.Y.AttributeModel.Attribute.DisplayName) {
+ Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction);
+ }
+ if (attr.displayName === jp.V.AttributeModel.Attribute.DisplayName) {
+ V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction);
+ }
+ });
+ if (X && Y && V) {
+ return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization));
+ }
}
-
- ToScriptString(): string {
- return `new HistogramField("${this.Data}")`;
+ return new HistogramField(HistogramOperation.Empty);
+}
+
+@Deserializable("histogramField")
+export class HistogramField extends ObjectField {
+ @serializable(custom(serialize, deserialize)) @observable public readonly HistoOp: HistogramOperation;
+ constructor(data?: HistogramOperation) {
+ super();
+ this.HistoOp = data ? data : HistogramOperation.Empty;
}
-
- ToJson() {
- return {
- type: Types.HistogramOp,
- data: this.toString(),
- id: this.Id
- };
+ toString(): string {
+ return JSON.stringify(OmitKeys(this.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit);
}
- @action
- static FromJson(id: string, data: any): HistogramField {
- let jp = JSON.parse(data);
- let X: AttributeTransformationModel | undefined;
- let Y: AttributeTransformationModel | undefined;
- let V: AttributeTransformationModel | undefined;
-
- let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
- if (schema) {
- CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
- if (attr.displayName === jp.X.AttributeModel.Attribute.DisplayName) {
- X = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.X.AggregateFunction);
- }
- if (attr.displayName === jp.Y.AttributeModel.Attribute.DisplayName) {
- Y = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.Y.AggregateFunction);
- }
- if (attr.displayName === jp.V.AttributeModel.Attribute.DisplayName) {
- V = new AttributeTransformationModel(new ColumnAttributeModel(attr), jp.V.AggregateFunction);
- }
- });
- if (X && Y && V) {
- return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization), id, false);
- }
- }
- return new HistogramField(HistogramOperation.Empty, id, false);
+ [Copy]() {
+ return new HistogramField(this.HistoOp.Copy());
}
} \ No newline at end of file
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 0e84ace50..5e7b867b3 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -1,10 +1,6 @@
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";
@@ -21,18 +17,20 @@ import "./HistogramBox.scss";
import { HistogramBoxPrimitives } from './HistogramBoxPrimitives';
import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives";
import { StyleConstants } from "../utils/StyleContants";
+import { NumCast, Cast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { Doc } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/RefField";
@observer
export class HistogramBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
private _dropXRef = React.createRef<HTMLDivElement>();
private _dropYRef = React.createRef<HTMLDivElement>();
private _dropXDisposer?: DragManager.DragDropDisposer;
private _dropYDisposer?: DragManager.DragDropDisposer;
- @observable public PanelWidth: number = 100;
- @observable public PanelHeight: number = 100;
@observable public HistoOp: HistogramOperation = HistogramOperation.Empty;
@observable public VisualBinRanges: VisualBinRange[] = [];
@observable public ValueRange: number[] = [];
@@ -50,9 +48,9 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@action
dropX = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
- if (h && h !== FieldWaiting) {
- this.HistoOp.X = h.Data.X;
+ let h = Cast(de.data.draggedDocuments[0].data, HistogramField);
+ if (h) {
+ this.HistoOp.X = h.HistoOp.X;
}
e.stopPropagation();
e.preventDefault();
@@ -61,9 +59,9 @@ export class HistogramBox extends React.Component<FieldViewProps> {
@action
dropY = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
- let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
- if (h && h !== FieldWaiting) {
- this.HistoOp.Y = h.Data.X;
+ let h = Cast(de.data.draggedDocuments[0].data, HistogramField);
+ if (h) {
+ this.HistoOp.Y = h.HistoOp.X;
}
e.stopPropagation();
e.preventDefault();
@@ -88,7 +86,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) {
@@ -113,59 +111,63 @@ export class HistogramBox extends React.Component<FieldViewProps> {
}
}
- activateHistogramOperation(catalog?: Catalog) {
+ async activateHistogramOperation(catalog?: Catalog) {
if (catalog) {
- this.props.Document.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {
- this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;
+ let histoOp = await Cast(this.props.Document[this.props.fieldKey], HistogramField);
+ runInAction(() => {
+ this.HistoOp = histoOp ? histoOp.HistoOp : HistogramOperation.Empty;
if (this.HistoOp !== HistogramOperation.Empty) {
- 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,
+ reaction(() => Cast(this.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });
+ reaction(() => Cast(this.props.Document.brushingDocs, listSpec(Doc), []).length,
() => {
- let brushingDocs = this.props.Document.GetList(KeyStore.BrushingDocs, [] as Document[]);
- let proto = this.props.Document.GetPrototype() as Document;
- this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
- brush.SetNumber(KeyStore.BackgroundColor, StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]);
- let brushed = brush.GetList(KeyStore.BrushingDocs, [] as Document[]);
- return { l: brush, b: brushed[0].Id === proto.Id ? brushed[1] : brushed[0] };
- }));
+ let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ const proto = this.props.Document.proto;
+ if (proto) {
+ this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => {
+ brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length];
+ let brushed = Cast(brush.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] };
+ }));
+ }
}, { fireImmediately: true });
reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true });
}
- }));
+ });
}
}
+
+ @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/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/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts
index 760106023..78b206bdc 100644
--- a/src/client/northstar/operations/HistogramOperation.ts
+++ b/src/client/northstar/operations/HistogramOperation.ts
@@ -1,7 +1,4 @@
import { action, computed, observable, trace } from "mobx";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { ColumnAttributeModel } from "../core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../core/attribute/AttributeTransformationModel";
@@ -16,14 +13,16 @@ import { AggregateFunction, AggregateParameters, Attribute, AverageAggregatePara
import { ModelHelpers } from "../model/ModelHelpers";
import { ArrayUtil } from "../utils/ArrayUtil";
import { BaseOperation } from "./BaseOperation";
+import { Doc } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
export class HistogramOperation extends BaseOperation implements IBaseFilterConsumer, IBaseFilterProvider {
public static Empty = new HistogramOperation("-empty schema-", new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())), new AttributeTransformationModel(new ColumnAttributeModel(new Attribute())));
@observable public FilterOperand: FilterOperand = FilterOperand.AND;
- @observable public Links: Document[] = [];
- @observable public BrushLinks: { l: Document, b: Document }[] = [];
+ @observable public Links: Doc[] = [];
+ @observable public BrushLinks: { l: Doc, b: Doc }[] = [];
@observable public BrushColors: number[] = [];
- @observable public FilterModels: FilterModel[] = [];
+ @observable public BarFilterModels: FilterModel[] = [];
@observable public Normalization: number = -1;
@observable public X: AttributeTransformationModel;
@@ -50,17 +49,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);
}
@@ -70,15 +76,37 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
trace();
let brushes: string[] = [];
this.BrushLinks.map(brushLink => {
- let brushHistogram = brushLink.b.GetT(KeyStore.Data, HistogramField);
- if (brushHistogram && brushHistogram !== FieldWaiting) {
+ let brushHistogram = Cast(brushLink.b.data, HistogramField);
+ if (brushHistogram) {
let filterModels: FilterModel[] = [];
- brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.Data, new Set<IBaseFilterProvider>(), filterModels, false));
+ brushes.push(FilterModel.GetFilterModelsRecursive(brushHistogram.HistoOp, new Set<IBaseFilterProvider>(), filterModels, false));
}
});
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));
@@ -120,7 +148,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons
@action
public async Update(): Promise<void> {
- this.BrushColors = this.BrushLinks.map(e => e.l.GetNumber(KeyStore.BackgroundColor, 0));
+ this.BrushColors = this.BrushLinks.map(e => NumCast(e.l.backgroundColor));
return super.Update();
}
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 3b5a5b470..779b07ce5 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,9 +1,9 @@
import { computed, observable } from 'mobx';
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from '../../fields/Field';
-import { KeyStore } from '../../fields/KeyStore';
-import { ListField } from '../../fields/ListField';
import { DocumentView } from '../views/nodes/DocumentView';
+import { Doc } from '../../new_fields/Doc';
+import { FieldValue, Cast, BoolCast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
+import { SelectionManager } from './SelectionManager';
export class DocumentManager {
@@ -25,28 +25,29 @@ export class DocumentManager {
// this.DocumentViews = new Array<DocumentView>();
}
- public getDocumentView(toFind: Document): DocumentView | null {
+ public getDocumentView(toFind: Doc): DocumentView | null {
- let toReturn: DocumentView | null;
- toReturn = null;
+ let toReturn: DocumentView | null = null;
//gets document view that is in a freeform canvas collection
DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- if (doc === toFind) {
+ if (view.props.Document === 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.proto;
+ if (doc && Object.is(doc, toFind)) {
+ toReturn = view;
+ }
+ });
+ }
return toReturn;
}
- public getDocumentViews(toFind: Document): DocumentView[] {
+ public getDocumentViews(toFind: Doc): DocumentView[] {
let toReturn: DocumentView[] = [];
@@ -58,8 +59,8 @@ export class DocumentManager {
if (doc === toFind) {
toReturn.push(view);
} else {
- let docSrc = doc.GetT(KeyStore.Prototype, Document);
- if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) {
+ let docSrc = FieldValue(doc.proto);
+ if (docSrc && Object.is(docSrc, toFind)) {
toReturn.push(view);
}
}
@@ -70,21 +71,34 @@ export class DocumentManager {
@computed
public get LinkedDocumentViews() {
- return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
- if (linksList && linksList !== FieldWaiting && linksList.Data.length) {
- pairs.push(...linksList.Data.reduce((pairs, link) => {
- if (link instanceof Document) {
- let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
- if (linkToDoc && linkToDoc !== FieldWaiting) {
+ return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+ let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
+ if (linkToDoc) {
DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
pairs.push({ a: dv, b: docView1, l: link }));
}
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]));
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+ }
+ linksList = Cast(dv.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ if (linksList && linksList.length) {
+ pairs.push(...linksList.reduce((pairs, link) => {
+ if (link) {
+ let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
+ if (linkFromDoc) {
+ DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
+ pairs.push({ a: dv, b: docView1, l: link }));
+ }
+ }
+ return pairs;
+ }, pairs));
}
return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Document }[]);
+ }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
}
} \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4bd654e15..266679c16 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,14 +1,12 @@
-import { action } from "mobx";
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from "../../fields/Field";
-import { KeyStore } from "../../fields/KeyStore";
+import { action, runInAction } from "mobx";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Cast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { DocumentDecorations } from "../views/DocumentDecorations";
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 type dropActionType = "alias" | "copy" | undefined;
+export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
let onRowMove = action((e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
@@ -16,7 +14,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
var dragData = new DragManager.DocumentDragData([docFunc()]);
- dragData.copyOnDrop = copyOnDrop;
+ dragData.dropAction = dropAction;
dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
});
@@ -40,19 +38,21 @@ 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.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) : [];
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+ let srcTarg = sourceDoc.proto;
+ let draggedDocs: Doc[] = [];
+ let draggedFromDocs: Doc[] = []
+ if (srcTarg) {
+ let linkToDocs = await DocListCast(srcTarg.linkedToDocs);
+ let linkFromDocs = await DocListCast(srcTarg.linkedFromDocs);
+ if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+ if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+ }
draggedDocs.push(...draggedFromDocs);
if (draggedDocs.length) {
- let moddrag = [] as Document[];
+ let moddrag: Doc[] = [];
for (const draggedDoc of draggedDocs) {
- let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document);
+ let doc = await Cast(draggedDoc.annotationOn, Doc);
if (doc) moddrag.push(doc);
}
let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
@@ -134,35 +134,46 @@ export namespace DragManager {
};
}
- export type MoveFunction = (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
export class DocumentDragData {
- constructor(dragDoc: Document[]) {
+ constructor(dragDoc: Doc[]) {
this.draggedDocuments = dragDoc;
this.droppedDocuments = dragDoc;
this.xOffset = 0;
this.yOffset = 0;
}
- draggedDocuments: Document[];
- droppedDocuments: Document[];
+ draggedDocuments: Doc[];
+ droppedDocuments: Doc[];
xOffset: number;
yOffset: number;
- aliasOnDrop?: boolean;
- copyOnDrop?: boolean;
+ dropAction: dropActionType;
+ userDropAction: dropActionType;
moveDocument?: MoveFunction;
[id: string]: any;
}
+ export let StartDragFunctions: (() => void)[] = [];
+
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ runInAction(() => StartDragFunctions.map(func => func()));
StartDrag(eles, dragData, downX, downY, options,
- (dropData: { [id: string]: any }) => (dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => d.CreateAlias()) : dragData.copyOnDrop ? dragData.draggedDocuments.map(d => d.Copy(true) as Document) : dragData.draggedDocuments));
+ (dropData: { [id: string]: any }) =>
+ (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
+ dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
+ dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
+ dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
+ dragData.draggedDocuments));
}
export class LinkDragData {
- constructor(linkSourceDoc: Document) {
+ constructor(linkSourceDoc: Doc, blacklist: Doc[] = []) {
this.linkSourceDocument = linkSourceDoc;
+ this.blacklist = blacklist;
}
- droppedDocuments: Document[] = [];
- linkSourceDocument: Document;
+ droppedDocuments: Doc[] = [];
+ linkSourceDocument: Doc;
+ blacklist: Doc[];
+ dontClearTextBox?: boolean;
[id: string]: any;
}
@@ -170,20 +181,22 @@ 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[] = [];
let xs: number[] = [];
let ys: number[] = [];
- const docs: Document[] =
+ const docs: Doc[] =
dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
@@ -204,6 +217,7 @@ export namespace DragManager {
dragElement.style.top = "0";
dragElement.style.bottom = "";
dragElement.style.left = "0";
+ dragElement.style.color = "black";
dragElement.style.transformOrigin = "0 0";
dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "1000";
dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
@@ -247,10 +261,10 @@ export namespace DragManager {
e.stopPropagation();
e.preventDefault();
if (dragData instanceof DocumentDragData) {
- dragData.aliasOnDrop = e.ctrlKey || e.altKey;
+ dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
}
if (e.shiftKey) {
- abortDrag();
+ AbortDrag();
CollectionDockingView.Instance.StartOtherDrag(docs, {
pageX: e.pageX,
pageY: e.pageY,
@@ -269,31 +283,47 @@ 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);
- return [dragEle, parent];
+ // let parent = dragEle.parentElement;
+ // if (parent) parent.removeChild(dragEle);
+ let ret = [dragEle, dragEle.style.width, dragEle.style.height];
+ dragEle.style.width = "0";
+ dragEle.style.height = "0";
+ return ret;
});
const target = document.elementFromPoint(e.x, e.y);
removed.map(r => {
- let dragEle = r[0];
- let parent = r[1];
- if (parent && dragEle) parent.appendChild(dragEle);
+ let dragEle = r[0] as HTMLElement;
+ dragEle.style.width = r[1] as string;
+ dragEle.style.height = r[2] as string;
+ // let parent = r[1];
+ // if (parent && dragEle) parent.appendChild(dragEle);
});
if (target) {
if (finishDrag) finishDrag(dragData);
@@ -308,11 +338,6 @@ export namespace DragManager {
}
})
);
-
- if (options) {
- options.handlers.dragComplete({});
- }
}
- DocumentDecorations.Instance.Hidden = false;
}
}
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index d2665b0fc..964720b06 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -7,7 +7,6 @@ import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirr
import { undo, redo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
import { Transaction, EditorState } from "prosemirror-state";
-import { MenuItem } from "prosemirror-menu";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 9ef71e305..c0e6f7899 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -84,6 +84,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
+ width: { default: "100px" },
alt: { default: null },
title: { default: null }
},
@@ -94,11 +95,16 @@ export const nodes: { [index: string]: NodeSpec } = {
return {
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt")
- };
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ }
}
}],
- toDOM(node: any) { return ["img", node.attrs]; }
+ // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` }
+ return ["img", { ...node.attrs, ...attrs }]
+ }
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
@@ -290,6 +296,13 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
+ p14: {
+ parseDOM: [{ style: 'font-size: 14px;' }],
+ toDOM: () => ['span', {
+ style: 'font-size: 14px;'
+ }]
+ },
+
p16: {
parseDOM: [{ style: 'font-size: 16px;' }],
toDOM: () => ['span', {
@@ -325,7 +338,75 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
};
+function getFontSize(element: any) {
+ return parseFloat((getComputedStyle(element) as any).fontSize);
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any) {
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ let self = this;
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ }
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ view.dispatch(
+ view.state.tr.setNodeMarkup(getPos(), null,
+ { src: node.attrs.src, width: self._outer.style.width })
+ .setSelection(view.state.selection));
+ }
+
+ document.addEventListener("pointermove", onpointermove)
+ document.addEventListener("pointerup", onpointerup)
+ }
+ this._outer.appendChild(this._handle);
+ this._outer.appendChild(this._img);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c67cc067a..e45f61c11 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,13 +1,5 @@
// import * as ts from "typescript"
let ts = (window as any).ts;
-import { Opt, Field } from "../../fields/Field";
-import { Document } from "../../fields/Document";
-import { NumberField } from "../../fields/NumberField";
-import { ImageField } from "../../fields/ImageField";
-import { TextField } from "../../fields/TextField";
-import { RichTextField } from "../../fields/RichTextField";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
// // @ts-ignore
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// // @ts-ignore
@@ -15,8 +7,11 @@ import { ListField } from "../../fields/ListField";
// @ts-ignore
import * as typescriptlib from '!!raw-loader!./type_decls.d';
-import { Documents } from "../documents/Documents";
-import { Key } from "../../fields/Key";
+import { Docs } from "../documents/Documents";
+import { Doc, Field } from '../../new_fields/Doc';
+import { ImageField, PdfField, VideoField, AudioField } from '../../new_fields/URLField';
+import { List } from '../../new_fields/List';
+import { RichTextField } from '../../new_fields/RichTextField';
export interface ScriptSucccess {
success: true;
@@ -50,9 +45,9 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
return { compiled: false, errors: diagnostics };
}
- let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField, Key];
- let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)];
- let params: any[] = [KeyStore, Documents, ...fieldTypes];
+ let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField];
+ let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
+ let params: any[] = [Docs, ...fieldTypes];
let compiledFunction = new Function(...paramNames, `return ${script}`);
let { capturedVariables = {} } = options;
let run = (args: { [name: string]: any } = {}): ScriptResult => {
@@ -171,17 +166,4 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
return Run(outputText, paramNames, diagnostics, script, options);
-}
-
-export function OrLiteralType(returnType: string): string {
- return `${returnType} | string | number`;
-}
-
-export function ToField(data: any): Opt<Field> {
- if (typeof data === "string") {
- return new TextField(data);
- } else if (typeof data === "number") {
- return new NumberField(data);
- }
- return undefined;
} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index b15a93d9f..8c92c2023 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,8 +1,8 @@
import { observable, action } from "mobx";
+import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
-import { Document } from "../../fields/Document";
-import { Main } from "../views/Main";
-import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { NumCast } from "../../new_fields/Types";
export namespace SelectionManager {
class Manager {
@@ -26,7 +26,7 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- MainOverlayTextBox.Instance.SetTextDoc();
+ FormattedTextBox.InputBoxOverlay = undefined;
}
@action
ReselectAll() {
@@ -36,7 +36,7 @@ export namespace SelectionManager {
}
@action
ReselectAll2(sdocs: DocumentView[]) {
- sdocs.map(s => SelectionManager.SelectDoc(s, false));
+ sdocs.map(s => SelectionManager.SelectDoc(s, true));
}
}
@@ -50,7 +50,7 @@ export namespace SelectionManager {
return manager.SelectedDocuments.indexOf(doc) !== -1;
}
- export function DeselectAll(except?: Document): void {
+ export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
for (const view of manager.SelectedDocuments) {
@@ -64,9 +64,25 @@ export namespace SelectionManager {
export function ReselectAll() {
let sdocs = manager.ReselectAll();
- manager.ReselectAll2(sdocs);
+ setTimeout(() => manager.ReselectAll2(sdocs), 0);
}
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments;
}
+ export function ViewsSortedVertically(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
+ export function ViewsSortedHorizontally(): DocumentView[] {
+ let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
+ if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
+ return 0;
+ });
+ return sorted;
+ }
}
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
new file mode 100644
index 000000000..7ded85e43
--- /dev/null
+++ b/src/client/util/SerializationHelper.ts
@@ -0,0 +1,130 @@
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
+import { Field } from "../../new_fields/Doc";
+
+export namespace SerializationHelper {
+ let serializing: number = 0;
+ export function IsSerializing() {
+ return serializing > 0;
+ }
+
+ export function Serialize(obj: Field): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!(obj.constructor.name in reverseMap)) {
+ throw Error(`type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const json = serialize(obj);
+ json.__type = reverseMap[obj.constructor.name];
+ serializing -= 1;
+ return json;
+ }
+
+ export function Deserialize(obj: any): any {
+ if (obj === undefined || obj === null) {
+ return undefined;
+ }
+
+ if (typeof obj !== 'object') {
+ return obj;
+ }
+
+ serializing += 1;
+ if (!obj.__type) {
+ throw Error("No property 'type' found in JSON.");
+ }
+
+ if (!(obj.__type in serializationTypes)) {
+ throw Error(`type '${obj.__type}' not registered. Make sure you register it using a @Deserializable decorator`);
+ }
+
+ const value = deserialize(serializationTypes[obj.__type], obj);
+ serializing -= 1;
+ return value;
+ }
+}
+
+let serializationTypes: { [name: string]: any } = {};
+let reverseMap: { [ctor: string]: string } = {};
+
+export interface DeserializableOpts {
+ (constructor: { new(...args: any[]): any }): void;
+ withFields(fields: string[]): Function;
+}
+
+export function Deserializable(name: string): DeserializableOpts;
+export function Deserializable(constructor: { new(...args: any[]): any }): void;
+export function Deserializable(constructor: { new(...args: any[]): any } | string): DeserializableOpts | void {
+ function addToMap(name: string, ctor: { new(...args: any[]): any }) {
+ const schema = getDefaultModelSchema(ctor) as any;
+ if (schema.targetClass !== ctor) {
+ const newSchema = { ...schema, factory: () => new ctor() };
+ setDefaultModelSchema(ctor, newSchema);
+ }
+ if (!(name in serializationTypes)) {
+ serializationTypes[name] = ctor;
+ reverseMap[ctor.name] = name;
+ } else {
+ throw new Error(`Name ${name} has already been registered as deserializable`);
+ }
+ }
+ if (typeof constructor === "string") {
+ return Object.assign((ctor: { new(...args: any[]): any }) => {
+ addToMap(constructor, ctor);
+ }, { withFields: Deserializable.withFields });
+ }
+ addToMap(constructor.name, constructor);
+}
+
+export namespace Deserializable {
+ export function withFields(fields: string[]) {
+ return function (constructor: { new(...fields: any[]): any }) {
+ Deserializable(constructor);
+ let schema = getDefaultModelSchema(constructor);
+ if (schema) {
+ schema.factory = context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ };
+ // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
+ // fields.forEach(field => {
+ // if (field in schema.props) {
+ // let propSchema = schema.props[field];
+ // if (propSchema === false) {
+ // return;
+ // } else if (propSchema === true) {
+ // propSchema = primitive();
+ // }
+ // schema.props[field] = custom(propSchema.serializer,
+ // () => {
+ // return SKIP;
+ // });
+ // }
+ // });
+ } else {
+ schema = {
+ props: {},
+ factory: context => {
+ const args = fields.map(key => context.json[key]);
+ return new constructor(...args);
+ }
+ };
+ setDefaultModelSchema(constructor, schema);
+ }
+ };
+ }
+}
+
+export function autoObject(): PropSchema {
+ return custom(
+ (s) => SerializationHelper.Serialize(s),
+ (s) => SerializationHelper.Deserialize(s)
+ );
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 7a6f90dd0..18ec5732b 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -11,36 +11,52 @@ 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 { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list';
+import { liftTarget, RemoveMarkStep, AddMarkStep } from 'prosemirror-transform';
import {
faListUl, faGrinTongueSquint,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { FieldViewProps } from "../views/nodes/FieldView";
import { throwStatement } from "babel-types";
-import { KeyStore } from "../../fields/KeyStore";
-import { NumberField } from "../../fields/NumberField";
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
+import { View } from "@react-pdf/renderer";
+import { DragManager } from "./DragManager";
+import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/RefField";
+import { Utils } from "../northstar/utils/Utils";
+import { DocServer } from "../DocServer";
+import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
+import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { DocumentManager } from "./DocumentManager";
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");
private link: HTMLAnchorElement;
+
//dropdown doms
private fontSizeDom?: Node;
private fontStyleDom?: Node;
+ private listTypeBtnDom?: Node;
+
+ private linkEditor?: HTMLDivElement;
+ private linkText?: HTMLDivElement;
+ private linkDrag?: HTMLImageElement;
constructor(view: EditorView, editorProps: FieldViewProps) {
this.view = view;
@@ -62,8 +78,9 @@ export class TooltipTextMenu {
{ command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") },
{ command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") },
{ command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") },
- { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") },
- { command: lift, dom: this.icon("<", "lift") },
+ // { 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") },
];
//add menu items
items.forEach(({ dom, command }) => {
@@ -93,6 +110,7 @@ export class TooltipTextMenu {
this.fontSizeToNum = new Map();
this.fontSizeToNum.set(schema.marks.p10, 10);
this.fontSizeToNum.set(schema.marks.p12, 12);
+ this.fontSizeToNum.set(schema.marks.p14, 14);
this.fontSizeToNum.set(schema.marks.p16, 16);
this.fontSizeToNum.set(schema.marks.p24, 24);
this.fontSizeToNum.set(schema.marks.p32, 32);
@@ -100,7 +118,11 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p72, 72);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
- //this.addFontDropdowns();
+ //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.link = document.createElement("a");
this.link.target = "_blank";
@@ -118,7 +140,7 @@ export class TooltipTextMenu {
//font SIZES
let fontSizeBtns: MenuItem[] = [];
this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
});
if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); }
@@ -137,7 +159,7 @@ export class TooltipTextMenu {
//font STYLES
let fontBtns: MenuItem[] = [];
this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ 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); }
@@ -149,6 +171,119 @@ export class TooltipTextMenu {
this.tooltip.appendChild(this.fontStyleDom);
}
+ updateLinkMenu() {
+ if (!this.linkEditor || !this.linkText) {
+ this.linkEditor = document.createElement("div");
+ this.linkEditor.style.color = "white";
+ this.linkText = document.createElement("div");
+ this.linkText.style.cssFloat = "left";
+ this.linkText.style.marginRight = "5px";
+ this.linkText.style.marginLeft = "5px";
+ this.linkText.setAttribute("contenteditable", "true");
+ this.linkText.style.whiteSpace = "nowrap";
+ this.linkText.style.width = "150px";
+ this.linkText.style.overflow = "hidden";
+ this.linkText.style.color = "white";
+ this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }
+ let linkBtn = document.createElement("div");
+ linkBtn.textContent = ">>";
+ linkBtn.style.width = "20px";
+ linkBtn.style.height = "20px";
+ linkBtn.style.color = "white";
+ linkBtn.style.cssFloat = "left";
+ linkBtn.onpointerdown = (e: PointerEvent) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ console.log("Link to : " + link.attrs.href);
+ let href: string = link.attrs.href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f))
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ else CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ this.linkDrag = document.createElement("img");
+ this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png";
+ this.linkDrag.style.width = "20px";
+ this.linkDrag.style.height = "20px";
+ this.linkDrag.style.color = "white";
+ this.linkDrag.style.background = "black";
+ this.linkDrag.style.cssFloat = "left";
+ this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ let dragData = new DragManager.LinkDragData(this.editorProps.Document);
+ dragData.dontClearTextBox = true;
+ DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
+ {
+ handlers: {
+ dragComplete: action(() => {
+ let m = dragData.droppedDocuments as Doc[];
+ this.makeLink(DocServer.prepend("/doc/" + m[0][Id]));
+ }),
+ },
+ hideSource: false
+ })
+ };
+ this.linkEditor.appendChild(this.linkDrag);
+ this.linkEditor.appendChild(this.linkText);
+ this.linkEditor.appendChild(linkBtn);
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = node && node.marks.find(m => m.type.name === "link");
+ this.linkText.textContent = link ? link.attrs.href : "-empty-";
+
+ this.linkText.onkeydown = (e: KeyboardEvent) => {
+ if (e.key === "Enter") {
+ this.makeLink(this.linkText!.textContent!);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ this.tooltip.appendChild(this.linkEditor);
+ }
+
+ makeLink = (target: string) => {
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ //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;
@@ -180,9 +315,18 @@ export class TooltipTextMenu {
return toggleMark(markType)(view.state, view.dispatch, view);
}
- //makes a button for the drop down
+ //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
- dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+ 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,
@@ -233,6 +377,22 @@ export class TooltipTextMenu {
});
}
+ //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);
+ }
+ });
+ }
+
markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) {
let { from, $from, to, empty } = state.selection
if (empty) return type.isInSet(state.storedMarks || $from.marks())
@@ -337,6 +497,9 @@ export class TooltipTextMenu {
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) {
@@ -361,9 +524,11 @@ export class TooltipTextMenu {
} else { //multiple font sizes selected
this.updateFontSizeDropdown("Various");
}
+
+ this.updateLinkMenu();
}
- //finds all active marks on selection
+ //finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
let { empty, $cursor, ranges } = this.view.state.selection as TextSelection;
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 27aed4bac..c0ed015bd 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,7 +1,6 @@
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import 'source-map-support/register';
import { Without } from "../../Utils";
-import { string } from "prop-types";
function getBatchName(target: any, key: string | symbol): string {
let keyName = key.toString();
@@ -94,6 +93,10 @@ export namespace UndoManager {
return redoStack.length > 0;
}
+ export function PrintBatches(): void {
+ GetOpenBatches().forEach(batch => console.log(batch.batchName));
+ }
+
let openBatches: Batch[] = [];
export function GetOpenBatches(): Without<Batch, 'end'>[] {
return openBatches;
@@ -140,10 +143,11 @@ export namespace UndoManager {
}
});
- export function RunInBatch(fn: () => void, batchName: string) {
+ //TODO Make this return the return value
+ export function RunInBatch<T>(fn: () => T, batchName: string) {
let batch = StartBatch(batchName);
try {
- fn();
+ return runInAction(fn);
} finally {
batch.end();
}
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 0964d5ff3..5008ddfcf 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
new file mode 100644
index 000000000..d6562492f
--- /dev/null
+++ b/src/client/views/DocComponent.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { Doc } from '../../new_fields/Doc';
+import { computed } from 'mobx';
+
+export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed
+ get Document(): T {
+ return schemaCtor(this.props.Document);
+ }
+ }
+ return Component;
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index c1a949639..6a2e33836 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,20 +1,22 @@
@import "globalCssVariables";
+$linkGap : 3px;
.documentDecorations {
position: absolute;
}
-#documentDecorations-container {
+
+.documentDecorations-container {
+ z-index: $docDecorations-zindex;
position: absolute;
top: 0;
- left:0;
+ left: 0;
display: grid;
- z-index: $docDecorations-zindex;
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 {
- grid-column:3;
+ grid-column: 3;
background: none;
}
@@ -39,8 +41,8 @@
#documentDecorations-bottomRightResizer,
#documentDecorations-topRightResizer,
#documentDecorations-rightResizer {
- grid-column-start:5;
- grid-column-end:7;
+ grid-column-start: 5;
+ grid-column-end: 7;
}
#documentDecorations-topLeftResizer,
@@ -63,16 +65,17 @@
cursor: ew-resize;
}
.title{
- width:100%;
background: lightblue;
- grid-column-start:3;
+ grid-column-start: 3;
grid-column-end: 4;
pointer-events: auto;
+ overflow: hidden;
}
}
+
.documentDecorations-closeButton {
- background:$alt-accent;
+ background: $alt-accent;
opacity: 0.8;
grid-column-start: 4;
grid-column-end: 6;
@@ -80,15 +83,22 @@
text-align: center;
cursor: pointer;
}
+
.documentDecorations-minimizeButton {
- background:$alt-accent;
+ background: $alt-accent;
opacity: 0.8;
grid-column-start: 1;
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 {
background: lightblue;
position: absolute;
@@ -96,8 +106,8 @@
}
.linkFlyout {
- grid-column: 1/4;
- margin-left: 25px;
+ grid-column: 2/4;
+ margin-top: $linkGap;
}
.linkButton-empty:hover {
@@ -112,35 +122,34 @@
cursor: pointer;
}
+.link-button-container {
+ grid-column: 1/4;
+ width: auto;
+ height: auto;
+ display: flex;
+ flex-direction: row;
+}
+
.linkButton-linker {
- position:absolute;
- bottom:0px;
- left: 0px;
+ margin-left: 5px;
+ margin-top: $linkGap;
height: 20px;
width: 20px;
- margin-top: 10px;
- margin-right: 5px;
+ text-align: center;
border-radius: 50%;
- opacity: 0.9;
pointer-events: auto;
color: $dark-color;
border: $dark-color 1px solid;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- transition: transform 0.2s;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
}
-.linkButton-tray {
- grid-column: 1/4;
+
+.linkButton-linker:hover {
+ cursor: pointer;
+ 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;
@@ -154,17 +163,19 @@
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;
text-transform: uppercase;
letter-spacing: 2px;
font-size: 75%;
@@ -173,4 +184,43 @@
display: flex;
justify-content: center;
align-items: center;
+}
+
+.fa-icon-link {
+ margin-top: 3px;
+}
+.templating-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ font-size:14;
+ 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: max-content;
+ font-family: $sans-serif;
+ font-size: 12px;
+ background-color: $light-color-secondary;
+ padding: 2px 12px;
+ list-style: none;
+ .templateToggle {
+ text-align: left;
+ }
+
+ 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 16fac0694..4786b4de6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,94 +1,126 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Key } from "../../fields/Key";
-import { KeyStore } from "../../fields/KeyStore";
-import { ListField } from "../../fields/ListField";
-import { NumberField } from "../../fields/NumberField";
-import { TextField } from "../../fields/TextField";
-import { emptyFunction } from "../../Utils";
+import { emptyFunction, Utils } from "../../Utils";
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import './DocumentDecorations.scss';
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
-import { DocumentView } from "./nodes/DocumentView";
+import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
+import { TemplateMenu } from "./TemplateMenu";
import React = require("react");
+import { Template, Templates } from "./Templates";
+import { CompileScript } from "../util/Scripting";
+import { IconBox } from "./nodes/IconBox";
+import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
+import { Doc, FieldResult } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Docs } from "../documents/Documents";
+import { List } from "../../new_fields/List";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
+import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
+import { CollectionView } from "./collections/CollectionView";
+import { DocumentManager } from "../util/DocumentManager";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FieldView } from "./nodes/FieldView";
+
+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 : "";
- @observable private _fieldKey: Key = KeyStore.Title;
+ private _downX = 0;
+ private _downY = 0;
+ private _iconDoc?: Doc = undefined;
+ @observable private _minimizedX = 0;
+ @observable private _minimizedY = 0;
+ @observable private _title: string = "";
+ @observable private _edtingTitle = false;
+ @observable private _fieldKey = "title";
@observable private _hidden = false;
@observable private _opacity = 1;
- @observable private _dragging = false;
-
+ @observable private _removeIcon = 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 => 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) {
var text = e.target.value;
if (text[0] === '#') {
- let command = text.slice(1, text.length);
- this._fieldKey = new Key(command);
- // if (command === "Title" || command === "title") {
- // this._fieldKey = KeyStore.Title;
- // }
- // else if (command === "Width" || command === "width") {
- // this._fieldKey = KeyStore.Width;
- // }
- this._title = "changed";
- // TODO: Change field with switch statement
+ this._fieldKey = text.slice(1, text.length);
+ this._title = this.selectionTitle;
}
else {
- if (this._documents.length > 0) {
- let field = this._documents[0].props.Document.Get(this._fieldKey);
- if (field instanceof TextField) {
- this._documents.forEach(d =>
- d.props.Document.Set(this._fieldKey, new TextField(this._title)));
+ if (SelectionManager.SelectedDocuments().length > 0) {
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "number") {
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = +this._title;
+ });
+ } else {
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = this._title;
+ });
}
- else if (field instanceof NumberField) {
- this._documents.forEach(d =>
- d.props.Document.Set(this._fieldKey, new NumberField(+this._title)));
- }
- this._title = "changed";
}
}
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;
@@ -103,46 +135,38 @@ 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();
- if (e.currentTarget.localName !== "input") {
- e.preventDefault();
- }
+ e.preventDefault();
}
@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.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.xOffset = xoff;
+ dragData.yOffset = yoff;
+ dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
+ this.Interacting = true;
+ this._hidden = true;
document.removeEventListener("pointermove", this.onBackgroundMove);
document.removeEventListener("pointerup", this.onBackgroundUp);
+ 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._dragging = false),
- },
+ 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);
@@ -175,32 +199,121 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointerup", this.onCloseUp);
}
}
+ @action
onMinimizeDown = (e: React.PointerEvent): void => {
e.stopPropagation();
+ this._iconDoc = undefined;
if (e.button === 0) {
+ this._downX = e.pageX;
+ this._downY = e.pageY;
+ this._removeIcon = false;
+ 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 (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);
+
+ if (selectedDocs.length > 1) {
+ this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+ this.moveIconDoc(this._iconDoc);
+ } else {
+ this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
+ }
+ this._removeIcon = snapped;
+ }
}
+ @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);
+ if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
+ selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
+ }
+ if (!this._removeIcon) {
+ if (selectedDocs.length === 1)
+ this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
+ else {
+ let docViews = SelectionManager.ViewsSortedVertically();
+ let topDocView = docViews[0];
+ let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
+ if (ind !== -1) {
+ topDocView.templates.splice(ind, 1);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ topDocView.addTemplate(Templates.Bullet);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document));
+ }
+ }
+ }
+ this._removeIcon = false;
}
+ runInAction(() => this._minimizedX = this._minimizedY = 0);
}
+ @action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
+ let doc = selected[0].props.Document;
+ let iconDoc = Docs.IconDocument(layoutString);
+ iconDoc.isButton = true;
+ iconDoc.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title);
+ iconDoc.labelField = this._fieldKey;
+ iconDoc[this._fieldKey] = selected.length > 1 ? "collection" : undefined;
+ iconDoc.isMinimized = false;
+ iconDoc.width = Number(MINIMIZED_ICON_SIZE);
+ iconDoc.height = Number(MINIMIZED_ICON_SIZE);
+ iconDoc.x = NumCast(doc.x);
+ iconDoc.y = NumCast(doc.y) - 24;
+ iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
+ doc.minimizedDoc = iconDoc;
+ selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
+ return iconDoc;
+ }
+ @action
+ public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
+ let doc = docView.props.Document;
+ let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
+
+ if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
+ const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
+ iconDoc = this.createIcon([docView], layout);
+ }
+ return iconDoc;
+ }
+ moveIconDoc(iconDoc: Doc) {
+ let selView = SelectionManager.SelectedDocuments()[0];
+ let zoom = NumCast(selView.props.Document.zoomBasis, 1);
+ let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom).
+ transformPoint(this._minimizedX - 12, this._minimizedY - 12);
+ iconDoc.x = where[0] + NumCast(selView.props.Document.x);
+ iconDoc.y = where[1] + NumCast(selView.props.Document.y);
+ }
+
+ @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);
@@ -221,11 +334,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
}
+ @action
onLinkerButtonMoved = (e: PointerEvent): void => {
if (this._linkerButton.current !== null) {
document.removeEventListener("pointermove", this.onLinkerButtonMoved);
document.removeEventListener("pointerup", this.onLinkerButtonUp);
- let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0].props.Document);
+ let selDoc = SelectionManager.SelectedDocuments()[0];
+ let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
+ let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
+ FormattedTextBox.InputBoxOverlay = undefined;
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
dragComplete: action(emptyFunction),
@@ -269,7 +386,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":
@@ -308,39 +425,45 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- MainOverlayTextBox.Instance.SetTextDoc();
+ runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
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);
- let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- let nheight = doc.GetNumber(KeyStore.NativeHeight, 0);
- let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0);
- let x = doc.GetOrCreate(KeyStore.X, NumberField);
- let y = doc.GetOrCreate(KeyStore.Y, NumberField);
+ let doc = PositionDocument(element.props.Document);
+ let width = FieldValue(doc.width, 0);
+ let nwidth = FieldValue(doc.nativeWidth, 0);
+ let nheight = FieldValue(doc.nativeHeight, 0);
+ let height = FieldValue(doc.height, nwidth ? nheight / nwidth * width : 0);
+ let x = FieldValue(doc.x, 0);
+ let y = FieldValue(doc.y, 0);
let scale = width / rect.width;
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- x.Data += dX * (actualdW - width);
- y.Data += dY * (actualdH - height);
- var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0);
+ x += dX * (actualdW - width);
+ y += dY * (actualdH - height);
+ doc.x = x;
+ doc.y = y;
+ var nativeWidth = FieldValue(doc.nativeWidth, 0);
+ var nativeHeight = FieldValue(doc.nativeHeight, 0);
if (nativeWidth > 0 && nativeHeight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
actualdH = nativeHeight / nativeWidth * actualdW;
}
else actualdW = nativeWidth / nativeHeight * actualdH;
}
- doc.SetNumber(KeyStore.Width, actualdW);
- doc.SetNumber(KeyStore.Height, actualdH);
+ doc.width = actualdW;
+ doc.height = actualdH;
}
});
}
+ @action
onPointerUp = (e: PointerEvent): void => {
e.stopPropagation();
+ this._resizing = "";
+ this.Interacting = false;
+ SelectionManager.ReselectAll();
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
@@ -349,17 +472,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);
- if (field instanceof TextField) {
- return (field).GetValue();
+ @computed
+ get selectionTitle(): string {
+ if (SelectionManager.SelectedDocuments().length === 1) {
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "string") {
+ return field;
}
- else if (field instanceof NumberField) {
- return (field).GetValue().toString();
+ else if (typeof field === "number") {
+ return field.toString();
}
+ } else if (SelectionManager.SelectedDocuments().length > 1) {
+ return "-multiple-";
}
- return this._title;
+ return "-unset-";
}
changeFlyoutContent = (): void => {
@@ -368,53 +494,81 @@ 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(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
+ </div>);
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
- let linkToSize = selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length;
- let linkFromSize = selFirst.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []).length;
+ let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
+ let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
let linkCount = linkToSize + linkFromSize;
linkButton = (<Flyout
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>
- </Flyout>);
+ <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
+ </Flyout >);
}
+
+ let templates: Map<Template, boolean> = new Map();
+ Array.from(Object.values(Templates.TemplateList)).map(template => {
+ let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
+ let temps = doc.props.Document.templates;
+ if (temps instanceof List) {
+ temps.map(temp => {
+ if (temp !== Templates.Bullet.Layout || i === 0) {
+ res.push(temp);
+ }
+ })
+ }
+ return res
+ }, [] as string[]);
+ let checked = false;
+ docTemps.forEach(temp => {
+ if (template.Layout === temp) {
+ 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.onBackgroundDown} 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>
@@ -425,9 +579,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
- <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
- <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div>
+ <div className="link-button-container">
+ <div className="linkButtonWrapper">
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
+ </div>
+ <div className="linkButtonWrapper">
+ <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
+ <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
+ </div>
+ </div>
+ <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+ </div>
</div >
</div>
);
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 2f17c6c51..73467eb9d 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -60,7 +60,7 @@ export class EditableView extends React.Component<EditableProps> {
return (
<div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
onClick={action(() => this.editing = true)} >
- {this.props.contents}
+ <span>{this.props.contents}</span>
</div>
);
}
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 47ee8eb85..1c0d13545 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,9 +1,5 @@
import { action, computed, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../fields/Document";
-import { FieldWaiting } from "../../fields/Field";
-import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { KeyStore } from "../../fields/KeyStore";
import { Utils } from "../../Utils";
import { Transform } from "../util/Transform";
import "./InkingCanvas.scss";
@@ -11,10 +7,13 @@ import { InkingControl } from "./InkingControl";
import { InkingStroke } from "./InkingStroke";
import React = require("react");
import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
- Document: Document;
+ Document: Doc;
children: () => JSX.Element[];
}
@@ -23,7 +22,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
@observable inkMidX: number = 0;
@observable inkMidY: number = 0;
- private previousState?: StrokeMap;
+ private previousState?: Map<string, StrokeData>;
private _currentStrokeId: string = "";
public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean {
return stroke.pathData.reduce((inside: boolean, val) => inside ||
@@ -33,9 +32,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
componentDidMount() {
- this.props.Document.GetTAsync(KeyStore.Ink, InkField, ink => runInAction(() => {
+ PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => {
if (ink) {
- let bounds = Array.from(ink.Data).reduce(([mix, max, miy, may], [id, strokeData]) =>
+ let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
strokeData.pathData.reduce(([mix, max, miy, may], p) =>
[Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)],
[mix, max, miy, may]),
@@ -47,13 +46,13 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
@computed
- get inkData(): StrokeMap {
- let map = this.props.Document.GetT(KeyStore.Ink, InkField);
- return !map || map === FieldWaiting ? new Map : new Map(map.Data);
+ get inkData(): Map<string, StrokeData> {
+ let map = Cast(this.props.Document.ink, InkField);
+ return !map ? new Map : new Map(map.inkData);
}
- set inkData(value: StrokeMap) {
- this.props.Document.SetDataOnPrototype(KeyStore.Ink, value, InkField);
+ set inkData(value: Map<string, StrokeData>) {
+ Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value));
}
@action
@@ -78,7 +77,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
+ page: NumCast(this.props.Document.curPage, -1)
});
this.inkData = data;
}
@@ -137,24 +136,28 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get drawnPaths() {
- let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+ let curPage = NumCast(this.props.Document.curPage, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
if (strokeData.page === -1 || strokeData.page === curPage) {
- paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData}
+ paths.push(<InkingStroke key={id} id={id}
+ line={strokeData.pathData}
+ count={strokeData.pathData.length}
offsetX={this.maxCanvasDim - this.inkMidX}
offsetY={this.maxCanvasDim - this.inkMidY}
- color={strokeData.color} width={strokeData.width}
- tool={strokeData.tool} deleteCallback={this.removeLine} />);
+ color={strokeData.color}
+ width={strokeData.width}
+ tool={strokeData.tool}
+ deleteCallback={this.removeLine} />);
}
return paths;
}, [] as JSX.Element[]);
- return [<svg className={`inkingCanvas-paths-markers`} key="Markers"
+ 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)}
+ {paths.filter(path => path.props.tool !== InkTool.Highlighter)}
</svg>,
- <svg className={`inkingCanvas-paths-ink`} key="Pens"
+ <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)}
+ {paths.filter(path => path.props.tool === InkTool.Highlighter)}
</svg>];
}
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 9a68f0671..4b3dbd4e0 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -2,15 +2,14 @@ import { observable, action, computed } from "mobx";
import { CirclePicker, ColorResult } from 'react-color';
import React = require("react");
-import { InkTool } from "../../fields/InkField";
import { observer } from "mobx-react";
import "./InkingControl.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
import { SelectionManager } from "../util/SelectionManager";
-import { KeyStore } from "../../fields/KeyStore";
-import { TextField } from "../../fields/TextField";
+import { InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
library.add(faPen, faHighlighter, faEraser, faBan);
@@ -39,7 +38,7 @@ export class InkingControl extends React.Component {
if (SelectionManager.SelectedDocuments().length === 1) {
var sdoc = SelectionManager.SelectedDocuments()[0];
if (sdoc.props.ContainingCollectionView) {
- sdoc.props.Document.SetDataOnPrototype(KeyStore.BackgroundColor, color.hex, TextField);
+ Doc.SetOnPrototype(sdoc.props.Document, "backgroundColor", color.hex);
}
}
}
diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss
new file mode 100644
index 000000000..cdbfdcff3
--- /dev/null
+++ b/src/client/views/InkingStroke.scss
@@ -0,0 +1,3 @@
+.inkingStroke-marker {
+ mix-blend-mode: multiply
+} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 0f05da22c..37b1f5899 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,14 +1,16 @@
import { observer } from "mobx-react";
-import { observable } from "mobx";
+import { observable, trace } from "mobx";
import { InkingControl } from "./InkingControl";
-import { InkTool } from "../../fields/InkField";
import React = require("react");
+import { InkTool } from "../../new_fields/InkField";
+import "./InkingStroke.scss";
interface StrokeProps {
offsetX: number;
offsetY: number;
id: string;
+ count: number;
line: Array<{ x: number, y: number }>;
color: string;
width: string;
@@ -48,10 +50,12 @@ export class InkingStroke extends React.Component<StrokeProps> {
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
return (
- <path d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
+ <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
);
}
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 4373534b2..5c5c252e9 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -1,5 +1,6 @@
@import "globalCssVariables";
@import "nodeModuleOverrides";
+
html,
body {
width: 100%;
@@ -7,9 +8,13 @@ body {
overflow: auto;
font-family: $sans-serif;
margin: 0;
- position:absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
+}
+
+div {
+ user-select: none;
}
#dash-title {
@@ -42,7 +47,9 @@ h1 {
}
.jsx-parser {
- width:100%
+ width:100%;
+ pointer-events: none;
+ border-radius: inherit;
}
p {
@@ -53,7 +60,7 @@ p {
::-webkit-scrollbar {
-webkit-appearance: none;
height: 5px;
- width: 5px;
+ width: 10px;
}
::-webkit-scrollbar-thumb {
@@ -114,6 +121,7 @@ button:hover {
position: absolute;
bottom: 62px;
left: 24px;
+
.toolbar-button {
display: block;
margin-bottom: 10px;
@@ -123,8 +131,9 @@ button:hover {
// add nodes menu. Note that the + button is actually an input label, not an actual button.
#add-nodes-menu {
position: absolute;
- bottom: 24px;
+ bottom: 22px;
left: 24px;
+
label {
background: $dark-color;
color: $light-color;
@@ -137,44 +146,52 @@ button:hover {
cursor: pointer;
transition: transform 0.2s;
}
+
label p {
padding-left: 10.5px;
- padding-top: 3px;
}
+
label:hover {
background: $main-accent;
transform: scale(1.15);
}
+
input {
display: none;
}
+
input:not(:checked)~#add-options-content {
display: none;
}
+
input:checked~label {
transform: rotate(45deg);
transition: transform 0.5s;
cursor: pointer;
}
}
+
#root {
overflow: visible;
}
+
#main-div {
- width:100%;
- height:100%;
- position:absolute;
+ width: 100%;
+ height: 100%;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
overflow: scroll;
+ z-index: 1;
}
#mainContent-div {
- width:100%;
- height:100%;
- position:absolute;
+ width: 100%;
+ height: 100%;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
}
+
#add-options-content {
display: table;
opacity: 1;
@@ -188,7 +205,8 @@ button:hover {
ul#add-options-list {
list-style: none;
- padding: 0;
+ padding: 5 0 0 0;
+
li {
display: inline-block;
padding: 0;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 5cae4fdaf..617580332 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,17 +8,10 @@ import * as React from 'react';
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, 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 { RouteStore } from '../../server/RouteStore';
-import { ServerUtils } from '../../server/ServerUtil';
-import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne } from '../../Utils';
-import { Documents } from '../documents/Documents';
+import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { Docs } from '../documents/Documents';
import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel';
import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel';
import { Gateway, NorthstarSettings } from '../northstar/manager/Gateway';
@@ -26,10 +19,10 @@ 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 { PresentationView } from './PresentationView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
@@ -38,6 +31,12 @@ import "./Main.scss";
import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
import { PreviewCursor } from './PreviewCursor';
+import { SelectionManager } from '../util/SelectionManager';
+import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+import { listSpec } from '../../new_fields/Schema';
+import { Id } from '../../new_fields/RefField';
@observer
@@ -47,11 +46,13 @@ export class Main extends React.Component {
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
- @computed private get mainContainer(): Document | undefined | FIELD_WAITING {
- return CurrentUserUtils.UserDocument.GetT(KeyStore.ActiveWorkspace, Document);
+ @computed private get mainContainer(): Opt<Doc> {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
}
- private set mainContainer(doc: Document | undefined | FIELD_WAITING) {
- doc && CurrentUserUtils.UserDocument.Set(KeyStore.ActiveWorkspace, doc);
+ private set mainContainer(doc: Opt<Doc>) {
+ if (doc) {
+ CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ }
}
constructor(props: Readonly<{}>) {
@@ -84,11 +85,11 @@ export class Main extends React.Component {
this.initEventListeners();
this.initAuthenticationRouters();
- try {
- this.initializeNorthstar();
- } catch (e) {
+ // try {
+ // this.initializeNorthstar();
+ // } catch (e) {
- }
+ // }
}
componentDidMount() { window.onpopstate = this.onHistory; }
@@ -98,9 +99,8 @@ export class Main extends React.Component {
onHistory = () => {
if (window.location.pathname !== RouteStore.home) {
let pathname = window.location.pathname.split("/");
- CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
- Server.GetField(CurrentUserUtils.MainDocId, action((field: Opt<Field>) => {
- if (field instanceof Document) {
+ DocServer.GetRefField(pathname[pathname.length - 1]).then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
this.openWorkspace(field, true);
}
}));
@@ -111,6 +111,12 @@ export class Main extends React.Component {
// 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)) {
@@ -119,56 +125,55 @@ export class Main extends React.Component {
}), true);
}
- initAuthenticationRouters = () => {
+ initAuthenticationRouters = async () => {
// Load the user's active workspace, or create a new one if initial session after signup
if (!CurrentUserUtils.MainDocId) {
- CurrentUserUtils.UserDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => {
- if (doc) {
- CurrentUserUtils.MainDocId = doc.Id;
- this.openWorkspace(doc);
- } else {
- this.createNewWorkspace();
- }
- });
+ const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ if (doc) {
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
} else {
- Server.GetField(CurrentUserUtils.MainDocId).then(field =>
- field instanceof Document ? this.openWorkspace(field) :
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+ field instanceof Doc ? this.openWorkspace(field) :
this.createNewWorkspace(CurrentUserUtils.MainDocId));
}
}
@action
- createNewWorkspace = (id?: string): void => {
- 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)] }] };
- let mainDoc = Documents.DockDocument(JSON.stringify(dockingLayout), { title: `Main Container ${list.Data.length + 1}` }, id);
- list.Data.push(mainDoc);
- CurrentUserUtils.MainDocId = mainDoc.Id;
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- let pendingDocument = Documents.SchemaDocument([], { title: "New Mobile Uploads" });
- mainDoc.Set(KeyStore.OptionalRightCollection, pendingDocument);
- }, 0);
- }
- }));
+ createNewWorkspace = async (id?: string) => {
+ const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ if (list) {
+ let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` });
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
+ let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` });
+ list.push(mainDoc);
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => {
+ this.openWorkspace(mainDoc);
+ let pendingDocument = Docs.SchemaDocument([], { title: "New Mobile Uploads" });
+ mainDoc.optionalRightCollection = pendingDocument;
+ }, 0);
+ }
}
@action
- openWorkspace = (doc: Document, fromHistory = false): void => {
+ openWorkspace = async (doc: Doc, fromHistory = false) => {
+ CurrentUserUtils.MainDocId = doc[Id];
this.mainContainer = doc;
- fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id);
- 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(() =>
- col && col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) =>
- f && f.Data.length > 0 && CollectionDockingView.Instance.AddRightSplit(col))
- , 100)
- );
+ fromHistory || window.history.pushState(null, StrCast(doc.title), "/doc/" + doc[Id]);
+ const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
+ // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
+ setTimeout(async () => {
+ if (col) {
+ const l = Cast(col.data, listSpec(Doc));
+ if (l && l.length > 0) {
+ CollectionDockingView.Instance.AddRightSplit(col);
+ }
+ }
+ }, 100);
}
-
@computed
get mainContent() {
let pwidthFunc = () => this.pwidth;
@@ -180,6 +185,7 @@ export class Main extends React.Component {
<div ref={measureRef} id="mainContent-div">
{!mainCont ? (null) :
<DocumentView Document={mainCont}
+ toggleMinimized={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
@@ -188,35 +194,38 @@ export class Main extends React.Component {
PanelHeight={pheightFunc}
isTopMost={true}
selectOnLoad={false}
- focus={emptyDocFunction}
+ focus={emptyFunction}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
ContainingCollectionView={undefined} />}
+ <PresentationView key="presentation" />
</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() {
+ nodesMenu() {
+
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
let weburl = "https://cs.brown.edu/courses/cs166/";
let audiourl = "http://techslides.com/demos/samples/sample.mp3";
let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
- let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }));
- let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
- let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true }));
- 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" }));
+ let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
+ let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
+ let addSchemaNode = action(() => Docs.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));
+ let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
+ // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
+ let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
+ let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+ let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
+ let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
- let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Document][] = [
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
[React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
[React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
[React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
@@ -248,36 +257,20 @@ export class Main extends React.Component {
/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
@computed
get miscButtons() {
- let 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 [
- <button className="clear-db-button" key="clear-db" onClick={clearDatabase}>Clear Database</button>,
+ <button className="clear-db-button" key="clear-db" onClick={DocServer.DeleteDatabase}>Clear Database</button>,
<div id="toolbar" key="toolbar">
<button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
<button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
<button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
</div >,
- <div className="main-buttonDiv" key="workspaces" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}>
- <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), emptyFunction)}>Log Out</button></div>
+ <button onClick={() => request.get(DocServer.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() {
return (
<div id="main-div">
@@ -285,9 +278,8 @@ export class Main extends React.Component {
{this.mainContent}
<PreviewCursor />
<ContextMenu />
- {this.nodesMenu}
+ {this.nodesMenu()}
{this.miscButtons}
- {this.workspaceMenu}
<InkingControl />
<MainOverlayTextBox />
</div>
@@ -295,17 +287,17 @@ export class Main extends React.Component {
}
// --------------- Northstar hooks ------------- /
- private _northstarSchemas: Document[] = [];
+ private _northstarSchemas: Doc[] = [];
@action SetNorthstarCatalog(ctlog: Catalog) {
CurrentUserUtils.NorthstarDBCatalog = ctlog;
if (ctlog && ctlog.schemas) {
ctlog.schemas.map(schema => {
- let schemaDocuments: Document[] = [];
+ let schemaDocuments: Doc[] = [];
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) {
+ promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {
+ if (field instanceof Doc) {
schemaDocuments.push(field);
} else {
var atmod = new ColumnAttributeModel(attr);
@@ -313,12 +305,12 @@ export class Main extends React.Component {
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"));
+ schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }));
}
})));
return promises;
}, [] as Promise<void>[])).finally(() =>
- this._northstarSchemas.push(Documents.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })));
+ this._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })));
});
}
}
@@ -330,7 +322,7 @@ export class Main extends React.Component {
}
(async () => {
- await Documents.initProtos();
+ await Docs.initProtos();
await CurrentUserUtils.loadCurrentUser();
ReactDOM.render(<Main />, document.getElementById('root'));
})();
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
index 697d68c8c..f6a746e63 100644
--- a/src/client/views/MainOverlayTextBox.scss
+++ b/src/client/views/MainOverlayTextBox.scss
@@ -7,6 +7,7 @@
overflow: visible;
top: 0;
left: 0;
+ pointer-events: none;
z-index: $mainTextInput-zindex;
.formattedTextBox-cont {
background-color: rgba(248, 6, 6, 0.001);
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 8cb01117c..91f626737 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -1,15 +1,10 @@
-import { action, observable, trace } from 'mobx';
+import { action, observable, reaction } 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 } from '../../Utils';
-import '../northstar/model/ModelExtensions';
-import '../northstar/utils/Extensions';
+import { emptyFunction, returnTrue, returnZero } from '../../Utils';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
+import "normalize.css";
import "./MainOverlayTextBox.scss";
import { FormattedTextBox } from './nodes/FormattedTextBox';
@@ -19,11 +14,8 @@ interface MainOverlayTextBoxProps {
@observer
export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
public static Instance: MainOverlayTextBox;
- @observable public TextDoc?: Document = undefined;
- public TextScroll: number = 0;
- private _textRect: any;
- private _textXf: Transform = Transform.Identity();
- private _textFieldKey: Key = KeyStore.Data;
+ @observable _textXf: () => Transform = () => Transform.Identity();
+ private _textFieldKey: string = "data";
private _textColor: string | null = null;
private _textTargetDiv: HTMLDivElement | undefined;
private _textProxyDiv: React.RefObject<HTMLDivElement>;
@@ -32,31 +24,32 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
super(props);
this._textProxyDiv = React.createRef();
MainOverlayTextBox.Instance = this;
+ reaction(() => FormattedTextBox.InputBoxOverlay,
+ (box?: FormattedTextBox) => {
+ if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
+ else this.setTextDoc();
+ });
}
@action
- SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: Transform) {
+ private setTextDoc(textFieldKey?: string, 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._textXf = tx ? tx : () => Transform.Identity();
this._textTargetDiv = div;
if (div) {
+ if (div.parentElement && div.parentElement instanceof HTMLDivElement && div.parentElement.id === "screenSpace") this._textXf = () => Transform.Identity();
this._textColor = div.style.color;
div.style.color = "transparent";
- this._textRect = div.getBoundingClientRect();
- 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;
+ this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
}
}
@@ -66,14 +59,13 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
document.addEventListener('pointerup', this.textBoxUp);
}
}
+ @action
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);
+ let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
+ 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, {
@@ -90,18 +82,15 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
}
render() {
- 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;
- let t = this._textXf.transformPoint(0, 0);
- let s = this._textXf.transformPoint(1, 0);
- s[0] = Math.sqrt((s[0] - t[0]) * (s[0] - t[0]) + (s[1] - t[1]) * (s[1] - t[1]));
- return <div className="mainOverlayTextBox-textInput" style={{ pointerEvents: "none", transform: `translate(${x}px, ${y}px) scale(${1 / s[0]},${1 / s[0]})`, width: "auto", height: "auto" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} style={{ pointerEvents: "none", transform: `scale(${1}, ${1})`, width: `${w * s[0]}px`, height: `${h * s[0]}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} focus={emptyDocFunction} />
+ if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
+ let textRect = this._textTargetDiv.getBoundingClientRect();
+ let s = this._textXf().Scale;
+ return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
+ <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
+ style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
+ <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
+ selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
</div>
</ div>;
}
diff --git a/src/client/views/PresentationView.scss b/src/client/views/PresentationView.scss
new file mode 100644
index 000000000..7c5677f0d
--- /dev/null
+++ b/src/client/views/PresentationView.scss
@@ -0,0 +1,68 @@
+.presentationView-cont {
+ position: absolute;
+ background: white;
+ z-index: 1;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ right: 0;
+ top:0;
+ bottom:0;
+}
+
+.presentationView-item {
+ width: 220px;
+ height: 40px;
+ vertical-align: center;
+ padding-top: 15px;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+}
+
+.presentationView-item:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentationView-heading {
+ margin-top: 0px;
+ height: 40px;
+ background: lightseagreen;
+ padding: 30px;
+}
+.presentationView-title {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ font-size: 25px;
+ float:left;
+}
+.presentation-icon{
+ float: right;
+ display: inline;
+ width: 10px;
+ margin-top: 7px;
+}
+.presentationView-header {
+ padding-top: 1px;
+ padding-bottom: 1px;
+ font-size: 15px;
+ float:left;
+ }
+
+ .presentation-next{
+ float: right;
+ }
+ .presentation-back{
+ float: left;
+ }
+ .presentation-next:hover{
+ transition: all .1s;
+ background: #AAAAAA
+}
+.presentation-back:hover{
+ transition: all .1s;
+ background: #AAAAAA
+} \ No newline at end of file
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
new file mode 100644
index 000000000..94867b813
--- /dev/null
+++ b/src/client/views/PresentationView.tsx
@@ -0,0 +1,190 @@
+import { observer } from "mobx-react";
+import React = require("react")
+import { observable, action, runInAction, reaction } from "mobx";
+import "./PresentationView.scss"
+import "./Main.tsx";
+import { DocumentManager } from "../util/DocumentManager";
+import { Utils } from "../../Utils";
+import { Doc } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue } from "../../new_fields/Types";
+import { Id } from "../../new_fields/RefField";
+import { List } from "../../new_fields/List";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+
+export interface PresViewProps {
+ //Document: Doc;
+}
+
+@observer
+/**
+ * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ */
+class PresentationViewItem extends React.Component<PresViewProps> {
+
+ @observable Document: Doc;
+ constructor(props: PresViewProps) {
+ super(props);
+ this.Document = FieldValue(Cast(FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc))!.presentationView, Doc))!;
+ }
+ //look at CollectionFreeformView.focusDocument(d)
+ @action
+ openDoc = (doc: Doc) => {
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+
+ /**
+ * Removes a document from the presentation view
+ **/
+ @action
+ public RemoveDoc(doc: Doc) {
+ const value = Cast(this.Document.data, listSpec(Doc), []);
+ let index = -1;
+ for (let i = 0; i < value.length; i++) {
+ if (value[i][Id] === doc[Id]) {
+ index = i;
+ break;
+ }
+ }
+ if (index !== -1) {
+ value.splice(index, 1);
+ }
+ }
+
+ /**
+ * Renders a single child document. It will just append a list element.
+ * @param document The document to render.
+ */
+ renderChild(document: Doc) {
+ let title = document.title;
+
+ //to get currently selected presentation doc
+ let selected = NumCast(this.Document.selectedDoc, 0);
+
+ // finally, if it's a normal document, then render it as such.
+ const children = Cast(this.Document.data, listSpec(Doc));
+ const styles: any = {};
+ if (children && children[selected] === document) {
+ //this doc is selected
+ styles.background = "gray";
+ }
+ return (
+ <li className="presentationView-item" style={styles} key={Utils.GenerateGuid()}>
+ <div className="presentationView-header" onClick={() => this.openDoc(document)}>{title}</div>
+ <div className="presentation-icon" onClick={() => this.RemoveDoc(document)}>X</div>
+ </li>
+ );
+
+ }
+
+ render() {
+ const children = Cast(this.Document.data, listSpec(Doc), []);
+
+ return (
+ <div>
+ {children.map(value => this.renderChild(value))}
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class PresentationView extends React.Component<PresViewProps> {
+ public static Instance: PresentationView;
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ closePresentation = action(() => this.Document!.width = 0);
+ next = () => {
+ const current = NumCast(this.Document!.selectedDoc);
+ const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc)));
+ if (allDocs && current < allDocs.length + 1) {
+ //can move forwards
+ this.Document!.selectedDoc = current + 1;
+ const doc = allDocs[current + 1];
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+
+ }
+ back = () => {
+ const current = NumCast(this.Document!.selectedDoc);
+ const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc)));
+ if (allDocs && current - 1 >= 0) {
+ //can move forwards
+ this.Document!.selectedDoc = current - 1;
+ const doc = allDocs[current - 1];
+ let docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView) {
+ docView.props.focus(docView.props.Document);
+ }
+ }
+ }
+
+ private ref = React.createRef<HTMLDivElement>();
+
+ @observable Document?: Doc;
+ //initilize class variables
+ constructor(props: PresViewProps) {
+ super(props);
+ let self = this;
+ reaction(() =>
+ CurrentUserUtils.UserDocument.activeWorkspace,
+ (activeW) => {
+ if (activeW && activeW instanceof Doc) {
+ PromiseValue(Cast(activeW.presentationView, Doc)).
+ then(pv => runInAction(() =>
+ self.Document = pv ? pv : (activeW.presentationView = new Doc())))
+ }
+ },
+ { fireImmediately: true });
+ PresentationView.Instance = this;
+ }
+
+ /**
+ * Adds a document to the presentation view
+ **/
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const data = Cast(this.Document!.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ this.Document!.data = new List([doc]);
+ }
+
+ this.Document!.width = 300;
+ }
+
+ render() {
+ if (!this.Document)
+ return (null);
+ let titleStr = this.Document.Title;
+ let width = NumCast(this.Document.width);
+
+ //TODO: next and back should be icons
+ return (
+ <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
+ <div className="presentationView-heading">
+ <div className="presentationView-title">{titleStr}</div>
+ <div className='presentation-icon' onClick={this.closePresentation}>X</div></div>
+ <div>
+ <div className="presentation-back" onClick={this.back}>back</div>
+ <div className="presentation-next" onClick={this.next}>next</div>
+
+ </div>
+ <ul>
+ <PresentationViewItem />
+ </ul>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index ff8434681..78024a58c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -7,30 +7,48 @@ 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;
- PreviewCursor.hide();
}
- @observable static clickPoint = [0, 0];
- @observable public static Visible = false;
- @observable public static hide = () => { };
+ 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" || e.key === "q") {
+ PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
+ PreviewCursor.Visible = false;
+ }
+ }
+ }
@action
- public static Show(hide: any, x: number, y: number) {
- this.clickPoint = [x, y];
- this.hide = hide;
+ 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) {
+ 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 }}>
+ style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}>
I
</div >;
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
new file mode 100644
index 000000000..e2b3bd07a
--- /dev/null
+++ b/src/client/views/TemplateMenu.tsx
@@ -0,0 +1,91 @@
+import { observable, computed, action, trace } from "mobx";
+import React = require("react");
+import { observer } from "mobx-react";
+import './DocumentDecorations.scss';
+import { Template } from "./Templates";
+import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../new_fields/List";
+import { Doc } from "../../new_fields/Doc";
+import { NumCast } from "../../new_fields/Types";
+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 className="templateToggle">
+ <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 {
+ docs: DocumentView[];
+ templates: Map<Template, boolean>;
+}
+
+@observer
+export class TemplateMenu extends React.Component<TemplateMenuProps> {
+ @observable private _hidden: boolean = true;
+
+ constructor(props: TemplateMenuProps) {
+ super(props);
+ console.log("");
+ }
+
+ @action
+ toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
+ if (event.target.checked) {
+ if (template.Name == "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.addTemplate(template);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document));
+ } else {
+ this.props.docs.map(d => d.addTemplate(template));
+ }
+ this.props.templates.set(template, true);
+ } else {
+ if (template.Name == "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.removeTemplate(template);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ this.props.docs.map(d => d.removeTemplate(template));
+ }
+ this.props.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.props.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..02f9aa510
--- /dev/null
+++ b/src/client/views/Templates.tsx
@@ -0,0 +1,84 @@
+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 id="screenSpace" style="margin-top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={"caption"} /></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={"caption"}/></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={"caption"}/></div> </div>`
+ );
+
+ export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div></div>`
+ );
+ export const Title = new Template("Title", TemplatePosition.InnerTop,
+ `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div></div>`
+ );
+
+ export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+ `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="isBullet" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+ <img id="isBullet" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
+ width="15px" height="15px"/>
+ </div>
+ </div>`
+ );
+
+ export const TemplateList: Template[] = [Title, TitleOverlay, OuterCaption, InnerCaption, SideCaption, Bullet];
+
+ 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/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index a8b061b04..2b1f7bb37 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,13 +1,14 @@
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 { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from '../../../fields/ListField';
-import { NumberField } from '../../../fields/NumberField';
import { ContextMenu } from '../ContextMenu';
import { FieldViewProps } from '../nodes/FieldView';
+import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
+import { Doc, FieldResult, Opt } from '../../../new_fields/Doc';
+import { listSpec } from '../../../new_fields/Schema';
+import { List } from '../../../new_fields/List';
+import { Id } from '../../../new_fields/RefField';
+import { SelectionManager } from '../../util/SelectionManager';
export enum CollectionViewType {
Invalid,
@@ -18,9 +19,9 @@ export enum CollectionViewType {
}
export interface CollectionRenderProps {
- addDocument: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Document) => boolean;
- moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
}
@@ -37,11 +38,9 @@ export interface CollectionViewProps extends FieldViewProps {
export class CollectionBaseView extends React.Component<CollectionViewProps> {
get collectionViewType(): CollectionViewType | undefined {
let Document = this.props.Document;
- let viewField = Document.GetT(KeyStore.ViewType, NumberField);
- if (viewField === FieldWaiting) {
- return undefined;
- } else if (viewField) {
- return viewField.Data;
+ let viewField = Cast(Document.viewType, "number");
+ if (viewField !== undefined) {
+ return viewField;
} else {
return CollectionViewType.Invalid;
}
@@ -60,100 +59,77 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
this.props.whenActiveChanged(isActive);
}
- createsCycle(documentToAdd: Document, containerDocument: Document): boolean {
- if (!(documentToAdd instanceof Document)) {
+ createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
+ if (!(documentToAdd instanceof Doc)) {
return false;
}
- let data = documentToAdd.GetList(KeyStore.Data, [] as Document[]);
+ let data = Cast(documentToAdd.data, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
for (const doc of data.filter(d => d instanceof Document)) {
if (this.createsCycle(doc, containerDocument)) {
return true;
}
}
- let annots = documentToAdd.GetList(KeyStore.Annotations, [] as Document[]);
+ let annots = Cast(documentToAdd.annotations, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
for (const annot of annots) {
if (this.createsCycle(annot, containerDocument)) {
return true;
}
}
- for (let containerProto: FieldValue<Document> = containerDocument; containerProto && containerProto !== FieldWaiting; containerProto = containerProto.GetPrototype()) {
- if (containerProto.Id === documentToAdd.Id) {
+ for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
+ if (containerProto[Id] === documentToAdd[Id]) {
return true;
}
}
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?
+ @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
@action.bound
- addDocument(doc: Document, allowDuplicates: boolean = false): boolean {
+ addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
let props = this.props;
- var curPage = props.Document.GetNumber(KeyStore.CurPage, -1);
- doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage));
- if (this.isAnnotationOverlay) {
- doc.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1));
- }
+ var curPage = Cast(props.Document.curPage, "number", -1);
+ Doc.SetOnPrototype(doc, "page", curPage);
if (curPage >= 0) {
- doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document);
+ Doc.SetOnPrototype(doc, "annotationOn", props.Document);
}
- if (props.Document.Get(props.fieldKey) instanceof Field) {
+ if (!this.createsCycle(doc, props.Document)) {
//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) {
+ const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
+ let alreadyAdded = true;
+ if (value !== undefined) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
value.push(doc);
}
+ } else {
+ alreadyAdded = false;
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([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 + " " + 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);
- }
- 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 (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
+ let zoom = NumCast(this.props.Document.scale, 1);
+ Doc.SetOnPrototype(doc, "zoomBasis", zoom);
}
}
return true;
}
@action.bound
- removeDocument(doc: Document): boolean {
+ removeDocument(doc: Doc): boolean {
const props = this.props;
//TODO This won't create the field if it doesn't already exist
- const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>());
+ const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
let index = -1;
for (let i = 0; i < value.length; i++) {
- if (value[i].Id === doc.Id) {
+ let v = value[i];
+ if (v instanceof Doc && v[Id] === doc[Id]) {
index = i;
break;
}
}
- doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => {
+ PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => {
if (annotationOn === props.Document) {
- doc.Set(KeyStore.AnnotationOn, undefined, true);
+ doc.annotationOn = undefined;
}
});
@@ -168,11 +144,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
}
@action.bound
- moveDocument(doc: Document, targetCollection: Document, addDocument: (doc: Document) => boolean): boolean {
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
if (this.props.Document === targetCollection) {
return true;
}
if (this.removeDocument(doc)) {
+ SelectionManager.DeselectAll();
return addDocument(doc);
}
return false;
@@ -188,7 +165,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
};
const viewtype = this.collectionViewType;
return (
- <div className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
+ <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.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 39e27b601..159815ea5 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,36 +1,35 @@
-import * as GoldenLayout from "golden-layout";
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, trace } from "mobx";
+import { action, observable, reaction } from "mobx";
import { observer } from "mobx-react";
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, FieldWaiting } from "../../../fields/Field";
-import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne } from "../../../Utils";
-import { Server } from "../../Server";
-import { undoBatch } from "../../util/UndoManager";
+import * as GoldenLayout from "../../../client/goldenLayout";
+import { Doc, Field, Opt } from "../../../new_fields/Doc";
+import { FieldId, Id } from "../../../new_fields/RefField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { Transform } from '../../util/Transform';
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
-import React = require("react");
import { SubCollectionViewProps } from "./CollectionSubView";
-import { ServerUtils } from "../../../server/ServerUtil";
-import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
-import { TextField } from "../../../fields/TextField";
-import { ListField } from "../../../fields/ListField";
-import { thisExpression } from "babel-types";
+import React = require("react");
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
public static Instance: CollectionDockingView;
- public static makeDocumentConfig(document: Document) {
+ public static makeDocumentConfig(document: Doc, width?: number) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
- title: document.Title,
+ title: document.title,
+ width: width,
props: {
- documentId: document.Id,
+ documentId: document[Id],
//collectionDockingView: CollectionDockingView.Instance
}
};
@@ -38,7 +37,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
private _goldenLayout: any = null;
private _containerRef = React.createRef<HTMLDivElement>();
- private _fullScreen: any = null;
private _flush: boolean = false;
private _ignoreStateChange = "";
@@ -48,14 +46,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
}
- public StartOtherDrag(dragDocs: Document[], e: any) {
+ hack: boolean = false;
+ undohack: any = null;
+ public StartOtherDrag(dragDocs: Doc[], 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: emptyFunction, button: 0 }));
}
@action
- public OpenFullScreen(document: Document) {
+ public OpenFullScreen(document: Doc) {
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
@@ -64,26 +66,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.root.contentItems[0].addChild(docconfig);
docconfig.callDownwards('_$init');
this._goldenLayout._$maximiseItem(docconfig);
- this._fullScreen = docconfig;
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
}
+
+ @undoBatch
@action
- public CloseFullScreen() {
- if (this._fullScreen) {
- this._goldenLayout._$minimiseItem(this._fullScreen);
- this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen);
- this._fullScreen = null;
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
- this.stateChanged();
+ public CloseRightSplit(document: Doc) {
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ this._goldenLayout.root.contentItems[0].contentItems.map((child: any, i: number) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ child.contentItems[0].config.props.documentId == document[Id]) {
+ child.contentItems[0].remove();
+ this.layoutChanged(document);
+ this.stateChanged();
+ } else
+ child.contentItems.map((tab: any, j: number) => {
+ if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) {
+ child.contentItems[j].remove();
+ child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
+ this.stateChanged();
+ }
+ });
+ })
}
}
+ @action
+ layoutChanged(removed?: Doc) {
+ this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
+ this._goldenLayout.emit('sbcreteChanged');
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+ if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
+ this.stateChanged();
+ }
+
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@action
- public AddRightSplit(document: Document, minimize: boolean = false) {
+ public AddRightSplit(document: Doc, minimize: boolean = false) {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (docs) {
+ docs.push(document);
+ }
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
@@ -106,20 +134,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
newContentItem.config.width = 50;
}
if (minimize) {
- newContentItem.config.width = 10;
- newContentItem.config.height = 10;
+ // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
+ // newContentItem.config.width = 10;
+ // newContentItem.config.height = 10;
}
newContentItem.callDownwards('_$init');
- this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
- this._goldenLayout.emit('stateChanged');
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
- this.stateChanged();
+ this.layoutChanged();
return newContentItem;
}
setupGoldenLayout() {
- var config = this.props.Document.GetText(KeyStore.Data, "");
+ var config = StrCast(this.props.Document.dockingConfig);
if (config) {
if (!this._goldenLayout) {
this._goldenLayout = new GoldenLayout(JSON.parse(config));
@@ -154,7 +180,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
componentDidMount: () => void = () => {
if (this._containerRef.current) {
reaction(
- () => this.props.Document.GetText(KeyStore.Data, ""),
+ () => StrCast(this.props.Document.dockingConfig),
() => {
if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
setTimeout(() => this.setupGoldenLayout(), 1);
@@ -202,8 +228,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let y = e.clientY;
let docid = (e.target as any).DashDocId;
let tab = (e.target as any).parentElement as HTMLElement;
- Server.GetField(docid, action(async (sourceDoc: Opt<Field>) =>
- (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
+ DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
+ (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
} else
if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
e.stopPropagation();
@@ -212,12 +238,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let y = e.clientY;
let docid = (e.target as any).DashDocId;
let tab = (e.target as any).parentElement as HTMLElement;
- Server.GetField(docid, action((f: Opt<Field>) => {
- if (f instanceof Document) {
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
{
handlers: {
- dragComplete: action(emptyFunction),
+ dragComplete: emptyFunction,
},
hideSource: false
});
@@ -234,8 +260,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
stateChanged = () => {
+ let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
+ CollectionDockingView.Instance._removedDocs.map(theDoc =>
+ docs && docs.indexOf(theDoc) !== -1 &&
+ docs.splice(docs.indexOf(theDoc), 1));
+ CollectionDockingView.Instance._removedDocs.length = 0;
var json = JSON.stringify(this._goldenLayout.toConfig());
- this.props.Document.SetText(KeyStore.Data, json);
+ this.props.Document.dockingConfig = json;
+ if (this.undohack && !this.hack) {
+ this.undohack.end();
+ this.undohack = undefined;
+ }
+ this.hack = false;
}
itemDropped = () => {
@@ -249,40 +285,42 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return template.content.firstChild;
}
- tabCreated = (tab: any) => {
+ tabCreated = async (tab: any) => {
if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
- 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;
- });
- }));
+ if (tab.contentItem.config.fixed) {
+ tab.contentItem.parent.config.fixed = true;
+ }
+ DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
+ if (doc instanceof Doc) {
+ let counter: any = this.htmlToElement(`<div class="messageCounter">0</div>`);
+ tab.element.append(counter);
+ counter.DashDocId = tab.contentItem.config.props.documentId;
+ tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ () => {
+ const lf = Cast(doc.linkedFromDocs, listSpec(Doc), []);
+ const lt = Cast(doc.linkedToDocs, listSpec(Doc), []);
+ let count = (lf ? lf.length : 0) + (lt ? lt.length : 0);
+ counter.innerHTML = count;
+ tab.titleElement[0].textContent = doc.title;
+ }, { fireImmediately: true });
tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
}
- }));
+ });
}
tab.closeElement.off('click') //unbind the current click handler
- .click(function () {
+ .click(async function () {
if (tab.reactionDisposer) {
tab.reactionDisposer();
}
+ let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
tab.contentItem.remove();
});
}
+ _removedDocs: Doc[] = [];
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
@@ -291,13 +329,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.click(action(function () {
//if (confirm('really close this?')) {
stack.remove();
+ stack.contentItems.map(async (contentItem: any) => {
+ let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
+ });
//}
}));
stack.header.controlsContainer.find('.lm_popout') //get the close icon
.off('click') //unbind the current click handler
.click(action(function () {
- var url = ServerUtils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
- let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
+ stack.config.fixed = !stack.config.fixed;
+ // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+ // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
}));
}
@@ -307,6 +353,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
);
}
+
}
interface DockedFrameProps {
@@ -315,38 +362,45 @@ interface DockedFrameProps {
}
@observer
export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
-
_mainCont = React.createRef<HTMLDivElement>();
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
- @observable private _document: Opt<Document>;
+ @observable private _document: Opt<Doc>;
constructor(props: any) {
super(props);
- Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document));
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
}
- nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth);
- nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight);
+ nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
+ nativeHeight = () => NumCast(this._document!.nativeHeight, 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;
+ const nativeH = this.nativeHeight();
+ const nativeW = this.nativeWidth();
+ let wscale = this._panelWidth / nativeW;
+ return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
}
ScreenToLocalTransform = () => {
- 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());
+ 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; }
get content() {
+ if (!this._document) {
+ return (null);
+ }
return (
<div className="collectionDockingView-content" ref={this._mainCont}
style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView key={this._document!.Id} Document={this._document!}
+ <DocumentView key={this._document[Id]} Document={this._document}
+ toggleMinimized={emptyFunction}
+ bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={this.contentScaling}
@@ -357,15 +411,17 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
selectOnLoad={false}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
- focus={emptyDocFunction}
+ focus={emptyFunction}
+ bringToFront={emptyFunction}
ContainingCollectionView={undefined} />
- </div>);
+ </div >);
}
render() {
+ let theContent = this.content;
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>}
+ {({ measureRef }) => <div ref={measureRef}> {theContent} </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 229bc4059..b3762206a 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,6 +1,5 @@
-import { action } from "mobx";
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
import "./CollectionPDFView.scss";
import React = require("react");
@@ -8,32 +7,60 @@ import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormV
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
import { emptyFunction } from "../../../Utils";
+import { NumCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/RefField";
@observer
export class CollectionPDFView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string = "DataKey") {
+ public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionPDFView, fieldKey);
}
+ @observable _inThumb = false;
- private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); }
- private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
- @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : -1;
- @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : -1;
+ private set curPage(value: number) { this.props.Document.curPage = value; }
+ private get curPage() { return NumCast(this.props.Document.curPage, -1); }
+ private get numPages() { return NumCast(this.props.Document.numPages); }
+ @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1;
+ @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.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 = () => NumCast(this.props.Document.nativeWidth);
+ nativeHeight = () => NumCast(this.props.Document.nativeHeight);
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
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction });
}
}
@@ -50,7 +77,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 bced0d5d4..cfdb3ab22 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,6 +1,7 @@
@import "../globalCssVariables";
+
.collectionSchemaView-container {
border-width: $COLLECTION_BORDER_WIDTH;
border-color : $intermediate-color;
@@ -10,6 +11,10 @@
position: absolute;
width: 100%;
height: 100%;
+
+ .collectionSchemaView-cellContents {
+ height: $MAX_ROW_HEIGHT;
+ }
.collectionSchemaView-previewRegion {
position: relative;
@@ -104,7 +109,7 @@
}
.rt-tr-group {
direction: ltr;
- max-height: 44px;
+ max-height: $MAX_ROW_HEIGHT;
}
.rt-td {
border-width: 1px;
@@ -136,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;
@@ -153,9 +158,6 @@
}
.documentView-node:first-child {
background: $light-color;
- .imageBox-cont img {
- object-fit: contain;
- }
}
}
//options menu styling
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 08c9740d0..ae949b2ed 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,19 +2,14 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked } from "mobx";
+import { action, computed, observable, untracked, runInAction } from "mobx";
import { observer } from "mobx-react";
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 } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { emptyDocFunction, emptyFunction, returnFalse } from "../../../Utils";
-import { Server } from "../../Server";
+import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
import { SetupDrag } from "../../util/DragManager";
-import { CompileScript, ToField } from "../../util/Scripting";
+import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
import { anchorPoints, Flyout } from "../DocumentDecorations";
@@ -24,44 +19,47 @@ import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
+import { Opt, Field, Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { List } from "../../../new_fields/List";
+import { Id } from "../../../new_fields/RefField";
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@observer
-class KeyToggle extends React.Component<{ keyId: string, checked: boolean, toggle: (key: Key) => void }> {
- @observable key: Key | undefined;
-
+class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
constructor(props: any) {
super(props);
- Server.GetField(this.props.keyId, action((field: Opt<Field>) => field instanceof Key && (this.key = field)));
}
render() {
- 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 (
+ <div key={this.props.keyName}>
+ <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
+ {this.props.keyName}
+ </div>
+ );
}
}
@observer
-export class CollectionSchemaView extends CollectionSubView {
+export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _mainCont?: HTMLDivElement;
private _startSplitPercent = 0;
private DIVIDER_WIDTH = 4;
- @observable _columns: Array<Key> = [KeyStore.Title, KeyStore.Data, KeyStore.Author];
+ @observable _columns: Array<string> = ["title", "data", "author"];
@observable _selectedIndex = 0;
@observable _columnsPercentage = 0;
- @observable _keys: Key[] = [];
+ @observable _keys: string[] = [];
@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 COLLECTION_BORDER_WIDTH; }
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
+ @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
@@ -73,43 +71,38 @@ export class CollectionSchemaView extends CollectionSubView {
isTopMost: false,
selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
- focus: emptyDocFunction,
+ focus: emptyFunction,
active: returnFalse,
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 applyToDoc = (doc: Document, run: (args?: { [name: string]: any }) => any) => {
+ let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
const res = run({ this: doc });
if (!res.success) return false;
const field = res.result;
- if (field instanceof Field) {
- doc.Set(props.fieldKey, field);
- return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- doc.Set(props.fieldKey, dataField);
- return true;
- }
- }
- return false;
+ doc[props.fieldKey] = field;
+ return true;
};
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) {
- return field.ToScriptString();
+ let field = props.Document[props.fieldKey];
+ if (field) {
+ //TODO Types
+ // return field.ToScriptString();
+ return String(field);
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
@@ -118,21 +111,18 @@ export class CollectionSchemaView extends CollectionSubView {
}
return applyToDoc(props.Document, script.run);
}}
- OnFillDown={(value: string) => {
+ OnFillDown={async (value: string) => {
let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
if (!script.compiled) {
return;
}
const run = script.run;
//TODO This should be able to be refactored to compile the script once
- this.props.Document.GetTAsync<ListField<Document>>(this.props.fieldKey, ListField).then((val) => {
- if (val) {
- val.Data.forEach(doc => applyToDoc(doc, run));
- }
- });
+ const val = await DocListCast(this.props.Document[this.props.fieldKey])
+ val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
- </div>
+ </div >
);
}
@@ -163,36 +153,37 @@ export class CollectionSchemaView extends CollectionSubView {
}
@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);
- }
-
- });
+ toggleKey = (key: string) => {
+ let list = Cast(this.props.Document.schemaColumns, listSpec("string"));
+ if (list === undefined) {
+ this.props.Document.schemaColumns = list = new List<string>([key]);
+ } else {
+ const index = list.indexOf(key);
+ if (index === -1) {
+ list.push(key);
+ } else {
+ list.splice(index, 1);
+ }
+ }
}
//toggles preview side-panel of schema
@action
toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0);
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)));
+ this.props.Document.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);
if (this._startSplitPercent === this.splitPercentage) {
- this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0);
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
}
onDividerDown = (e: React.PointerEvent) => {
@@ -205,8 +196,7 @@ export class CollectionSchemaView extends CollectionSubView {
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected())
- e.stopPropagation();
+ if (this.props.isSelected()) e.stopPropagation();
else e.preventDefault();
}
}
@@ -219,7 +209,7 @@ export class CollectionSchemaView extends CollectionSubView {
@action
addColumn = () => {
- this.columns.push(new Key(this._newKeyName));
+ this.columns.push(this._newKeyName);
this._newKeyName = "";
}
@@ -234,21 +224,22 @@ export class CollectionSchemaView extends CollectionSubView {
this.previewScript = e.currentTarget.value;
}
- 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;
- return selected ? (this.previewScript ? selected.Get(new Key(this.previewScript)) as Document : selected) : undefined;
+ get previewDocument(): Doc | undefined {
+ const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
+ return selected ? (this.previewScript ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
}
- get tableWidth() { return this.props.PanelWidth() * (1 - this.splitPercentage / 100); }
- get previewRegionHeight() { return this.props.PanelHeight(); }
- get previewRegionWidth() { return this.props.PanelWidth() * this.splitPercentage / 100 - this.DIVIDER_WIDTH; }
+ 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 previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth);
+ private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight);
private previewContentScaling = () => {
let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth);
- if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight)
+ if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
+ }
return wscale;
}
private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
@@ -256,23 +247,26 @@ export class CollectionSchemaView extends CollectionSubView {
get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
- - this.borderWidth).scale(1 / this.previewContentScaling());
+ - this.borderWidth).scale(1 / this.previewContentScaling())
@computed
get previewPanel() {
// let doc = CompileScript(this.previewScript, { this: selected }, true)();
- return !this.previewDocument ? (null) : (
- <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}>
+ const previewDoc = this.previewDocument;
+ return !previewDoc ? (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}
+ <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
+ toggleMinimized={emptyFunction}
addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
ScreenToLocalTransform={this.getPreviewTransform}
ContentScaling={this.previewContentScaling}
PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight}
ContainingCollectionView={this.props.CollectionView}
- focus={emptyDocFunction}
+ focus={emptyFunction}
parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
/>
</div>
<input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
@@ -282,24 +276,25 @@ export class CollectionSchemaView extends CollectionSubView {
}
get documentKeysCheckList() {
- const docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
- let keys: { [id: string]: boolean } = {};
+ const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ let keys: { [key: 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))));
+ //TODO Types
+ untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
- this.columns.forEach(key => keys[key.Id] = true);
+ this.columns.forEach(key => keys[key] = true);
return Array.from(Object.keys(keys)).map(item =>
- (<KeyToggle checked={keys[item]} key={item} keyId={item} toggle={this.toggleKey} />));
+ (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />));
}
get tableOptionsPanel() {
return !this.props.active() ? (null) :
(<Flyout
- anchorPoint={anchorPoints.LEFT_TOP}
+ anchorPoint={anchorPoints.RIGHT_TOP}
content={<div>
<div id="schema-options-header"><h5><b>Options</b></h5></div>
<div id="options-flyout-div">
@@ -327,19 +322,16 @@ export class CollectionSchemaView extends CollectionSubView {
render() {
library.add(faCog);
library.add(faPlus);
- if (!this.previewDocument)
- return (null);
-
- const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
+ const children = this.children;
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: `calc(100% - ${this.splitPercentage}%)` }}>
+ <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
+ Header: col,
+ accessor: (doc: Doc) => [doc, col],
+ id: col
}))}
column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
getTrProps={this.getTrProps}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5c3b2e586..0b08e150a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,28 +1,28 @@
import { action, runInAction } from "mobx";
-import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
import React = require("react");
-import { KeyStore } from "../../../fields/KeyStore";
-import { FieldWaiting, Opt } from "../../../fields/Field";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DragManager } from "../../util/DragManager";
-import { Documents, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions } from "../../documents/Documents";
import { RouteStore } from "../../../server/RouteStore";
-import { TupleField } from "../../../fields/TupleField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { NumberField } from "../../../fields/NumberField";
-import { 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";
+import { Doc, Opt, FieldResult } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { DocServer } from "../../DocServer";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField";
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;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
}
@@ -31,200 +31,195 @@ export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
}
-export type CursorEntry = TupleField<[string, string], [number, number]>;
-
-export class CollectionSubView extends React.Component<SubCollectionViewProps> {
- private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
+export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
+ class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
}
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ protected CreateDropTarget(ele: HTMLDivElement) {
+ this.createDropTarget(ele);
+ }
+
+ get children() {
+ //TODO tfs: This might not be what we want?
+ //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc)).map(doc => doc as Doc);
}
- }
- protected CreateDropTarget(ele: HTMLDivElement) {
- this.createDropTarget(ele);
- }
- @action
- protected setCursorPosition(position: [number, number]) {
- let ind;
- let doc = this.props.Document;
- let id = CurrentUserUtils.id;
- let email = CurrentUserUtils.email;
- if (id && email) {
- let textInfo: [string, string] = [id, email];
- doc.GetTAsync(KeyStore.Prototype, Document).then(proto => {
+ @action
+ protected async setCursorPosition(position: [number, number]) {
+ let ind;
+ let doc = this.props.Document;
+ let id = CurrentUserUtils.id;
+ let email = CurrentUserUtils.email;
+ let pos = { x: position[0], y: position[1] };
+ if (id && email) {
+ const proto = await doc.proto;
if (!proto) {
return;
}
- proto.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, action((field: ListField<CursorEntry>) => {
- let cursors = field.Data;
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
- } else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
- cursors.push(entry);
- }
- }));
- });
+ let cursors = Cast(proto.cursors, listSpec(CursorField));
+ if (!cursors) {
+ proto.cursors = cursors = new List<CursorField>();
+ }
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
+ cursors[ind].setPosition(pos);
+ } else {
+ let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos });
+ cursors.push(entry);
+ }
+ }
}
- }
- @undoBatch
- @action
- protected drop(e: Event, de: DragManager.DropEvent): boolean {
- if (de.data instanceof DragManager.DocumentDragData) {
- if (de.data.aliasOnDrop || de.data.copyOnDrop) {
- [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key =>
- de.data.draggedDocuments.map((draggedDocument: Document, i: number) =>
- draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocuments[i].SetNumber(key, f.Data) : null)));
- }
- let added = false;
- if (de.data.aliasOnDrop || de.data.copyOnDrop) {
- 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) => {
- let moved = move(d, this.props.Document, this.props.addDocument);
- return moved || added;
- }, false);
- } else {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => {
- let moved = this.props.addDocument(d);
- return moved || added;
- }, false);
+ @undoBatch
+ @action
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.dropAction || de.data.userDropAction) {
+ ["width", "height", "curPage"].map(key =>
+ de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
+ PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
+ }
+ let added = false;
+ if (de.data.dropAction || de.data.userDropAction) {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ } else if (de.data.moveDocument) {
+ const move = de.data.moveDocument;
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = move(d, this.props.Document, this.props.addDocument);
+ return moved || added;
+ }, false);
+ } else {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ }
+ e.stopPropagation();
+ return added;
}
- e.stopPropagation();
- return added;
+ return false;
}
- return false;
- }
- 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;
- }
- if (type.indexOf("video") !== -1) {
- ctor = Documents.VideoDocument;
- }
- if (type.indexOf("audio") !== -1) {
- ctor = Documents.AudioDocument;
- }
- if (type.indexOf("pdf") !== -1) {
- 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('/');
- let id = s[s.length - 1];
- Server.GetField(id).then(field => {
- if (field instanceof Document) {
- let alias = field.CreateAlias();
- alias.SetNumber(KeyStore.X, options.x || 0);
- alias.SetNumber(KeyStore.Y, options.y || 0);
- alias.SetNumber(KeyStore.Width, options.width || 300);
- alias.SetNumber(KeyStore.Height, options.height || options.width || 300);
- this.props.addDocument(alias, false);
- }
- });
- return undefined;
+ protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+ if (type.indexOf("image") !== -1) {
+ ctor = Docs.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.AudioDocument;
}
- ctor = Documents.WebDocument;
- options = { height: options.width, ...options, title: path };
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes('localhost')) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ this.props.addDocument(alias, false);
+ }
+ });
+ return undefined;
+ }
+ ctor = Docs.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
}
- return ctor ? ctor(path, options) : undefined;
- }
- @undoBatch
- @action
- protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
- let html = e.dataTransfer.getData("text/html");
- let text = e.dataTransfer.getData("text/plain");
+ @undoBatch
+ @action
+ protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ let html = e.dataTransfer.getData("text/html");
+ let text = e.dataTransfer.getData("text/plain");
- if (text && text.startsWith("<div")) {
- return;
- }
- e.stopPropagation();
- e.preventDefault();
-
- if (html && html.indexOf("<img") !== 0 && !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);
- return;
- }
+ if (text && text.startsWith("<div")) {
+ return;
+ }
+ e.stopPropagation();
+ e.preventDefault();
- let batch = UndoManager.StartBatch("collection view drop");
- let promises: Promise<void>[] = [];
- // tslint:disable-next-line:prefer-for-of
- for (let i = 0; i < e.dataTransfer.items.length; i++) {
- const upload = window.location.origin + RouteStore.upload;
- let item = e.dataTransfer.items[i];
- if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
- let str: string;
- let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s)))))
- .then(result => {
- let type = result.headers["content-type"];
- if (type) {
- this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
- .then(doc => doc && this.props.addDocument(doc, false));
- }
- });
- promises.push(prom);
+ if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
+ let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ this.props.addDocument(htmlDoc, false);
+ return;
}
- let type = item.type;
- if (item.kind === "file") {
- let file = item.getAsFile();
- let formData = new FormData();
- if (file) {
- formData.append('file', file);
- }
- let dropFileName = file ? file.name : "-empty-";
-
- let prom = fetch(upload, {
- method: 'POST',
- body: formData
- }).then(async (res: Response) => {
- (await res.json()).map(action((file: any) => {
- let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName });
-
- docPromise.then(action((doc?: Document) => {
- 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);
- }
+ let batch = UndoManager.StartBatch("collection view drop");
+ let promises: Promise<void>[] = [];
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ const upload = window.location.origin + RouteStore.upload;
+ let item = e.dataTransfer.items[i];
+ if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
+ let str: string;
+ let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
+ .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(result => {
+ let type = result["content-type"];
+ if (type) {
+ this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ .then(doc => doc && this.props.addDocument(doc, false));
}
+ });
+ promises.push(prom);
+ }
+ let type = item.type;
+ if (item.kind === "file") {
+ let file = item.getAsFile();
+ let formData = new FormData();
+
+ if (file) {
+ formData.append('file', file);
+ }
+ let dropFileName = file ? file.name : "-empty-";
+
+ let prom = fetch(upload, {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let path = window.location.origin + file;
+ let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName });
+
+ docPromise.then(doc => doc && this.props.addDocument(doc));
}));
- }));
- });
- promises.push(prom);
+ });
+ promises.push(prom);
+ }
}
- }
- if (promises.length) {
- Promise.all(promises).finally(() => batch.end());
- } else {
- batch.end();
+ if (promises.length) {
+ Promise.all(promises).finally(() => batch.end());
+ } else {
+ batch.end();
+ }
}
}
+ return CollectionSubView;
}
+
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 8ecc5b67b..411d67ff7 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -23,37 +23,37 @@
margin: 5px 0;
}
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
.no-indent {
padding-left: 0;
}
.bullet {
- width: 1.5em;
- display: inline-block;
+ float:left;
+ position: relative;
+ width: 15px;
+ display: block;
color: $intermediate-color;
- }
-
- .coll-title {
- font-size: 24px;
- margin-bottom: 20px;
+ margin-top: 3px;
+ transform: scale(1.3,1.3);
}
.docContainer {
- display: inline-table;
+ margin-left: 10px;
+ display: block;
+ // width:100%;//width: max-content;
}
-
.docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
+ .treeViewItem-openRight {
+ display:inline;
}
}
+
+ .editableView-container {
+ font-weight: bold;
+ }
+
.delete-button {
color: $intermediate-color;
// float: right;
@@ -61,4 +61,28 @@
// margin-top: 3px;
display: inline;
}
+ .treeViewItem-openRight {
+ margin-left: 5px;
+ display:none;
+ }
+ .docContainer:hover {
+ .delete-button {
+ display: inline;
+ // width: auto;
+ }
+ }
+
+ .coll-title {
+ width:max-content;
+ display: block;
+ font-size: 24px;
+ }
+ .collection-child {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ }
+ .collectionTreeView-keyHeader {
+ font-style: italic;
+ font-size: 8pt;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 51a02fc25..33787f06b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,27 +1,33 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { SetupDrag, DragManager } from "../../util/DragManager";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
import { EditableView } from "../EditableView";
-import "./CollectionTreeView.scss";
-import { CollectionView } from "./CollectionView";
-import * as globalCssVariables from "../../views/globalCssVariables.scss";
import { CollectionSubView } from "./CollectionSubView";
+import "./CollectionTreeView.scss";
import React = require("react");
-import { props } from 'bluebird';
+import { Document, listSpec } from '../../../new_fields/Schema';
+import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/RefField';
+import { ContextMenu } from '../ContextMenu';
+import { undoBatch } from '../../util/UndoManager';
+import { Main } from '../Main';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { CollectionDockingView } from './CollectionDockingView';
+import { DocumentManager } from '../../util/DocumentManager';
+import { Utils } from '../../../Utils';
+import { List } from '../../../new_fields/List';
+import { indexOf } from 'typescript-collections/dist/lib/arrays';
export interface TreeViewProps {
- document: Document;
- deleteDoc: (doc: Document) => void;
+ document: Doc;
+ deleteDoc: (doc: Doc) => void;
moveDocument: DragManager.MoveFunction;
- copyOnDrag: boolean;
+ dropAction: "alias" | "copy" | undefined;
}
export enum BulletType {
@@ -31,6 +37,7 @@ export enum BulletType {
}
library.add(faTrashAlt);
+library.add(faAngleRight);
library.add(faCaretDown);
library.add(faCaretRight);
@@ -42,13 +49,29 @@ class TreeView extends React.Component<TreeViewProps> {
@observable _collapsed: boolean = true;
- delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+
+ @undoBatch openRight = async () => {
+ if (this.props.document.dockingConfig) {
+ Main.Instance.openWorkspace(this.props.document);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(this.props.document);
+ }
+ };
+
+ get children() {
+ return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
+ }
+
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ }
@action
- remove = (document: Document) => {
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ remove = (document: Document, key: string) => {
+ let children = Cast(this.props.document[key], listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
}
}
@@ -58,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}
//TODO This should check if it was removed
- this.remove(document);
+ this.remove(document, "data");
return addDoc(document);
}
@@ -77,83 +100,136 @@ 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.dropAction);
let editableView = (titleString: string) =>
(<EditableView
display={"inline"}
contents={titleString}
height={36}
- GetValue={() => this.props.document.Title}
+ GetValue={() => StrCast(this.props.document.title)}
SetValue={(value: string) => {
- this.props.document.SetText(KeyStore.Title, value);
+ let target = this.props.document.proto ? this.props.document.proto : this.props.document;
+ target.title = value;
return true;
}}
/>);
+ let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []);
+ let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon icon="angle-right" size="lg" />
+ <FontAwesomeIcon icon="angle-right" size="lg" />
+ </div>);
return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown}>
- {editableView(this.props.document.Title)}
- <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>
+ <div className="docContainer" ref={reference} onPointerDown={onItemDown}
+ style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {editableView(StrCast(this.props.document.title))}
+ {openRight}
+ {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
</div >);
}
+ onWorkspaceContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) });
+ if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ }
+ ContextMenu.Instance.addItem({
+ description: "Delete", event: undoBatch(() => {
+ this.props.deleteDoc(this.props.document);
+ })
+ });
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ e.stopPropagation();
+ }
+ }
+
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
+
render() {
let bulletType = BulletType.List;
- let childElements: JSX.Element | undefined = undefined;
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) { // add children for a collection
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- childElements = <ul>
- {children.Data.map(value => <TreeView key={value.Id} document={value} deleteDoc={this.remove} moveDocument={this.move} copyOnDrag={this.props.copyOnDrag} />)}
- </ul >;
- }
- else bulletType = BulletType.Collapsed;
+ let contentElement: (JSX.Element | null)[] = [];
+ let keys = Array.from(Object.keys(this.props.document));
+ if (this.props.document.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.props.document.proto)));
}
- return <div className="treeViewItem-container" >
+ keys.map(key => {
+ let docList = Cast(this.props.document[key], listSpec(Doc));
+ if (docList instanceof List && docList.length && docList[0] instanceof Doc) {
+ if (!this._collapsed) {
+ bulletType = BulletType.Collapsible;
+ contentElement.push(<ul key={key + "more"}>
+ {(key === "data") ? (null) :
+ <span className="collectionTreeView-keyHeader" key={key}>{key}</span>}
+ {TreeView.GetChildElements(docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)}
+ </ul >);
+ } else
+ bulletType = BulletType.Collapsed;
+ }
+ });
+ return <div className="treeViewItem-container"
+ onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
{this.renderBullet(bulletType)}
{this.renderTitle()}
- {childElements ? childElements : (null)}
+ {contentElement}
</li>
</div>;
}
+ public static GetChildElements(docs: (Doc | Promise<Doc>)[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
+ return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
+ <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ }
}
@observer
-export class CollectionTreeView extends CollectionSubView {
-
+export class CollectionTreeView extends CollectionSubView(Document) {
@action
remove = (document: Document) => {
- var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ let children = Cast(this.props.Document.data, listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
+ }
+ }
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) });
+ }
+ if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) {
+ ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) });
}
}
-
render() {
- let children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- let copyOnDrag = this.props.Document.GetBoolean(KeyStore.CopyDraggedItems, false);
- let childrenElement = !children || children === FieldWaiting ? (null) :
- (children.Data.map(value =>
- <TreeView document={value} key={value.Id} deleteDoc={this.remove} moveDocument={this.props.moveDocument} copyOnDrag={copyOnDrag} />)
- );
+ const children = this.children;
+ let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
+ if (!children) {
+ return (null);
+ }
+ let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction);
return (
- <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
+ <div id="body" className="collectionTreeView-dropTarget"
+ style={{ borderRadius: "inherit" }}
+ onContextMenu={this.onContextMenu}
+ onWheel={(e: React.WheelEvent) => e.stopPropagation()}
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
<div className="coll-title">
<EditableView
- contents={this.props.Document.Title}
+ contents={this.props.Document.title}
display={"inline"}
height={72}
- GetValue={() => this.props.Document.Title}
+ GetValue={() => StrCast(this.props.Document.title)}
SetValue={(value: string) => {
- this.props.Document.SetText(KeyStore.Title, value);
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = value;
return true;
}} />
</div>
- <hr />
<ul className="no-indent">
- {childrenElement}
+ {childElements}
</ul>
</div >
);
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 29fb342c6..cb3fd1ba4 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,6 +1,5 @@
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
import React = require("react");
@@ -8,17 +7,18 @@ import "./CollectionVideoView.scss";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import { emptyFunction } from "../../../Utils";
+import { Id } from "../../../new_fields/RefField";
+import { VideoBox } from "../nodes/VideoBox";
@observer
export class CollectionVideoView extends React.Component<FieldViewProps> {
- private _intervalTimer: any = undefined;
- private _player: HTMLVideoElement | undefined = undefined;
+ private _videoBox: VideoBox | undefined = undefined;
+ @observable _playTimer?: NodeJS.Timeout = undefined;
@observable _currentTimecode: number = 0;
- @observable _isPlaying: boolean = false;
- public static LayoutString(fieldKey: string = "DataKey") {
+ public static LayoutString(fieldKey: string = "data") {
return FieldView.LayoutString(CollectionVideoView, fieldKey);
}
private get uIButtons() {
@@ -29,7 +29,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
<span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span>
</div>,
<div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
- {this._isPlaying ? "\"" : ">"}
+ {this._playTimer ? "\"" : ">"}
</div>,
<div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
F
@@ -38,53 +38,36 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
}
@action
- mainCont = (ele: HTMLDivElement | null) => {
- if (ele) {
- this._player = ele.getElementsByTagName("video")[0];
- if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) {
- this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- }
+ updateTimecode = () => {
+ if (this._videoBox && this._videoBox.player) {
+ this._currentTimecode = this._videoBox.player.currentTime;
+ this.props.Document.curPage = Math.round(this._currentTimecode);
}
}
- componentDidMount() {
- this._intervalTimer = setInterval(this.updateTimecode, 1000);
- }
+ componentDidMount() { this.updateTimecode(); }
- componentWillUnmount() {
- clearInterval(this._intervalTimer);
- }
-
- @action
- updateTimecode = () => {
- if (this._player) {
- if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero !== -1) {
- this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero;
- (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1;
- } else {
- this._currentTimecode = this._player.currentTime;
- this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this._currentTimecode));
- }
- }
- }
+ componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); }
@action
onPlayDown = () => {
- if (this._player) {
- if (this._player.paused) {
- this._player.play();
- this._isPlaying = true;
+ if (this._videoBox && this._videoBox.player) {
+ if (this._videoBox.player.paused) {
+ this._videoBox.player.play();
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000);
} else {
- this._player.pause();
- this._isPlaying = false;
+ this._videoBox.player.pause();
+ if (this._playTimer) clearInterval(this._playTimer);
+ this._playTimer = undefined;
+
}
}
}
@action
onFullDown = (e: React.PointerEvent) => {
- if (this._player) {
- this._player.requestFullscreen();
+ if (this._videoBox && this._videoBox.player) {
+ this._videoBox.player.requestFullscreen();
e.stopPropagation();
e.preventDefault();
}
@@ -92,33 +75,34 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {
@action
onResetDown = () => {
- if (this._player) {
- this._player.pause();
- this._player.currentTime = 0;
+ if (this._videoBox && this._videoBox.player) {
+ this._videoBox.player.pause();
+ this._videoBox.player.currentTime = 0;
+ if (this._playTimer) clearInterval(this._playTimer);
+ this._playTimer = undefined;
+ this.updateTimecode();
}
-
}
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
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
ContextMenu.Instance.addItem({ description: "VideoOptions", event: emptyFunction });
}
}
+ setVideoBox = (player: VideoBox) => { this._videoBox = player; };
+
private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
- return (
- <>
- <CollectionFreeFormView {...props} CollectionView={this} />
- {this.props.isSelected() ? this.uIButtons : (null)}
- </>
- );
+ return (<>
+ <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} />
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </>);
}
render() {
- trace();
return (
- <CollectionBaseView {...this.props} className="collectionVideoView-cont" contentRef={this.mainCont} onContextMenu={this.onContextMenu}>
+ <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
{this.subView}
</CollectionBaseView>);
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 675e720e2..8c1442d38 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -7,14 +7,15 @@ import { CollectionDockingView } from './CollectionDockingView';
import { CollectionTreeView } from './CollectionTreeView';
import { ContextMenu } from '../ContextMenu';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { KeyStore } from '../../../fields/KeyStore';
import { observer } from 'mobx-react';
import { undoBatch } from '../../util/UndoManager';
import { trace } from 'mobx';
+import { Id } from '../../../new_fields/RefField';
+import { Main } from '../Main';
@observer
export class CollectionView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(CollectionView, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
@@ -29,13 +30,13 @@ export class CollectionView extends React.Component<FieldViewProps> {
return (null);
}
- get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+ get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's?
onContextMenu = (e: React.MouseEvent): void => {
- if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document.Id !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.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)) });
+ if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform) });
+ ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema) });
+ ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree) });
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 3b2f79be1..737ffba7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,6 +1,12 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- stroke-width: 3;
transform: translate(10000px,10000px);
+ opacity: 0.5;
pointer-events: all;
-} \ No newline at end of file
+}
+.collectionfreeformlinkview-linkCircle {
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
+ transform: translate(10000px,10000px);
+ pointer-events: all;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 8868f7df0..63d2f7642 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,37 +1,58 @@
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
import { Utils } from "../../../../Utils";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
+import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { InkingControl } from "../../InkingControl";
export interface CollectionFreeFormLinkViewProps {
- A: Document;
- B: Document;
- LinkDocs: Document[];
+ A: Doc;
+ B: Doc;
+ LinkDocs: Doc[];
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => 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 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
+ this.props.LinkDocs.map(l => {
+ let width = l[WidthSym]();
+ l.x = (x1 + x2) / 2 - width / 2;
+ l.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.Width() / 2);
- let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.Height() / 2);
- let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.Width() / 2);
- let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.Height() / 2);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
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 / 2}` }}
+ x1={`${x1}`} y1={`${y1}`}
+ x2={`${x2}`} y2={`${y2}`} />
+ <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} />
+ </>
);
}
} \ 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 cd74d3a84..1d4584cfe 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,8 +1,5 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { ListField } from "../../../../fields/ListField";
import { Utils } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
@@ -10,57 +7,68 @@ import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
+import { Doc, DocListCast } from "../../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+import { List } from "../../../../new_fields/List";
+import { Id } from "../../../../new_fields/RefField";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)),
+ this._brushReactionDisposer = reaction(
() => {
- let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);
- for (let i = 0; i < views.length; i++) {
- for (let j = 0; j < views.length; j++) {
- 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);
- let x2w = dstDoc.GetNumber(KeyStore.Width, -1);
- if (x1w < 0 || x2w < 0 || i === j) {
- continue;
- }
+ let doclist = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ return { doclist: doclist ? doclist : [], xs: doclist instanceof List ? doclist.map(d => d instanceof Doc && d.x) : [] };
+ },
+ async () => {
+ let doclist = await DocListCast(this.props.Document[this.props.fieldKey]);
+ let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ views.forEach((dstDoc, i) => {
+ views.forEach((srcDoc, j) => {
let dstTarg = dstDoc;
let srcTarg = srcDoc;
- let findBrush = (field: ListField<Document>) => field.Data.findIndex(brush => {
- let bdocs = brush ? brush.GetList(KeyStore.BrushingDocs, [] as Document[]) : [];
- return (bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false);
- });
- let brushAction = (field: ListField<Document>) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- field.Data.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Document = new Document();
- linkDoc.SetText(KeyStore.Title, "Histogram Brush");
- linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);
- linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField);
-
- brushAction = (field: ListField<Document>) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- (findBrush(field) === -1) && field.Data.push(linkDoc);
+ let x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1);
+ let x2w = NumCast(dstDoc.width, -1);
+ if (x1w < 0 || x2w < 0 || i === j) { }
+ else {
+ let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ });
+ let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ let found = findBrush(field);
+ if (found !== -1) {
+ console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.splice(found, 1);
}
};
- }
- dstTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
- srcTarg.GetOrCreateAsync(KeyStore.BrushingDocs, ListField, brushAction);
+ if (Math.abs(x1 + x1w - x2) < 20) {
+ let linkDoc: Doc = new Doc();
+ linkDoc.title = "Histogram Brush";
+ linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
- }
- }
+ brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ if (findBrush(field) === -1) {
+ console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.push(linkDoc);
+ }
+ };
+ }
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
+ else brushAction(dstBrushDocs);
+ if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
+ else brushAction(srcBrushDocs);
+ }
+ })
+ })
});
}
componentWillUnmount() {
@@ -70,9 +78,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
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()!);
+ let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
+ if (containerDoc) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
+ }
+ if (view.props.ContainingCollectionView) {
+ let collid = view.props.ContainingCollectionView.props.Document[Id];
+ Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc).
+ 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);
}
@@ -82,12 +98,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
let srcViews = this.documentAnchors(connection.a);
let targetViews = this.documentAnchors(connection.b);
- let possiblePairs: { a: Document, b: Document, }[] = [];
+ let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
possiblePairs.map(possiblePair =>
drawnPairs.reduce((found, drawnPair) => {
let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b);
- if (match && !drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) {
+ if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
return match || found;
@@ -96,8 +112,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
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} />);
+ }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
+ return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index cf0a6de00..40ec8a325 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,27 +1,33 @@
-import { computed } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { CollectionViewProps, CursorEntry } from "../CollectionSubView";
+import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import CursorField from "../../../../new_fields/CursorField";
+import { Cast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
- protected getCursors(): CursorEntry[] {
+
+ protected getCursors(): CursorField[] {
let doc = this.props.Document;
+
let id = CurrentUserUtils.id;
- let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]);
- let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
- return id ? notMe : [];
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id);
}
private crosshairs?: HTMLCanvasElement;
drawCrosshairs = (backgroundColor: string) => {
if (this.crosshairs) {
- let c = this.crosshairs;
- let ctx = c.getContext('2d');
+ let ctx = this.crosshairs.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 20, 20);
@@ -50,29 +56,26 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
}
}
}
- @computed
+
get sharedCursors() {
- return this.getCursors().map(entry => {
- if (entry.Data.length > 0) {
- let id = entry.Data[0][0];
- let email = entry.Data[0][1];
- let point = entry.Data[1];
- this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22");
- return (
- <div key={id} className="collectionFreeFormRemoteCursors-cont"
- style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }}
- >
- <canvas className="collectionFreeFormRemoteCursors-canvas"
- ref={(el) => { if (el) this.crosshairs = el; }}
- width={20}
- height={20}
- />
- <p className="collectionFreeFormRemoteCursors-symbol">
- {email[0].toUpperCase()}
- </p>
- </div>
- );
- }
+ return this.getCursors().map(c => {
+ let m = c.data.metadata;
+ let l = c.data.position;
+ this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
+ return (
+ <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => { if (el) this.crosshairs = el; }}
+ width={20}
+ height={20}
+ />
+ <p className="collectionFreeFormRemoteCursors-symbol">
+ {m.identifier[0].toUpperCase()}
+ </p>
+ </div>
+ );
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 57706b51e..063c9e2cf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -1,90 +1,107 @@
@import "../../globalCssVariables";
-.collectionfreeformview {
- position: inherit;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- transform-origin: left top;
+
+.collectionfreeformview-ease {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+ transition: transform 1s;
}
-.collectionfreeformview-container {
- .collectionfreeformview > .jsx-parser {
+
+.collectionfreeformview-none {
position: inherit;
- height: 100%;
+ top: 0;
+ left: 0;
width: 100%;
- }
+ height: 100%;
+ transform-origin: left top;
+}
- //nested freeform views
- // .collectionfreeformview-container {
+.collectionfreeformview-container {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ width: 100%;
+ }
+
+ //nested freeform views
+ // .collectionfreeformview-container {
// background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
// linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
// background-size: 30px 30px;
- // }
-
- border-width: $COLLECTION_BORDER_WIDTH;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
- border-color: $light-color-secondary;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: absolute;
- overflow: hidden;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+ // }
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border: 0px solid $light-color-secondary;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: absolute;
+ .marqueeView {
+ overflow: hidden;
+ }
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
}
+
+
.collectionfreeformview-overlay {
- .collectionfreeformview > .jsx-parser {
- position: inherit;
- height: 100%;
- }
- .formattedTextBox-cont {
- background: $light-color-secondary;
- overflow: visible;
- }
-
- opacity: 0.99;
- border-width: 0;
- border-color: transparent;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: absolute;
- overflow: hidden;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- .collectionfreeformview {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ }
+
.formattedTextBox-cont {
- background:yellow;
+ background: $light-color-secondary;
+ overflow: visible;
+ }
+
+ opacity: 0.99;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position:absolute;
+ .marqueeView {
+ overflow: hidden;
+ }
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ .collectionfreeformview {
+ .formattedTextBox-cont {
+ background: yellow;
+ }
}
- }
}
// selection border...?
.border {
- border-style: solid;
- box-sizing: border-box;
- width: 98%;
- height: 98%;
- border-radius: $border-radius;
+ border-style: solid;
+ box-sizing: border-box;
+ width: 98%;
+ height: 98%;
+ border-radius: $border-radius;
}
//this is an animation for the blinking cursor!
@keyframes blink {
- 0% {
- opacity: 0;
- }
- 49% {
- opacity: 0;
- }
- 50% {
- opacity: 1;
- }
+ 0% {
+ opacity: 0;
+ }
+
+ 49% {
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
}
#prevCursor {
- animation: blink 1s infinite;
-}
+ animation: blink 1s infinite;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 228fc937e..6861ce58b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,8 +1,5 @@
-import { action, computed, observable, trace } from "mobx";
+import { action, computed, trace } from "mobx";
import { observer } from "mobx-react";
-import Measure from "react-measure";
-import { Document } from "../../../../fields/Document";
-import { KeyStore } from "../../../../fields/KeyStore";
import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
@@ -11,10 +8,9 @@ import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
import { InkingCanvas } from "../../InkingCanvas";
-import { MainOverlayTextBox } from "../../MainOverlayTextBox";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps } from "../../nodes/DocumentView";
+import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
@@ -22,72 +18,86 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import v5 = require("uuid/v5");
+import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types";
+import { pageSchema } from "../../nodes/ImageBox";
+import { Id } from "../../../../new_fields/RefField";
+
+export const panZoomSchema = createSchema({
+ panX: "number",
+ panY: "number",
+ scale: "number"
+});
+
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
@observer
-export class CollectionFreeFormView extends CollectionSubView {
+export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
+ public static RIGHT_BTN_DRAG = false;
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
private _lastX: number = 0;
private _lastY: number = 0;
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+ @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
+ @computed get nativeHeight() { return FieldValue(this.Document.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 childViews = () => this.views;
- 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 get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ private panX = () => FieldValue(this.Document.panX, 0);
+ private panY = () => FieldValue(this.Document.panY, 0);
+ private zoomScaling = () => FieldValue(this.Document.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
+ private addLiveTextBox = (newBox: Doc) => {
+ 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);
}
- private addDocument = (newBox: Document, allowDuplicates: boolean) => {
- if (this.isAnnotationOverlay) {
- newBox.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1));
- }
- return this.props.addDocument(this.bringToFront(newBox), false);
+ private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
+ this.props.addDocument(newBox, false);
+ this.bringToFront(newBox);
+ return true;
}
- private selectDocuments = (docs: Document[]) => {
+ private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll;
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) => {
- var page = doc.GetNumber(KeyStore.Page, -1);
- if (page === curPage || page === -1) {
- active.push(doc);
- }
- return active;
- }, [] as Document[]);
+ const curPage = FieldValue(this.Document.curPage, -1);
+ return this.children.filter(doc => {
+ var page = NumCast(doc.page, -1);
+ return page === curPage || page === -1;
+ });
}
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) {
- const [x, y] = this.getTransform().transformPoint(de.x - de.data.xOffset, de.y - de.data.yOffset);
if (de.data.droppedDocuments.length) {
- let dropX = de.data.droppedDocuments[0].GetNumber(KeyStore.X, 0);
- let dropY = de.data.droppedDocuments[0].GetNumber(KeyStore.Y, 0);
+ let dragDoc = de.data.droppedDocuments[0];
+ let zoom = NumCast(dragDoc.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 = NumCast(de.data.droppedDocuments[0].x);
+ let dropY = NumCast(de.data.droppedDocuments[0].y);
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);
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.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);
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
}
this.bringToFront(d);
});
@@ -99,51 +109,43 @@ export class CollectionFreeFormView extends CollectionSubView {
}
@action
- cleanupInteractions = () => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- @action
onPointerDown = (e: React.PointerEvent): void => {
- 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 (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || (e.button === 0 && e.altKey)) && (childSelected || this.props.active())) {
+ if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
+ (e.button === 0 && e.altKey)) && this.props.active())) ||
+ (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) {
document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
this._lastX = e.pageX;
this._lastY = e.pageY;
}
}
- @action
onPointerUp = (e: PointerEvent): void => {
- e.stopPropagation();
-
- this.cleanupInteractions();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
}
@action
onPointerMove = (e: PointerEvent): void => {
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 x = Cast(this.props.Document.panX, "number", 0);
+ let y = Cast(this.props.Document.panY, "number", 0);
+ let docs = this.children || [];
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 minx = docs.length ? Cast(docs[0].x, "number", 0) : 0;
+ let maxx = docs.length ? Cast(docs[0].width, "number", 0) + minx : minx;
+ let miny = docs.length ? Cast(docs[0].y, "number", 0) : 0;
+ let maxy = docs.length ? Cast(docs[0].height, "number", 0) + 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);
+ let x = Cast(doc.x, "number", 0);
+ let xe = x + Cast(doc.width, "number", 0);
+ let y = Cast(doc.y, "number", 0);
+ let ye = y + Cast(doc.height, "number", 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]]);
@@ -157,7 +159,7 @@ export class CollectionFreeFormView extends CollectionSubView {
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
this._lastY = e.pageY;
- e.stopPropagation();
+ 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();
}
}
@@ -167,10 +169,10 @@ export class CollectionFreeFormView extends CollectionSubView {
// if (!this.props.active()) {
// return;
// }
- let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => {
+ let childSelected = this.children.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
- return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false);
- }, false);
+ return dv && SelectionManager.IsSelected(dv) ? true : false;
+ });
if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
return;
}
@@ -179,8 +181,8 @@ export class CollectionFreeFormView extends CollectionSubView {
if (e.ctrlKey) {
let deltaScale = (1 - (e.deltaY / coefficient));
- this.props.Document.SetNumber(KeyStore.NativeWidth, this.nativeWidth * deltaScale);
- this.props.Document.SetNumber(KeyStore.NativeHeight, this.nativeHeight * deltaScale);
+ this.props.Document.nativeWidth = this.nativeWidth * deltaScale;
+ this.props.Document.nativeHeight = this.nativeHeight * deltaScale;
e.stopPropagation();
e.preventDefault();
} else {
@@ -190,23 +192,24 @@ export class CollectionFreeFormView extends CollectionSubView {
if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
deltaScale = 1 / this.zoomScaling();
}
+ 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.scale = Math.abs(safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
e.stopPropagation();
}
}
@action
setPan(panX: number, panY: number) {
- MainOverlayTextBox.Instance.SetTextDoc();
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);
+ this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
}
@action
@@ -218,49 +221,61 @@ export class CollectionFreeFormView extends CollectionSubView {
onDragOver = (): void => {
}
- @action
- bringToFront(doc: Document) {
- this.props.Document.GetList(this.props.fieldKey, [] as Document[]).slice().sort((doc1, doc2) => {
+ bringToFront = (doc: Doc) => {
+ const docs = this.children;
+ docs.slice().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));
- return doc;
+ return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
+ }).forEach((doc, index) => doc.zIndex = index + 1);
+ doc.zIndex = docs.length + 1;
}
- focusDocument = (doc: Document) => {
+ focusDocument = (doc: Doc) => {
+ SelectionManager.DeselectAll();
+ this.props.Document.panTransformType = "Ease";
this.setPan(
- doc.GetNumber(KeyStore.X, 0) + doc.Width() / 2,
- doc.GetNumber(KeyStore.Y, 0) + doc.Height() / 2);
+ NumCast(doc.x) + NumCast(doc.width) / 2,
+ NumCast(doc.y) + NumCast(doc.height) / 2);
this.props.focus(this.props.Document);
+ if (this.props.Document.panTransformType === "Ease") {
+ setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false
+ }
}
- getDocumentViewProps(document: Document): DocumentViewProps {
+
+ getDocumentViewProps(document: Doc): DocumentViewProps {
return {
Document: document,
+ toggleMinimized: emptyFunction,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
ScreenToLocalTransform: this.getTransform,
isTopMost: false,
- selectOnLoad: document.Id === this._selectOnLoaded,
- PanelWidth: document.Width,
- PanelHeight: document.Height,
+ selectOnLoad: document[Id] === this._selectOnLoaded,
+ PanelWidth: document[WidthSym],
+ PanelHeight: document[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
parentActive: this.props.active,
- whenActiveChanged: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
};
}
- @computed
+ @computed.struct
get views() {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
- let docviews = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {
- var page = doc.GetNumber(KeyStore.Page, -1);
+ let curPage = FieldValue(this.Document.curPage, -1);
+ let docviews = this.children.reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
+ var page = NumCast(doc.page, -1);
if (page === curPage || page === -1) {
- prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+ let minim = Cast(doc.isMinimized, "boolean");
+ if (minim === undefined || !minim) {
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ }
}
return prev;
}, [] as JSX.Element[]);
@@ -275,17 +290,20 @@ export class CollectionFreeFormView extends CollectionSubView {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
+ 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.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} >
- <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
+ <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}
- zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- <CollectionFreeFormBackgroundView {...this.getDocumentViewProps(this.props.Document)} />
+ 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}
@@ -293,20 +311,18 @@ export class CollectionFreeFormView extends CollectionSubView {
</CollectionFreeFormLinksView>
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} />
</MarqueeView>
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</div>
);
}
}
@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@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} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -314,12 +330,10 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
}
@observer
-class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps> {
+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={returnFalse} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.backgroundView;
@@ -332,17 +346,19 @@ interface CollectionFreeFormViewPannableContentsProps {
panX: () => number;
panY: () => number;
zoomScaling: () => number;
+ easing: () => boolean;
}
@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="collectionfreeformview" style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
+ 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>;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index ae0a9fd48..6e8ec8662 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: C (collection), or Delete"
+ content: "Press: c (collection), s (summary), r (replace) 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 65f461b27..1bf39e335 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,28 +1,30 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../../fields/Document";
-import { FieldWaiting } from "../../../../fields/Field";
-import { InkField, StrokeData } from "../../../../fields/InkField";
-import { KeyStore } from "../../../../fields/KeyStore";
-import { Documents } from "../../../documents/Documents";
+import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import { Utils } from "../../../../Utils";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, Cast } from "../../../../new_fields/Types";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
getTransform: () => Transform;
container: CollectionFreeFormView;
- addDocument: (doc: Document, allowDuplicates: false) => boolean;
- activeDocuments: () => Document[];
- selectDocuments: (docs: Document[]) => void;
- removeDocument: (doc: Document) => boolean;
- addLiveTextDocument: (doc: Document) => void;
+ addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ activeDocuments: () => Doc[];
+ selectDocuments: (docs: Doc[]) => void;
+ removeDocument: (doc: Doc) => boolean;
+ addLiveTextDocument: (doc: Doc) => void;
+ isSelected: () => boolean;
}
@observer
@@ -32,52 +34,73 @@ 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;
- _showOnUp: 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;
}
+ @undoBatch
@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._downX, this._downY);
- let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" });
+ //make textbox and add it to this collection
+ let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
+ if (e.key === "q" && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ let text = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t != "\r");
+ for (let i = 0; i < ns.length - 1; i++) {
+ if (ns[i].trim() === "") {
+ ns.splice(i, 1);
+ continue;
+ }
+ while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+ ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
+ ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+ ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+ let sub = ns[i].endsWith("\r") ? 1 : 0;
+ let br = ns[i + 1].trim() === "";
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+ if (br) break;
+ }
+ }
+ ns.map(line => {
+ let indent = line.search(/\S|$/);
+ let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ this.props.addDocument(newBox, false);
+ y += 40 * this.props.getTransform().Scale;
+ })
+ })();
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
this.props.addLiveTextDocument(newBox);
- PreviewCursor.Visible = false;
- e.stopPropagation();
}
- }
- hideCursor = () => {
- document.removeEventListener("keypress", this.onKeyPress, false);
+ 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._showOnUp = true;
- document.removeEventListener("keypress", this.onKeyPress, 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);
+ // bcz: do we need this? it kills the context menu on the main collection
+ // e.stopPropagation();
+ }
+ if (e.altKey) {
+ e.preventDefault();
}
}
@@ -86,33 +109,45 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._lastX = e.pageX;
this._lastY = e.pageY;
if (!e.cancelBubble) {
- if (Math.abs(this._downX - e.clientX) > 4 || Math.abs(this._downY - e.clientY) > 4) {
- this._showOnUp = false;
- PreviewCursor.Visible = false;
- }
- 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();
+ }
+ if (e.altKey) {
e.preventDefault();
}
}
@action
onPointerUp = (e: PointerEvent): void => {
- this.cleanupInteractions(true);
- this._visible = false;
- if (this._showOnUp) {
- PreviewCursor.Show(this.hideCursor, this._downX, this._downY);
- document.addEventListener("keypress", this.onKeyPress, false);
- } else {
+ 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);
+ 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) {
+ 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();
+ }
}
intersectRect(r1: { left: number, top: number, width: number, height: number },
@@ -132,42 +167,99 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@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);
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ if (ink) {
+ this.marqueeInkDelete(ink.inkData);
}
- this.cleanupInteractions();
+ SelectionManager.DeselectAll();
+ this.cleanupInteractions(false);
+ e.stopPropagation();
}
- if (e.key === "c") {
+ if (e.key === "c" || e.key === "r" || e.key === "s" || e.key === "e" || e.key === "p") {
+ 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);
+ if (e.key === "s") {
+ let dCopy = Doc.MakeCopy(d);
+ dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ dCopy.page = -1;
+ return dCopy;
+ }
+ else if (e.key !== "r") {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ }
return d;
});
- let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
- let inkData = ink && ink !== FieldWaiting ? ink.Data : undefined;
- //setTimeout(() => {
- let newCollection = Documents.FreeformDocument(selected, {
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ let inkData = ink ? ink.inkData : undefined;
+ let zoomBasis = NumCast(this.props.container.props.Document.scale, 1);
+ let newCollection = Docs.FreeformDocument(selected, {
x: bounds.left,
y: bounds.top,
- panx: 0,
- pany: 0,
- width: bounds.width,
- height: bounds.height,
- ink: inkData ? this.marqueeInkSelect(inkData) : undefined,
- title: "a nested collection"
+ panX: 0,
+ panY: 0,
+ borderRounding: e.key === "e" ? -1 : undefined,
+ scale: zoomBasis,
+ width: bounds.width * zoomBasis,
+ height: bounds.height * zoomBasis,
+ ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
+ title: "a nested collection",
});
- this.props.addDocument(newCollection, false);
+
this.marqueeInkDelete(inkData);
- // }, 100);
- this.cleanupInteractions();
- SelectionManager.DeselectAll();
- }
+ // SelectionManager.DeselectAll();
+ if (e.key === "s" || e.key === "r" || e.key === "p") {
+ e.preventDefault();
+ let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+
+ if (e.key === "s" || e.key === "p") {
+ summary.proto!.maximizeOnRight = true;
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ }
+ summary.proto!.summarizedDocs = new List<Doc>(selected);
+ summary.proto!.isButton = true;
+ selected.map(summarizedDoc => {
+ let maxx = NumCast(summarizedDoc.x, undefined);
+ let maxy = NumCast(summarizedDoc.y, undefined);
+ let maxw = NumCast(summarizedDoc.width, undefined);
+ let maxh = NumCast(summarizedDoc.height, undefined);
+ summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0])
+ });
+ this.props.addLiveTextDocument(summary);
+ }
+ else {
+ this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
+ }
+ this.cleanupInteractions(false);
+ } else
+ if (e.key === "s") {
+ // this._commandExecuted = true;
+ // e.stopPropagation();
+ // e.preventDefault();
+ // let bounds = this.Bounds;
+ // let selected = this.marqueeSelect();
+ // SelectionManager.DeselectAll();
+ // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+ // this.props.addLiveTextDocument(summary);
+ // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
+
+ // this.cleanupInteractions(false);
+ }
}
@action
marqueeInkSelect(ink: Map<any, any>) {
@@ -199,19 +291,19 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let idata = new Map();
ink.forEach((value: StrokeData, key: string, map: any) =>
!InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
- this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField);
+ Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
}
}
marqueeSelect() {
let selRect = this.Bounds;
- let selection: Document[] = [];
+ let selection: Doc[] = [];
this.props.activeDocuments().map(doc => {
- var z = doc.GetNumber(KeyStore.Zoom, 1);
- var x = doc.GetNumber(KeyStore.X, 0);
- var y = doc.GetNumber(KeyStore.Y, 0);
- var w = doc.Width() / z;
- var h = doc.Height() / z;
+ var z = NumCast(doc.zoomBasis, 1);
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width) / z;
+ var h = NumCast(doc.height) / z;
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
@@ -229,7 +321,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
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/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 5c8e9c8fc..cb4d1ad87 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -1,7 +1,7 @@
@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);
+$light-color-secondary:#f1efeb;
$main-accent: #61aaa3;
// $alt-accent: #cdd5ec;
// $alt-accent: #cdeceb;
@@ -23,7 +23,11 @@ $mainTextInput-zindex: 999; // then text input overlay so that it's context menu
$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
index e874b815d..9788d31f7 100644
--- a/src/client/views/globalCssVariables.scss.d.ts
+++ b/src/client/views/globalCssVariables.scss.d.ts
@@ -1,7 +1,9 @@
interface IGlobalScss {
contextMenuZindex: string; // context menu shows up over everything
- COLLECTION_BORDER_WIDTH: number;
+ COLLECTION_BORDER_WIDTH: string;
+ MINIMIZED_ICON_SIZE: string;
+ MAX_ROW_HEIGHT: string;
}
declare const globalCssVariables: IGlobalScss;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1493ff25b..be12dced3 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,36 +1,19 @@
import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
import { observer } from "mobx-react";
-import { ContextMenu } from "../../views/ContextMenu";
-import { observable, action } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
-import { AudioField } from "../../../fields/AudioField";
import "./AudioBox.scss";
-import { NumberField } from "../../../fields/NumberField";
+import { Cast } from "../../../new_fields/Types";
+import { AudioField } from "../../../new_fields/URLField";
+const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
export class AudioBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(AudioBox); }
- constructor(props: FieldViewProps) {
- super(props);
- }
-
-
-
- componentDidMount() {
- }
-
- componentWillUnmount() {
- }
-
-
render() {
- let field = this.props.Document.Get(this.props.fieldKey);
- let path = field === FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3" :
- field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3";
+ let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
+ let path = field.url.href;
return (
<div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 8cf7a0dd2..b05f2eea2 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,62 +1,79 @@
-import { computed, trace } from "mobx";
+import { computed, trace, action, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
-import { NumberField } from "../../../fields/NumberField";
import { Transform } from "../../util/Transform";
-import { DocumentView, DocumentViewProps } from "./DocumentView";
+import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
-import { OmitKeys } from "../../../Utils";
+import { DocComponent } from "../DocComponent";
+import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
+import { OmitKeys, Utils } from "../../../Utils";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
+const schema = createSchema({
+ zoomBasis: "number",
+ zIndex: "number"
+});
+
+//TODO Types: The import order is wrong, so positionSchema is undefined
+type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>;
+const FreeformDocument = makeInterface(schema, positionSchema);
+
@observer
-export class CollectionFreeFormDocumentView extends React.Component<CollectionFreeFormDocumentViewProps> {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
private _mainCont = React.createRef<HTMLDivElement>();
+ private _downX: number = 0;
+ private _downY: number = 0;
+ _bringToFrontDisposer?: IReactionDisposer;
- @computed
- get transform(): 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) scale(${this.zoom}, ${this.zoom}) `;
+ @computed get transform() {
+ return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;
}
- @computed get zoom(): number { return 1 / this.props.Document.GetNumber(KeyStore.Zoom, 1); }
- @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 FieldValue(this.Document.x, 0); }
+ @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
+ @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
+ @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
+ @computed get width(): number { return FieldValue(this.Document.width, 0); }
+ @computed get height(): number { return FieldValue(this.Document.height, 0); }
+ @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); }
set width(w: number) {
- this.props.Document.SetData(KeyStore.Width, w, NumberField);
+ this.Document.width = w;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w);
+ this.Document.height = this.nativeHeight / this.nativeWidth * w;
}
}
-
set height(h: number) {
- this.props.Document.SetData(KeyStore.Height, h, NumberField);
+ this.Document.height = h;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h);
+ this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
-
set zIndex(h: number) {
- this.props.Document.SetData(KeyStore.ZIndex, h, NumberField);
+ this.Document.zIndex = h;
}
- getTransform = (): Transform =>
- this.props.ScreenToLocalTransform()
- .translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0))
- .scale(1 / this.contentScaling()).scale(1 / this.zoom)
-
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
- panelWidth = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelWidth();
- panelHeight = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelHeight();
+ panelWidth = () => this.props.PanelWidth();
+ panelHeight = () => this.props.PanelHeight();
+ toggleMinimized = async () => this.toggleIcon(await DocListCast(this.props.Document.maximizedDocs));
+ getTransform = (): Transform => this.props.ScreenToLocalTransform()
+ .translate(-this.X, -this.Y)
+ .scale(1 / this.contentScaling()).scale(1 / this.zoom)
@computed
get docView() {
- return <DocumentView {...OmitKeys(this.props, ['zoomFade'])}
+ return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
+ toggleMinimized={this.toggleMinimized}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
PanelWidth={this.panelWidth}
@@ -64,30 +81,173 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr
/>;
}
+ componentDidMount() {
+ this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => {
+ this.props.bringToFront(this.props.Document);
+ if (values instanceof List) {
+ let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
+ this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false);
+ }
+ }, { fireImmediately: true });
+ }
+
+ componentWillUnmount() {
+ if (this._bringToFrontDisposer) this._bringToFrontDisposer();
+ }
+
+ animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
+ if (first) {
+ if (maximizing) target.width = target.height = 1;
+ }
+ setTimeout(() => {
+ let now = Date.now();
+ let progress = Math.min(1, (now - stime) / 200);
+ let pval = maximizing ?
+ [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
+ [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
+ target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ target.x = pval[0];
+ target.y = pval[1];
+ if (now < stime + 200) {
+ this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing);
+ }
+ else {
+ if (!maximizing) {
+ target.isMinimized = true;
+ target.x = targ[0];
+ target.y = targ[1];
+ target.width = width;
+ target.height = height;
+ }
+ target.isIconAnimating = undefined;
+ }
+ },
+ 2);
+ }
+ @action
+ public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => {
+ SelectionManager.DeselectAll();
+ let isMinimized: boolean | undefined;
+ let minimizedDoc: Doc | undefined = this.props.Document;
+ if (!maximizedDocs) {
+ minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) maximizedDocs = await DocListCast(minimizedDoc.maximizedDocs);
+ }
+ if (minimizedDoc && maximizedDocs) {
+ let minimizedTarget = minimizedDoc;
+ if (!CollectionFreeFormDocumentView._undoBatch) {
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
+ maximizedDocs.forEach(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
+ let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
+ let maxx = NumCast(maximizedDoc.x, undefined);
+ let maxy = NumCast(maximizedDoc.y, undefined);
+ let maxw = NumCast(maximizedDoc.width, undefined);
+ let maxh = NumCast(maximizedDoc.height, undefined);
+ if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
+ maxw !== undefined && maxh !== undefined) {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
+ if (isMinimized) maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
+ }
+ }
+ });
+ setTimeout(() => {
+ CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+ CollectionFreeFormDocumentView._undoBatch = undefined;
+ }, 500);
+ }
+ }
+ static _undoBatch?: UndoManager.Batch = undefined;
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ // e.stopPropagation();
+ }
+ onClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ let altKey = e.altKey;
+ if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ let isBullet = (e.target as any).id === "isBullet";
+ let isIcon = StrCast(this.props.Document.layout).indexOf("IconBox") !== -1;
+ if (BoolCast(this.props.Document.isButton, false) || isBullet) {
+ let maximizedDocs = await DocListCast(isBullet ? this.props.Document.subBulletDocs : isIcon ? this.props.Document.maximizedDocs : this.props.Document.summarizedDocs);
+ if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
+ let dataDocs = await DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ SelectionManager.DeselectAll();
+ maximizedDocs.forEach(maxDoc => {
+ maxDoc.isMinimized = false;
+ if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) {
+ CollectionDockingView.Instance.AddRightSplit(maxDoc);
+ } else {
+ CollectionDockingView.Instance.CloseRightSplit(maxDoc);
+ }
+ });
+ }
+ } else {
+ this.props.addDocument && maximizedDocs.forEach(async maxDoc => this.props.addDocument!(await maxDoc, false));
+ this.toggleIcon(maximizedDocs);
+ }
+ }
+ }
+ }
+ }
+
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
+
+ borderRounding = () => {
+ let br = NumCast(this.props.Document.borderRounding);
+ return br >= 0 ? br :
+ NumCast(this.props.Document.nativeWidth) === 0 ?
+ Math.min(this.props.PanelWidth(), this.props.PanelHeight())
+ : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0);
+ }
+
render() {
+ let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
let zoomFade = 1;
- // //var zoom = doc.GetNumber(KeyStore.Zoom, 1);
- // let transform = (this.props.ScreenToLocalTransform().scale(this.props.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;
- // let fadeUp = .75 * 1800;
- // let fadeDown = .075 * 1800;
- // zoomFade = w < fadeDown || w > fadeUp ? Math.max(0, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 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 = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
+ let fadeUp = .75 * screenWidth;
+ let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
+ zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1;
return (
- <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{
- opacity: zoomFade,
- 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}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter}
+ onClick={this.onClick}
+ style={{
+ outlineColor: "black",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px",
+ 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 76f852601..f404b7bc6 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,9 +1,5 @@
-import { computed } from "mobx";
+import { computed, trace } from "mobx";
import { observer } from "mobx-react";
-import { FieldWaiting, Field } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
@@ -15,56 +11,84 @@ 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";
+import { FieldView } from "./FieldView";
import { WebBox } from "./WebBox";
import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
-import { Document } from "../../../fields/Document";
import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
export interface JsxBindings {
props: BindingProps;
- [keyName: string]: BindingProps | Field;
}
+class ObserverJsxParser1 extends JsxParser {
+ constructor(props: any) {
+ super(props);
+ observer(this as any);
+ }
+}
+
+const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+
@observer
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
- layoutKey: Key
+ layoutKey: string
}> {
- @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "<p>Error loading layout data</p>"); }
- @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
- @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
-
+ @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", this.props.layoutKey === "layout" ? "<p>Error loading layout data</p>" : ""); }
CreateBindings(): JsxBindings {
- let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) };
+ return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+ }
- for (const key of this.layoutKeys) {
- bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
}
- for (const key of this.layoutFields) {
- let field = this.props.Document.Get(key);
- bindings[key.Name] = field && field !== FieldWaiting ? field.GetValue() : field;
+ return new List<string>();
+ }
+ @computed get finalLayout() {
+ const baseLayout = this.layout;
+ let base = baseLayout;
+ let layout = baseLayout;
+
+ // bcz: templates are intended only for a document's primary layout or overlay (not background). However,
+ // a DocumentContentsView is used to render annotation overlays, so we detect that here
+ // by checking the layoutKey. This should probably be moved into
+ // a prop so that the overlay can explicitly turn off templates.
+ if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
+ (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
+ this.templates.forEach(template => {
+ let self = this;
+ function convertConstantsToNative(match: string, offset: number, x: string) {
+ let px = Number(match.replace("px", ""));
+ return `${px * self.props.ScreenToLocalTransform().Scale}px`;
+ }
+ let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
+ layout = nativizedTemplate.replace("{layout}", base);
+ base = layout;
+ });
}
- return bindings;
+ return layout;
}
render() {
- let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
- if (!lkeys || lkeys === FieldWaiting) {
- return <p>Error loading layout keys</p>;
- }
- return <JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
+ return <ObserverJsxParser
+ components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings()}
- jsx={this.layout}
+ jsx={this.finalLayout}
showWarnings={true}
onError={(test: any) => { console.log(test); }}
/>;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 690ee50e8..7c72fb6e6 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,7 +4,7 @@
position: inherit;
top: 0;
left:0;
- background: $light-color; //overflow: hidden;
+ // background: $light-color; //overflow: hidden;
transform-origin: left top;
&.minimized {
@@ -13,7 +13,6 @@
}
.top {
- background: #232323;
height: 20px;
cursor: pointer;
}
@@ -31,18 +30,4 @@
}
.documentView-node-topmost {
background: white;
-}
-
-.minimized-box {
- height: 10px;
- width: 10px;
- border-radius: 2px;
- background: $dark-color;
- transform-origin: left top;
-}
-
-.minimized-box:hover {
- background: $main-accent;
- transform: scale(1.15);
- cursor: pointer;
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d74f9fc57..25a75904b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,17 +1,9 @@
-import { action, computed, runInAction } from "mobx";
+import { action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { BooleanField } from "../../../fields/BooleanField";
-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 { TextField } from "../../../fields/TextField";
-import { ServerUtils } from "../../../server/ServerUtil";
import { emptyFunction, Utils } from "../../../Utils";
-import { Documents } from "../../documents/Documents";
+import { Docs } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
-import { DragManager } from "../../util/DragManager";
+import { DragManager, dropActionType } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -20,14 +12,35 @@ 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 { Opt, Doc, WidthSym, HeightSym, DocListCast } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { DocServer } from "../../DocServer";
+import { Id } from "../../../new_fields/RefField";
+import { PresentationView } from "../PresentationView";
+const linkSchema = createSchema({
+ title: "string",
+ linkDescription: "string",
+ linkTags: "string",
+ linkedTo: Doc,
+ linkedFrom: Doc
+});
+
+type LinkDoc = makeInterface<[typeof linkSchema]>;
+const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- Document: Document;
+ Document: Doc;
addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Document) => boolean;
moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
@@ -40,49 +53,34 @@ export interface DocumentViewProps {
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
-}
-export interface JsxArgs extends DocumentViewProps {
- Keys: { [name: string]: Key };
- Fields: { [name: string]: Field };
+ toggleMinimized: () => void;
+ bringToFront: (doc: Doc) => void;
}
-/*
-This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that
-jsx-to-string can recover the jsx from
-Example usage of this function:
- public static LayoutString() {
- let args = FakeJsxArgs(["Data"]);
- return jsxToString(
- <CollectionFreeFormView
- doc={args.Document}
- fieldKey={args.Keys.Data}
- DocumentViewForField={args.DocumentView} />,
- { useFunctionCode: true, functionNameOnly: true }
- )
- }
-*/
-export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
- let Keys: { [name: string]: any } = {};
- let Fields: { [name: string]: any } = {};
- for (const key of keys) {
- Object.defineProperty(emptyFunction, "name", { value: key + "Key" });
- Keys[key] = emptyFunction;
- }
- for (const field of fields) {
- Object.defineProperty(emptyFunction, "name", { value: field });
- Fields[field] = emptyFunction;
- }
- let args: JsxArgs = {
- Document: function Document() { },
- DocumentView: function DocumentView() { },
- Keys,
- Fields
- } as any;
- return args;
-}
+const schema = createSchema({
+ layout: "string",
+ nativeWidth: "number",
+ nativeHeight: "number",
+ backgroundColor: "string"
+});
+
+export const positionSchema = createSchema({
+ nativeWidth: "number",
+ nativeHeight: "number",
+ width: "number",
+ height: "number",
+ x: "number",
+ y: "number",
+});
+
+export type PositionDocument = makeInterface<[typeof positionSchema]>;
+export const PositionDocument = makeInterface(positionSchema);
+
+type Document = makeInterface<[typeof schema]>;
+const Document = makeInterface(schema);
@observer
-export class DocumentView extends React.Component<DocumentViewProps> {
+export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
private _mainCont = React.createRef<HTMLDivElement>();
@@ -91,43 +89,39 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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>()); }
-
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- if (e.button === 2 && !this.isSelected()) {
- return;
- }
- 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.stopPropagation();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
}
+ return new List<string>();
}
+ set templates(templates: List<string>) { this.props.Document.templates = templates; }
+ screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ _reactionDisposer?: IReactionDisposer;
+ @action
componentDidMount() {
if (this._mainCont.current) {
this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
handlers: { drop: this.drop.bind(this) }
});
}
- runInAction(() => DocumentManager.Instance.DocumentViews.push(this));
+ // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
+ this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+ async () => {
+ let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
+ if (maxDoc && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
+ this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
+ }
+ let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
+ if (sumDoc instanceof Doc) {
+ this.props.Document.title = sumDoc.title + ".expanded";
+ }
+ }, { fireImmediately: true });
+ DocumentManager.Instance.DocumentViews.push(this);
}
-
+ @action
componentDidUpdate() {
if (this._dropDisposer) {
this._dropDisposer();
@@ -138,142 +132,151 @@ export class DocumentView extends React.Component<DocumentViewProps> {
});
}
}
-
+ @action
componentWillUnmount() {
- if (this._dropDisposer) {
- this._dropDisposer();
- }
- runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1));
+ if (this._reactionDisposer) this._reactionDisposer();
+ if (this._dropDisposer) this._dropDisposer();
+ DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
+ }
+
+ stopPropagation = (e: React.SyntheticEvent) => {
+ e.stopPropagation();
}
- startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
- const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- let dragData = new DragManager.DocumentDragData([this.props.Document]);
- dragData.aliasOnDrop = dropAliasOfDraggedDoc;
- dragData.xOffset = x - left;
- dragData.yOffset = y - top;
+ let allConnected = dragSubBullets ? [this.props.Document, ...Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc)] : [this.props.Document];
+ const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
+ let dragData = new DragManager.DocumentDragData(allConnected);
+ const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
+ dragData.dropAction = dropAction;
+ dragData.xOffset = xoff;
+ dragData.yOffset = yoff;
dragData.moveDocument = this.props.moveDocument;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction)
},
- hideSource: !dropAliasOfDraggedDoc
+ hideSource: !dropAction
});
}
}
- 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);
+ }
+ }
+ _hitIsBullet = false;
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) {
return;
}
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+ this._hitIsBullet = (e.target && (e.target as any).id === "isBullet");
+ if (e.shiftKey && e.buttons === 1) {
+ if (this.props.isTopMost) {
+ this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitIsBullet);
+ } else if (this.props.Document) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(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);
+ }
+ }
+ 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 ? "alias" : undefined, this._hitIsBullet);
+ }
}
+ 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) && e.button !== 2 &&
- Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
- SelectionManager.SelectDoc(this, e.ctrlKey);
- }
- }
- stopPropagation = (e: React.SyntheticEvent) => {
- e.stopPropagation();
}
deleteClicked = (): void => {
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 = Docs.KVPDocument(this.props.Document, { title: this.props.Document.title + ".kvp", width: 300, height: 300 });
+ CollectionDockingView.Instance.AddRightSplit(kvp);
+ }
+ makeButton = (e: React.MouseEvent): void => {
+ let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ doc.isButton = !BoolCast(doc.isButton, false);
+ if (doc.isButton && !doc.nativeWidth) {
+ doc.nativeWidth = doc[WidthSym]();
+ doc.nativeHeight = doc[HeightSym]();
}
}
fullScreenClicked = (e: React.MouseEvent): void => {
- 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.displayMenu(e.pageX - 15, e.pageY - 15);
- }
-
- closeFullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.CloseFullScreen();
+ const doc = Doc.MakeDelegate(FieldValue(this.Document.proto));
+ if (doc) {
+ CollectionDockingView.Instance.OpenFullScreen(doc);
+ }
ContextMenu.Instance.clearItems();
- 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.SetBoolean(KeyStore.Minimized, true);
SelectionManager.DeselectAll();
}
-
@undoBatch
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
+ drop = async (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.LinkDragData) {
- let sourceDoc: Document = de.data.linkSourceDocument;
- let destDoc: Document = this.props.Document;
- let linkDoc: Document = new Document();
-
- destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest =>
- sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc =>
- runInAction(() => {
- let batch = UndoManager.StartBatch("document view drop");
- linkDoc.SetText(KeyStore.Title, "New Link");
- linkDoc.SetText(KeyStore.LinkDescription, "");
- linkDoc.SetText(KeyStore.LinkTags, "Default");
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
- let dstTarg = protoDest ? protoDest : destDoc;
- let srcTarg = protoSrc ? protoSrc : sourceDoc;
- 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());
- })
- )
- );
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ de.data.droppedDocuments.push(destDoc);
e.stopPropagation();
}
}
+ @action
onDrop = (e: React.DragEvent) => {
let text = e.dataTransfer.getData("text/plain");
if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- let oldLayout = this.props.Document.GetText(KeyStore.Layout, "");
+ let oldLayout = FieldValue(this.Document.layout) || "";
let layout = text.replace("{layout}", oldLayout);
- this.props.Document.SetText(KeyStore.Layout, layout);
+ this.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
}
+
+ @action
+ addTemplate = (template: Template) => {
+ this.templates.push(template.Layout);
+ this.templates = this.templates;
+ }
+
+ @action
+ removeTemplate = (template: Template) => {
+ for (let i = 0; i < this.templates.length; i++) {
+ if (this.templates[i] === template.Layout) {
+ this.templates.splice(i, 1);
+ break;
+ }
+ }
+ this.templates = this.templates;
+ }
+
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
@@ -284,51 +287,53 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
e.preventDefault();
- !this.isMinimized() && ContextMenu.Instance.addItem({ description: "Minimize", event: this.minimize });
ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked });
+ ContextMenu.Instance.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton });
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: "Copy URL", event: () => Utils.CopyText(DocServer.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: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) });
ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked });
+ if (!this.topMost) {
+ // DocumentViews should stop propagation of this event
+ e.stopPropagation();
+ }
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
- if (!SelectionManager.IsSelected(this))
+ if (!SelectionManager.IsSelected(this)) {
SelectionManager.SelectDoc(this, false);
+ }
}
- @action
- expand = () => this.props.Document.SetBoolean(KeyStore.Minimized, false)
- isMinimized = () => this.props.Document.GetBoolean(KeyStore.Minimized, false);
+
isSelected = () => SelectionManager.IsSelected(this);
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} />); }
+ @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); }
render() {
var scaling = this.props.ContentScaling();
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} onClick={this.expand} onDrop={this.onDrop}
- style={{ transform: `scale(${scaling} , ${scaling})` }} onPointerDown={this.onPointerDown} />;
- }
return (
<div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
- background: this.props.Document.GetText(KeyStore.BackgroundColor, ""),
- width: nativeWidth, height: nativeHeight,
+ borderRadius: "inherit",
+ background: this.Document.backgroundColor || "",
+ width: nativeWidth,
+ height: nativeHeight,
transform: `scale(${scaling}, ${scaling})`
}}
- onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown}
+ onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
>
{this.contents}
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e9f6950ff..8bdf34181 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,28 +1,23 @@
import React = require("react");
import { observer } from "mobx-react";
-import { computed } from "mobx";
-import { Field, FieldWaiting, FieldValue, Opt } from "../../../fields/Field";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
-import { NumberField } from "../../../fields/NumberField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { ImageField } from "../../../fields/ImageField";
-import { VideoField } from "../../../fields/VideoField";
-import { Key } from "../../../fields/Key";
+import { computed, observable } from "mobx";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
-import { WebBox } from "./WebBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
-import { AudioField } from "../../../fields/AudioField";
-import { ListField } from "../../../fields/ListField";
import { DocumentContentsView } from "./DocumentContentsView";
import { Transform } from "../../util/Transform";
-import { KeyStore } from "../../../fields/KeyStore";
-import { returnFalse, emptyDocFunction, emptyFunction, returnOne } from "../../../Utils";
+import { returnFalse, emptyFunction } from "../../../Utils";
import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { IconBox } from "./IconBox";
+import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
+import { IconField } from "../../../new_fields/IconField";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { DateField } from "../../../new_fields/DateField";
//
@@ -31,54 +26,62 @@ import { CollectionVideoView } from "../collections/CollectionVideoView";
// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
//
export interface FieldViewProps {
- fieldKey: Key;
+ fieldKey: string;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- Document: Document;
+ Document: Doc;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
isTopMost: boolean;
selectOnLoad: boolean;
- addDocument?: (document: Document, allowDuplicates?: boolean) => boolean;
- removeDocument?: (document: Document) => boolean;
- moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
+ addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (document: Doc) => boolean;
+ moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- focus: (doc: Document) => void;
+ focus: (doc: Doc) => void;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ setVideoBox?: (player: VideoBox) => void;
}
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") {
- return `<${fieldType.name} {...props} fieldKey={${fieldStr}} />`;
+ public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
}
@computed
- get field(): FieldValue<Field> {
- const { Document: doc, fieldKey } = this.props;
- return doc.Get(fieldKey);
+ get field(): FieldResult {
+ const { Document, fieldKey } = this.props;
+ return Document[fieldKey];
}
render() {
const field = this.field;
- if (!field) {
+ if (field === undefined) {
return <p>{'<null>'}</p>;
}
- if (field instanceof TextField) {
- return <p>{field.Data}</p>;
- }
+ // if (typeof field === "string") {
+ // return <p>{field}</p>;
+ // }
else if (field instanceof RichTextField) {
return <FormattedTextBox {...this.props} />;
}
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} />;
}
else if (field instanceof AudioField) {
return <AudioBox {...this.props} />;
+ } else if (field instanceof DateField) {
+ return <p>{field.date.toLocaleString()}</p>;
}
- else if (field instanceof Document) {
+ else if (field instanceof Doc) {
return (
<DocumentContentsView Document={field}
addDocument={undefined}
@@ -89,29 +92,28 @@ export class FieldView extends React.Component<FieldViewProps> {
PanelHeight={() => 100}
isTopMost={true} //TODO Why is this top most?
selectOnLoad={false}
- focus={emptyDocFunction}
- isSelected={returnFalse}
+ focus={emptyFunction}
+ isSelected={this.props.isSelected}
select={returnFalse}
- layoutKey={KeyStore.Layout}
+ layoutKey={"layout"}
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged} />
+ toggleMinimized={emptyFunction}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction} />
);
}
- else if (field instanceof ListField) {
+ else if (field instanceof List) {
return (<div>
- {(field as ListField<Field>).Data.map(f => f instanceof Document ? f.Title : f.GetValue().toString()).join(", ")}
+ {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
</div>);
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
// else if (field instanceof HtmlField) {
// return <WebBox {...this.props} />
// }
- else if (field instanceof NumberField) {
- return <p>{field.Data}</p>;
- }
- else if (field !== FieldWaiting) {
- return <p>{JSON.stringify(field.GetValue())}</p>;
+ else if (!(field instanceof Promise)) {
+ return <p>{JSON.stringify(field)}</p>;
}
else {
return <p> {"Waiting for server..."} </p>;
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 5eb2bf7ce..458a62c5b 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -10,13 +10,14 @@
outline: none !important;
}
-.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: $light-color-secondary;
- padding: 0.9em;
+.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
+ background: inherit;
+ padding: 0;
border-width: 0px;
- border-radius: $border-radius;
+ border-radius: inherit;
border-color: $intermediate-color;
box-sizing: border-box;
+ background-color: inherit;
border-style: solid;
overflow-y: scroll;
overflow-x: hidden;
@@ -24,10 +25,19 @@
height: 100%;
pointer-events: all;
}
+
.formattedTextBox-cont-hidden {
overflow: hidden;
pointer-events: none;
}
+.formattedTextBox-inner-rounded {
+ height: calc(100% - 25px);
+ width: calc(100% - 40px);
+ position: absolute;
+ overflow: auto;
+ top: 15;
+ left: 20;
+}
.menuicon {
display: inline-block;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index bd98622fb..fc58e513b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,30 +1,35 @@
-import { action, IReactionDisposer, reaction } from "mobx";
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
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 { Doc, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { DocServer } from "../../DocServer";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { schema } from "../../util/RichTextSchema";
+import { ImageResizeView, schema } from "../../util/RichTextSchema";
+import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { ContextMenu } from "../../views/ContextMenu";
-import { MainOverlayTextBox } from "../MainOverlayTextBox";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-const { buildMenuItems } = require("prosemirror-example-setup");
-const { menuBar } = require("prosemirror-menu");
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
-// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"}
+// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name}
//
// In Code, the node's HTML is specified in the document's parameterized structure as:
// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
@@ -43,51 +48,91 @@ export interface FormattedTextBoxOverlay {
isOverlay?: boolean;
}
-export class FormattedTextBox extends React.Component<(FieldViewProps & FormattedTextBoxOverlay)> {
- public static LayoutString(fieldStr: string = "DataKey") {
+const richTextSchema = createSchema({
+ documentText: "string"
+});
+
+type RichTextDocument = makeInterface<[typeof richTextSchema]>;
+const RichTextDocument = makeInterface(richTextSchema);
+
+@observer
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
private _ref: React.RefObject<HTMLDivElement>;
+ private _proseRef: React.RefObject<HTMLDivElement>;
private _editorView: Opt<EditorView>;
private _gotDown: boolean = false;
+ private _dropDisposer?: DragManager.DragDropDisposer;
private _reactionDisposer: Opt<IReactionDisposer>;
private _inputReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
+ @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static InputBoxOverlayScroll: number = 0;
constructor(props: FieldViewProps) {
super(props);
this._ref = React.createRef();
- this.onChange = this.onChange.bind(this);
+ this._proseRef = React.createRef();
+ if (this.props.isOverlay) {
+ DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
+ }
}
_applyingChange: boolean = false;
+ _lastState: any = undefined;
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
- const state = this._editorView.state.apply(tx);
+ const state = this._lastState = this._editorView.state.apply(tx);
this._editorView.updateState(state);
this._applyingChange = true;
- this.props.Document.SetDataOnPrototype(
- this.props.fieldKey,
- JSON.stringify(state.toJSON()),
- RichTextField
- );
- this.props.Document.SetDataOnPrototype(KeyStore.DocumentText, state.doc.textBetween(0, state.doc.content.size, "\n\n"), TextField);
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
+ Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
this._applyingChange = false;
- // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);
+ let title = StrCast(this.props.Document.title);
+ if (title && title.startsWith("-") && this._editorView) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
+
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ e.stopPropagation();
}
}
componentDidMount() {
+ if (this._ref.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
+ }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
plugins: this.props.isOverlay ? [
+ this.tooltipTextMenuPlugin(),
history(),
keymap(buildKeymap(schema)),
keymap(baseKeymap),
- this.tooltipTextMenuPlugin(),
// this.tooltipLinkingMenuPlugin(),
new Plugin({
props: {
@@ -102,46 +147,51 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
};
if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc.Id,
+ this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
() => {
if (this._editorView) {
this._editorView.destroy();
}
- this.setupEditor(config, MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
+ this.setupEditor(config, 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()));
+ () => {
+ if (this.props.isSelected()) {
+ FormattedTextBox.InputBoxOverlay = this;
+ FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
+ }
+ });
}
+
this._reactionDisposer = reaction(
() => {
- const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined;
- return field && field !== FieldWaiting ? field.Data : undefined;
+ const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
+ return field ? field.Data : undefined;
},
- field => {
- if (field && this._editorView && !this._applyingChange) {
- 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.props.Document);
}
- shouldComponentUpdate() {
- return false;
- }
-
- 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, {
+ private setupEditor(config: any, doc?: Doc) {
+ let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
+ if (this._proseRef.current) {
+ this._editorView = new EditorView(this._proseRef.current, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
- dispatchTransaction: this.dispatchTransaction
+ dispatchTransaction: this.dispatchTransaction,
+ nodeViews: {
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos) }
+ }
});
+ let text = StrCast(this.props.Document.documentText);
+ if (text.startsWith("@@@")) {
+ this.props.Document.proto!.documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ }
}
if (this.props.selectOnLoad) {
@@ -163,67 +213,81 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
+ if (this._dropDisposer) {
+ this._dropDisposer();
+ }
}
- @action
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- const { fieldKey, Document } = this.props;
- Document.SetOnPrototype(fieldKey, new RichTextField(e.target.value));
- // doc.SetData(fieldKey, e.target.value, RichTextField);
- }
onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 1 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
- console.log("first");
+ 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) {
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) {
+ if (e.target && (e.target as any).href) {
+ let href = (e.target as any).href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "");
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f))
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ else CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ }
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
this._gotDown = true;
- console.log("second");
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
- console.log("pointer up");
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
+ this._toolTipTextMenu.tooltip.style.opacity = "1";
+ }
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ @action
onFocused = (e: React.FocusEvent): void => {
if (!this.props.isOverlay) {
- MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform());
+ FormattedTextBox.InputBoxOverlay = this;
} else {
- if (this._ref.current) {
- this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
+ if (this._proseRef.current) {
+ this._proseRef.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
}
}
}
//REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => { };
+ textCapability = (e: React.MouseEvent): void => {
+ if (NumCast(this.props.Document.nativeWidth)) {
+ this.props.Document.nativeWidth = undefined;
+ this.props.Document.nativeHeight = undefined;
+ } else {
+ this.props.Document.nativeWidth = this.props.Document[WidthSym]();
+ this.props.Document.nativeHeight = this.props.Document[HeightSym]();
+ }
+ }
specificContextMenu = (e: React.MouseEvent): void => {
if (!this._gotDown) {
e.preventDefault();
return;
}
ContextMenu.Instance.addItem({
- description: "Text Capability",
+ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
event: this.textCapability
});
-
- // ContextMenu.Instance.addItem({
- // description: "Submenu",
- // items: [
- // {
- // description: "item 1", event:
- // },
- // {
- // description: "item 2", event:
- // }
- // ]
- // })
- // e.stopPropagation()
}
onPointerWheel = (e: React.WheelEvent): void => {
@@ -232,15 +296,26 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
}
}
+ onClick = (e: React.MouseEvent): void => {
+ this._proseRef.current!.focus();
+ }
+ onMouseDown = (e: React.MouseEvent): void => {
+ if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
+ e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ }
+ }
+
tooltipTextMenuPlugin() {
let myprops = this.props;
+ let self = this;
return new Plugin({
view(_editorView) {
- return new TooltipTextMenu(_editorView, myprops);
+ return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
}
+ _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
tooltipLinkingMenuPlugin() {
let myprops = this.props;
return new Plugin({
@@ -249,28 +324,55 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte
}
});
}
-
- onKeyPress(e: React.KeyboardEvent) {
+ onBlur = (e: any) => {
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ }
+ public _undoTyping?: UndoManager.Batch;
+ onKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ SelectionManager.DeselectAll();
+ }
e.stopPropagation();
- if (e.keyCode === 9) e.preventDefault();
+ 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;
+ if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ }
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
}
render() {
- let style = this.props.isSelected() || this.props.isOverlay ? "scroll" : "hidden";
+ let style = this.props.isOverlay ? "scroll" : "hidden";
+ let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
+ let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
return (
- <div className={`formattedTextBox-cont-${style}`}
+ <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
+ style={{
+ pointerEvents: interactive ? "all" : "none",
+ }}
onKeyDown={this.onKeyPress}
onKeyPress={this.onKeyPress}
onFocus={this.onFocused}
+ onClick={this.onClick}
+ onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
+ onMouseDown={this.onMouseDown}
onContextMenu={this.specificContextMenu}
// tfs: do we need this event handler
onWheel={this.onPointerWheel}
- ref={this._ref}
- />
+ >
+ <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} 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..85bbdeb59
--- /dev/null
+++ b/src/client/views/nodes/IconBox.scss
@@ -0,0 +1,22 @@
+
+@import "../globalCssVariables";
+.iconBox-container {
+ position: absolute;
+ left:0;
+ top:0;
+ height: 100%;
+ width: max-content;
+ // overflow: hidden;
+ pointer-events: all;
+ svg {
+ width: $MINIMIZED_ICON_SIZE !important;
+ height: 100%;
+ background: white;
+ }
+ .iconBox-label {
+ position: inherit;
+ width:max-content;
+ font-size: 14px;
+ margin-top: 3px;
+ }
+} \ 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..4bcb4c636
--- /dev/null
+++ b/src/client/views/nodes/IconBox.tsx
@@ -0,0 +1,69 @@
+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 { computed, observable, runInAction, reaction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./IconBox.scss";
+import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { IconField } from "../../../new_fields/IconField";
+import { ContextMenu } from "../ContextMenu";
+import Measure from "react-measure";
+import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
+import { listSpec } from "../../../new_fields/Schema";
+
+
+library.add(faCaretUp);
+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 layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
+ @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" />;
+ }
+
+ setLabelField = (e: React.MouseEvent): void => {
+ this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label",
+ event: this.setLabelField
+ });
+ }
+ @observable _panelWidth: number = 0;
+ @observable _panelHeight: number = 0;
+ render() {
+ let labelField = StrCast(this.props.Document.labelField);
+ let hideLabel = BoolCast(this.props.Document.hideLabel);
+ let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []);
+ let firstDoc = maxDoc && maxDoc.length > 0 && maxDoc[0] instanceof Doc ? maxDoc[0] as Doc : undefined;
+ let label = !hideLabel && firstDoc && labelField ? firstDoc[labelField] : "";
+ return (
+ <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
+ {this.minimizedIcon}
+ <Measure onResize={(r) => runInAction(() => { if (r.entry.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.entry.width + Number(MINIMIZED_ICON_SIZE)); })}>
+ {({ measureRef }) =>
+ <span ref={measureRef} className="iconBox-label">{label}</span>
+ }
+ </Measure>
+ </div>);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index f4b3837ff..2316a050e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -6,6 +6,12 @@
height: auto;
max-width: 100%;
max-height: 100%;
+ pointer-events: none;
+}
+.imageBox-cont-interactive {
+ pointer-events: all;
+ width:100%;
+ height:auto;
}
.imageBox-dot {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ce855384c..0e9e904a8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -3,11 +3,6 @@ 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';
@@ -15,12 +10,27 @@ import { ContextMenu } from "../../views/ContextMenu";
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
+import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
+import { DocComponent } from '../DocComponent';
+import { positionSchema } from './DocumentView';
+import { FieldValue, Cast, StrCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
+import { List } from '../../../new_fields/List';
+import { InkingControl } from '../InkingControl';
+import { Doc } from '../../../new_fields/Doc';
+
+export const pageSchema = createSchema({
+ curPage: "number"
+});
+
+type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
+const ImageDocument = makeInterface(pageSchema, positionSchema);
@observer
-export class ImageBox extends React.Component<FieldViewProps> {
+export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
public static LayoutString() { return FieldView.LayoutString(ImageBox); }
- private _imgRef: React.RefObject<HTMLImageElement>;
+ private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
private _lastTap: number = 0;
@@ -28,19 +38,13 @@ export class ImageBox extends React.Component<FieldViewProps> {
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
- constructor(props: FieldViewProps) {
- super(props);
-
- this._imgRef = React.createRef();
- }
-
@action
onLoad = (target: any) => {
var h = this._imgRef.current!.naturalHeight;
var w = this._imgRef.current!.naturalWidth;
if (this._photoIndex === 0) {
- this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w);
- this.props.Document.SetNumber(KeyStore.Height, this.props.Document.Width() * h / w);
+ this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w;
+ this.Document.height = FieldValue(this.Document.width, 0) * h / w;
}
}
@@ -63,19 +67,18 @@ export class ImageBox extends React.Component<FieldViewProps> {
@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, "");
+ de.data.droppedDocuments.forEach(action((drop: Doc) => {
+ let layout = StrCast(drop.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 imgData = this.props.Document[this.props.fieldKey];
+ if (imgData instanceof ImageField) {
+ Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
}
- let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]);
+ let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] 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);
+ let field = drop.data;
+ if (field instanceof ImageField) imgList.push(field);
+ else if (field instanceof List) imgList.concat(field);
}
e.stopPropagation();
}
@@ -87,7 +90,6 @@ export class ImageBox extends React.Component<FieldViewProps> {
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);
@@ -126,9 +128,9 @@ export class ImageBox extends React.Component<FieldViewProps> {
}
specificContextMenu = (e: React.MouseEvent): void => {
- let field = this.props.Document.GetT(this.props.fieldKey, ImageField);
- if (field && field !== FieldWaiting) {
- let url = field.Data.href;
+ let field = Cast(this.Document[this.props.fieldKey], ImageField);
+ if (field) {
+ let url = field.url.href;
ContextMenu.Instance.addItem({
description: "Copy path", event: () => {
Utils.CopyText(url);
@@ -140,29 +142,29 @@ export class ImageBox extends React.Component<FieldViewProps> {
@action
onDotDown(index: number) {
this._photoIndex = index;
- this.props.Document.SetNumber(KeyStore.CurPage, index);
+ this.Document.curPage = index;
}
dots(paths: string[]) {
- let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1);
+ let nativeWidth = FieldValue(this.Document.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 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 field = this.Document[this.props.fieldKey];
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);
+ if (field instanceof ImageField) paths = [field.url.href];
+ else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href);
+ let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
+ let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
return (
- <div className="imageBox-cont" onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <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)}
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 6ebd73f2c..20cae03d4 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -8,6 +8,7 @@
border-radius: $border-radius;
box-sizing: border-box;
display: inline-block;
+ pointer-events: all;
.imageBox-cont img {
width: auto;
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index ddbec014b..876a3c173 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -2,24 +2,22 @@
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 { Field, FieldWaiting } from '../../../fields/Field';
-import { Key } from '../../../fields/Key';
-import { KeyStore } from '../../../fields/KeyStore';
-import { CompileScript, ToField } from "../../util/Scripting";
+import { CompileScript } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react");
+import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
+import { Doc, IsField } from "../../../new_fields/Doc";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
private _mainCont = React.createRef<HTMLDivElement>();
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
- @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 50); }
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
constructor(props: FieldViewProps) {
@@ -30,8 +28,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
if (this._keyInput && this._valueInput) {
- let doc = this.props.Document.GetT(KeyStore.Data, Document);
- if (!doc || doc === FieldWaiting) {
+ let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ if (!doc) {
return;
}
let realDoc = doc;
@@ -43,13 +41,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let res = script.run();
if (!res.success) return;
const field = res.result;
- if (field instanceof Field) {
- realDoc.Set(new Key(this._keyInput), field);
- } else {
- let dataField = ToField(field);
- if (dataField) {
- realDoc.Set(new Key(this._keyInput), dataField);
- }
+ if (IsField(field)) {
+ realDoc[this._keyInput] = field;
}
this._keyInput = "";
this._valueInput = "";
@@ -67,16 +60,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
createTable = () => {
- let doc = this.props.Document.GetT(KeyStore.Data, Document);
- if (!doc || doc === FieldWaiting) {
+ let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ if (!doc) {
return <tr><td>Loading...</td></tr>;
}
let realDoc = doc;
let ids: { [key: string]: string } = {};
- let protos = doc.GetAllPrototypes();
+ let protos = Doc.GetAllPrototypes(doc);
for (const proto of protos) {
- proto._proxies.forEach((val: any, key: string) => {
+ Object.keys(proto).forEach(key => {
if (!(key in ids)) {
ids[key] = key;
}
@@ -86,7 +79,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} keyWidth={100 - this.splitPercentage} 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")} key={key} keyName={key} />);
}
return rows;
}
@@ -116,7 +109,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@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)));
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 01701e02c..ff6885965 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -25,4 +25,5 @@
}
.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 d480eb5af..5de660d57 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,48 +1,33 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { 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 { CompileScript, ToField } from "../../util/Scripting";
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
+import { CompileScript } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import "./KeyValuePair.scss";
import React = require("react");
+import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { FieldValue } from '../../../new_fields/Types';
// Represents one row in a key value plane
export interface KeyValuePairProps {
rowStyle: string;
- fieldId: string;
- doc: Document;
+ keyName: string;
+ doc: Doc;
keyWidth: number;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
- @observable private key: Opt<Key>;
-
- constructor(props: KeyValuePairProps) {
- super(props);
- Server.GetField(this.props.fieldId,
- action((field: Opt<Field>) => field instanceof Key && (this.key = field)));
-
- }
-
-
render() {
- if (!this.key) {
- return <tr><td>error</td><td /></tr>;
- }
let props: FieldViewProps = {
Document: this.props.doc,
ContainingCollectionView: undefined,
- fieldKey: this.key,
+ fieldKey: this.props.keyName,
isSelected: returnFalse,
select: emptyFunction,
isTopMost: false,
@@ -50,29 +35,36 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
active: returnFalse,
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
- focus: emptyDocFunction,
+ focus: emptyFunction,
+ PanelWidth: returnZero,
+ PanelHeight: returnZero,
};
let contents = <FieldView {...props} />;
+ let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
return (
<tr className={this.props.rowStyle}>
<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);
- field && field instanceof Field && props.Document.Set(props.fieldKey, undefined);
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1)
+ props.Document[props.fieldKey] = undefined;
+ else props.Document.proto![props.fieldKey] = undefined;
}}>
X
</button>
- <div className="keyValuePair-keyField">{this.key.Name}</div>
+ <div className="keyValuePair-keyField">{fieldKey}</div>
</div>
</td>
<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 field = FieldValue(props.Document[props.fieldKey]);
+ if (field) {
+ //TODO Types
+ return String(field);
+ // return field.ToScriptString();
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
let script = CompileScript(value, { addReturn: true });
@@ -82,15 +74,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
let res = script.run();
if (!res.success) return false;
const field = res.result;
- if (field instanceof Field) {
- props.Document.Set(props.fieldKey, field);
+ if (IsField(field)) {
+ props.Document[props.fieldKey] = field;
return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- props.Document.Set(props.fieldKey, dataField);
- return true;
- }
}
return false;
}}>
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 8bc70b48f..639f83b38 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,14 +1,14 @@
@import "../globalCssVariables";
.link-container {
width: 100%;
- height: 35px;
+ height: 50px;
display: flex;
flex-direction: row;
border-top: 0.5px solid #bababa;
}
.info-container {
- width: 55%;
+ width: 65%;
padding-top: 5px;
padding-left: 5px;
display: flex;
@@ -24,7 +24,8 @@
}
.button-container {
- width: 45%;
+ width: 35%;
+ padding-top: 8px;
display: flex;
flex-direction: row;
}
@@ -49,17 +50,17 @@
cursor: pointer;
}
-.fa-icon-view {
- margin-left: 3px;
- margin-top: 5px;
-}
+// .fa-icon-view {
+// margin-left: 3px;
+// margin-top: 5px;
+// }
.fa-icon-edit {
- margin-left: 5px;
- margin-top: 5px;
+ margin-left: 6px;
+ margin-top: 6px;
}
.fa-icon-delete {
- margin-left: 6px;
- margin-top: 5px;
+ margin-left: 7px;
+ margin-top: 6px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 1c0e316e8..08cfa590b 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -2,15 +2,15 @@ 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 { 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 { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import './LinkBox.scss';
import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+import { Cast, NumCast } from '../../../new_fields/Types';
+import { listSpec } from '../../../new_fields/Schema';
+import { action } from 'mobx';
library.add(faEye);
@@ -18,9 +18,9 @@ library.add(faEdit);
library.add(faTimes);
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
linkName: String;
- pairedDoc: Document;
+ pairedDoc: Doc;
type: String;
showEditor: () => void;
}
@@ -29,62 +29,54 @@ interface Props {
export class LinkBox extends React.Component<Props> {
@undoBatch
- onViewButtonPressed = (e: React.PointerEvent): void => {
+ onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
if (docView) {
docView.props.focus(docView.props.Document);
} else {
- this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => {
- if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc.MakeDelegate());
- } else if (contextDoc instanceof Document) {
- this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => {
- contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => {
- if (pfield !== cfield) {
- contextDoc.SetNumber(KeyStore.CurPage, pfield.Data);
- }
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
- contextView.props.focus(contextDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
- }
- });
- });
+ const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc);
+ if (!contextDoc) {
+ CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc));
+ } else {
+ const page = NumCast(this.props.pairedDoc.page, undefined);
+ const curPage = NumCast(contextDoc.curPage, undefined);
+ if (page !== curPage) {
+ contextDoc.curPage = page;
}
- });
+ let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
+ if (contextView) {
+ contextDoc.panTransformType = "Ease";
+ contextView.props.focus(contextDoc);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(contextDoc);
+ }
+ }
}
}
onEditButtonPressed = (e: React.PointerEvent): void => {
- console.log("edit down");
e.stopPropagation();
this.props.showEditor();
}
- onDeleteButtonPressed = (e: React.PointerEvent): void => {
- console.log("delete down");
+ @action
+ onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- this.props.linkDoc.GetTAsync(KeyStore.LinkedFromDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedToDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- });
+ const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
+ if (linkedFrom) {
+ const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
+ if (linkedToDocs) {
+ linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
}
- });
- this.props.linkDoc.GetTAsync(KeyStore.LinkedToDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedFromDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- });
+ }
+ if (linkedTo) {
+ const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
+ if (linkedFromDocs) {
+ linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
}
- });
+ }
}
render() {
@@ -102,8 +94,8 @@ export class LinkBox extends React.Component<Props> {
</div>
<div className="button-container">
- <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
- <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div>
+ {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
<div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
<FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
<div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index ea2e7289c..9629585d7 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -22,7 +22,7 @@
.save-button {
width: 50px;
- height: 20px;
+ height: 22px;
pointer-events: auto;
background-color: $dark-color;
color: $light-color;
@@ -38,6 +38,5 @@
.save-button:hover {
background: $main-accent;
- transform: scale(1.05);
cursor: pointer;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index bde50fed8..71a423338 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -3,31 +3,30 @@ import React = require("react");
import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
import './LinkEditor.scss';
-import { KeyStore } from '../../../fields/KeyStore';
import { props } from "bluebird";
import { DocumentView } from "./DocumentView";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
import { link } from "fs";
+import { StrCast } from "../../../new_fields/Types";
+import { Doc } from "../../../new_fields/Doc";
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
showLinks: () => void;
}
@observer
export class LinkEditor extends React.Component<Props> {
- @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, "");
- @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, "");
+ @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
+ @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
onSaveButtonPressed = (e: React.PointerEvent): void => {
- console.log("view down");
e.stopPropagation();
- this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField);
- this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField);
+ let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+ linkDoc.title = this._nameInput;
+ linkDoc.linkDescription = this._descriptionInput;
this.props.showLinks();
}
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index ac09da305..24901913d 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,15 +1,14 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from "../../../fields/ListField";
import { DocumentView } from "./DocumentView";
import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
+import { Doc } from "../../../new_fields/Doc";
+import { Cast, FieldValue } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { Id } from "../../../new_fields/RefField";
interface Props {
docView: DocumentView;
@@ -19,28 +18,28 @@ interface Props {
@observer
export class LinkMenu extends React.Component<Props> {
- @observable private _editingLink?: Document;
+ @observable private _editingLink?: Doc;
- renderLinkItems(links: Document[], key: Key, type: string) {
+ renderLinkItems(links: Doc[], key: string, type: string) {
return links.map(link => {
- let doc = link.GetT(key, Document);
- if (doc && doc !== FieldWaiting) {
- return <LinkBox key={doc.Id} linkDoc={link} linkName={link.Title} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ let doc = FieldValue(Cast(link[key], Doc));
+ if (doc) {
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
}
});
}
render() {
//get list of links from document
- let linkFrom: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []);
- let linkTo: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []);
+ let linkFrom = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
+ let linkTo = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc);
if (this._editingLink === undefined) {
return (
<div id="linkMenu-container">
<input id="linkMenu-searchBar" type="text" placeholder="Search..."></input>
<div id="linkMenu-list">
- {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Destination: ")}
- {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Source: ")}
+ {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
+ {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
</div>
</div>
);
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..caa66cbeb 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,24 +1,26 @@
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";
//@ts-ignore
import { Document, Page } from "react-pdf";
import 'react-pdf/dist/Page/AnnotationLayer.css';
-import { FieldWaiting, Opt } from '../../../fields/Field';
-import { ImageField } from '../../../fields/ImageField';
-import { KeyStore } from '../../../fields/KeyStore';
-import { PDFField } from '../../../fields/PDFField';
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
import { Annotation } from './Annotation';
import { FieldView, FieldViewProps } from './FieldView';
-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 { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Opt } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { makeInterface } from "../../../new_fields/Schema";
+import { positionSchema } from "./DocumentView";
+import { pageSchema } from "./ImageBox";
+import { ImageField, PdfField } from "../../../new_fields/URLField";
+import { InkingControl } from "../InkingControl";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -40,46 +42,20 @@ 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
*/
+
+type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(positionSchema, pageSchema);
+
@observer
-export class PDFBox extends React.Component<FieldViewProps> {
+export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
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
@@ -89,8 +65,8 @@ export class PDFBox extends React.Component<FieldViewProps> {
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 1); }
- @computed private get thumbnailPage() { return this.props.Document.GetNumber(KeyStore.ThumbnailPage, -1); }
+ @computed private get curPage() { return FieldValue(this.Document.curPage, 1); }
+ @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); }
componentDidMount() {
this._reactionDisposer = reaction(
@@ -112,43 +88,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 +122,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 +145,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 +211,21 @@ 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,110 +233,28 @@ 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
saveThumbnail = () => {
this._renderAsSvg = false;
setTimeout(() => {
- var me = this;
- let nwidth = me.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- let nheight = me.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ let nwidth = FieldValue(this.Document.nativeWidth, 0);
+ let nheight = FieldValue(this.Document.nativeHeight, 0);
htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
.then(action((dataUrl: string) => {
- me.props.Document.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
- me.props.Document.SetNumber(KeyStore.ThumbnailPage, me.props.Document.GetNumber(KeyStore.CurPage, -1));
- me._renderAsSvg = true;
+ this.props.Document.thumbnail = new ImageField(new URL(dataUrl));
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
+ this._renderAsSvg = true;
}))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
@@ -397,24 +264,8 @@ 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);
+ this.props.Document.numPages = page._transport.numPages;
if (this._perPageInfo.length === 0) { //Makes sure it only runs once
this._perPageInfo = [...Array(page._transport.numPages)];
}
@@ -424,31 +275,38 @@ 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)) {
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ if (!FieldValue(this.Document.nativeHeight, 0)) {
var nativeHeight = nativeWidth * r.entry.height / r.entry.width;
- this.props.Document.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0));
- this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight);
+ this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.props.Document.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 pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
+ let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight;
+ let body = NumCast(this.props.Document.nativeHeight) ?
+ 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 >;
}
@@ -456,8 +314,8 @@ export class PDFBox extends React.Component<FieldViewProps> {
@computed
get pdfRenderer() {
let proxy = this._loaded ? (null) : this.imageProxyRenderer;
- let pdfUrl = this.props.Document.GetT(this.props.fieldKey, PDFField);
- if ((!this._interactive && proxy) || !pdfUrl || pdfUrl === FieldWaiting) {
+ let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
+ if ((!this._interactive && proxy) || !pdfUrl) {
return proxy;
}
return [
@@ -470,18 +328,32 @@ export class PDFBox extends React.Component<FieldViewProps> {
@computed
get imageProxyRenderer() {
- let thumbField = this.props.Document.Get(KeyStore.Thumbnail);
+ let thumbField = this.props.Document.thumbnail;
if (thumbField) {
- let path = thumbField === FieldWaiting || this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
+ thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
return <img src={path} width="100%" />;
}
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 9d7c2bc56..422508f90 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,78 +1,82 @@
import React = require("react");
import { observer } from "mobx-react";
-import { FieldWaiting, Opt } from '../../../fields/Field';
-import { VideoField } from '../../../fields/VideoField';
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
+import { action, computed, trace } from "mobx";
+import { DocComponent } from "../DocComponent";
+import { positionSchema } from "./DocumentView";
+import { makeInterface } from "../../../new_fields/Schema";
+import { pageSchema } from "./ImageBox";
+import { Cast, FieldValue, NumCast, ToConstructor, ListSpec } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
import Measure from "react-measure";
-import { action, trace, observable, IReactionDisposer, computed, reaction } from "mobx";
-import { KeyStore } from "../../../fields/KeyStore";
-import { number } from "prop-types";
+import "./VideoBox.scss";
+import { Field, FieldResult, Opt } from "../../../new_fields/Doc";
+
+type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
-export class VideoBox extends React.Component<FieldViewProps> {
+export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
- private _reactionDisposer: Opt<IReactionDisposer>;
- private _videoRef = React.createRef<HTMLVideoElement>();
+ private _videoRef: HTMLVideoElement | null = null;
+ private _loaded: boolean = false;
+ private get initialTimecode() { return FieldValue(this.Document.curPage, -1); }
public static LayoutString() { return FieldView.LayoutString(VideoBox); }
- constructor(props: FieldViewProps) {
- super(props);
+ public get player(): HTMLVideoElement | undefined {
+ if (this._videoRef) {
+ return this._videoRef;
+ }
}
-
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); }
-
-
- _loaded: boolean = false;
-
@action
setScaling = (r: any) => {
if (this._loaded) {
// bcz: the nativeHeight should really be set when the document is imported.
- // also, the native dimensions could be different for different pages of the PDF
- // so this design is flawed.
- var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
var newNativeHeight = nativeWidth * r.entry.height / r.entry.width;
if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) {
- this.props.Document.SetNumber(KeyStore.Height, newNativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0));
- this.props.Document.SetNumber(KeyStore.NativeHeight, newNativeHeight);
+ this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.Document.nativeHeight = newNativeHeight;
}
} else {
this._loaded = true;
}
}
- get player(): HTMLVideoElement | undefined {
- return this._videoRef.current ? this._videoRef.current.getElementsByTagName("video")[0] : undefined;
+ componentDidMount() {
+ if (this.props.setVideoBox) this.props.setVideoBox(this);
}
@action
setVideoRef = (vref: HTMLVideoElement | null) => {
- if (this.curPage >= 0 && vref) {
- vref.currentTime = this.curPage;
- (vref as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage;
+ this._videoRef = vref;
+ if (this.initialTimecode >= 0 && vref) {
+ vref.currentTime = this.initialTimecode;
}
}
+ 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) {
+ let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ if (!field) {
return <div>Loading</div>;
}
- let path = field.Data.href;
- trace();
- return (
+ let content = this.videoContent(field.url.href);
+ return NumCast(this.props.Document.nativeHeight) ?
+ content :
<Measure onResize={this.setScaling}>
{({ measureRef }) =>
<div style={{ width: "100%", height: "auto" }} ref={measureRef}>
- <video className="videobox-cont" ref={this.setVideoRef}>
- <source src={path} type="video/mp4" />
- Not supported.
- </video>
+ {content}
</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 2ad1129a4..eb09b0693 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,12 +1,19 @@
-.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 {
@@ -15,6 +22,12 @@
left:0;
}
+.webBox-overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+}
+
.webBox-button {
padding : 0vw;
border: none;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 1edb4d826..2239a8e38 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,23 +1,18 @@
import "./WebBox.scss";
import React = require("react");
-import { WebField } from '../../../fields/WebField';
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
+import { HtmlField } from "../../../new_fields/HtmlField";
+import { WebField } from "../../../new_fields/URLField";
import { observer } from "mobx-react";
-import { computed } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
+import { computed, reaction, IReactionDisposer } from 'mobx';
+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;
@@ -36,22 +31,29 @@ export class WebBox extends React.Component<FieldViewProps> {
}
}
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 field = this.props.Document[this.props.fieldKey];
+ let view;
+ if (field instanceof HtmlField) {
+ view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
+ } else if (field instanceof WebField) {
+ view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ } else {
+ view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ }
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%" }} />}
+ {view}
</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" >
+ <div className={classname} >
{content}
</div>
- {this.props.isSelected() ? (null) : <div onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} style={{ width: "100%", height: "100%", position: "absolute" }} />}
+ {!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/debug/Test.tsx b/src/debug/Test.tsx
index 11f2b0c4e..04ef00722 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -1,29 +1,80 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import JsxParser from 'react-jsx-parser';
+import { SerializationHelper } from '../client/util/SerializationHelper';
+import { createSchema, makeInterface, makeStrictInterface, listSpec } from '../new_fields/Schema';
+import { ImageField } from '../new_fields/URLField';
+import { Doc } from '../new_fields/Doc';
+import { List } from '../new_fields/List';
-class Hello extends React.Component<{ firstName: string, lastName: string }> {
- render() {
- return <div>Hello {this.props.firstName} {this.props.lastName}</div>;
- }
-}
+const schema1 = createSchema({
+ hello: "number",
+ test: "string",
+ fields: "boolean",
+ url: ImageField,
+ testDoc: Doc
+});
+
+type TestDoc = makeInterface<[typeof schema1]>;
+const TestDoc: (doc?: Doc) => TestDoc = makeInterface(schema1);
+
+const schema2 = createSchema({
+ hello: ImageField,
+ test: "boolean",
+ fields: listSpec("number"),
+ url: "number",
+ testDoc: ImageField
+});
+
+const Test2Doc = makeStrictInterface(schema2);
+type Test2Doc = makeStrictInterface<typeof schema2>;
+
+const assert = (bool: boolean) => {
+ if (!bool) throw new Error();
+};
class Test extends React.Component {
+ onClick = () => {
+ const url = new ImageField(new URL("http://google.com"));
+ const doc = new Doc();
+ const doc2 = new Doc();
+ doc.hello = 5;
+ doc.fields = "test";
+ doc.test = "hello doc";
+ doc.url = url;
+ doc.testDoc = doc2;
+
+
+ const test1: TestDoc = TestDoc(doc);
+ assert(test1.hello === 5);
+ assert(test1.fields === undefined);
+ assert(test1.test === "hello doc");
+ assert(test1.url === url);
+ assert(test1.testDoc === doc2);
+ test1.myField = 20;
+ assert(test1.myField === 20);
+
+ const test2: Test2Doc = Test2Doc(doc);
+ assert(test2.hello === undefined);
+ // assert(test2.fields === "test");
+ assert(test2.test === undefined);
+ assert(test2.url === undefined);
+ assert(test2.testDoc === undefined);
+ test2.url = 35;
+ assert(test2.url === 35);
+ const l = new List<Doc>();
+ //TODO push, and other array functions don't go through the proxy
+ l.push(doc2);
+ //TODO currently length, and any other string fields will get serialized
+ doc.list = l;
+ console.log(l.slice());
+ }
+
render() {
- let jsx = "<Hello {...props}/>";
- let bindings = {
- props: {
- firstName: "First",
- lastName: "Last"
- }
- };
- return <JsxParser jsx={jsx} bindings={bindings} components={{ Hello }}></JsxParser>;
+ return <button onClick={this.onClick}>Click me</button>;
}
}
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Test />
- </div>),
+ReactDOM.render(
+ <Test />,
document.getElementById('root')
); \ No newline at end of file
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 857da1ebb..4cac09dee 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -3,190 +3,184 @@ import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
-import { Document } from '../fields/Document';
-import { BasicField } from '../fields/BasicField';
-import { ListField } from '../fields/ListField';
-import { Key } from '../fields/Key';
-import { Opt, Field } from '../fields/Field';
-import { Server } from '../client/Server';
-
-configure({
- enforceActions: "observed"
-});
-
-@observer
-class FieldViewer extends React.Component<{ field: BasicField<any> }> {
- render() {
- return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>;
- }
-}
-
-@observer
-class KeyViewer extends React.Component<{ field: Key }> {
- render() {
- return this.props.field.Name;
- }
-}
-
-@observer
-class ListViewer extends React.Component<{ field: ListField<Field> }>{
- @observable
- expanded = false;
-
- render() {
- let content;
- if (this.expanded) {
- content = (
- <div>
- {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)}
- </div>
- );
- } else {
- content = <>[...] ({this.props.field.Id})</>;
- }
- return (
- <div>
- <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button>
- {content}
- </div >
- );
- }
-}
-
-@observer
-class DocumentViewer extends React.Component<{ field: Document }> {
- private keyMap: ObservableMap<string, Key> = new ObservableMap;
-
- private disposer?: Lambda;
-
- componentDidMount() {
- let f = () => {
- Array.from(this.props.field._proxies.keys()).forEach(id => {
- if (!this.keyMap.has(id)) {
- Server.GetField(id, (field) => {
- if (field && field instanceof Key) {
- this.keyMap.set(id, field);
- }
- });
- }
- });
- };
- this.disposer = this.props.field._proxies.observe(f);
- f();
- }
-
- componentWillUnmount() {
- if (this.disposer) {
- this.disposer();
- }
- }
-
- render() {
- let fields = Array.from(this.props.field._proxies.entries()).map(kv => {
- let key = this.keyMap.get(kv[0]);
- return (
- <div key={kv[0]}>
- <b>({key ? key.Name : kv[0]}): </b>
- <DebugViewer fieldId={kv[1]}></DebugViewer>
- </div>
- );
- });
- return (
- <div>
- Document ({this.props.field.Id})
- <div style={{ paddingLeft: "25px" }}>
- {fields}
- </div>
- </div>
- );
- }
-}
-
-@observer
-class DebugViewer extends React.Component<{ fieldId: string }> {
- @observable
- private field?: Field;
-
- @observable
- private error?: string;
-
- constructor(props: { fieldId: string }) {
- super(props);
- this.update();
- }
-
- update() {
- Server.GetField(this.props.fieldId, action((field: Opt<Field>) => {
- this.field = field;
- if (!field) {
- this.error = `Field with id ${this.props.fieldId} not found`;
- }
- }));
-
- }
-
- render() {
- let content;
- if (this.field) {
- // content = this.field.ToJson();
- if (this.field instanceof ListField) {
- content = (<ListViewer field={this.field} />);
- } else if (this.field instanceof Document) {
- content = (<DocumentViewer field={this.field} />);
- } else if (this.field instanceof BasicField) {
- content = (<FieldViewer field={this.field} />);
- } else if (this.field instanceof Key) {
- content = (<KeyViewer field={this.field} />);
- } else {
- content = (<span>Unrecognized field type</span>);
- }
- } else if (this.error) {
- content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>;
- } else {
- content = <span>Field loading: {this.props.fieldId}</span>;
- }
- return content;
- }
-}
-
-@observer
-class Viewer extends React.Component {
- @observable
- private idToAdd: string = '';
-
- @observable
- private ids: string[] = [];
-
- @action
- inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.idToAdd = e.target.value;
- }
-
- @action
- onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
- if (e.key === "Enter") {
- this.ids.push(this.idToAdd);
- this.idToAdd = "";
- }
- }
-
- render() {
- return (
- <>
- <input value={this.idToAdd}
- onChange={this.inputOnChange}
- onKeyDown={this.onKeyPress} />
- <div>
- {this.ids.map(id => <DebugViewer fieldId={id} key={id}></DebugViewer>)}
- </div>
- </>
- );
- }
-}
-
-ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <Viewer />
- </div>),
- document.getElementById('root')
-); \ No newline at end of file
+
+// configure({
+// enforceActions: "observed"
+// });
+
+// @observer
+// class FieldViewer extends React.Component<{ field: BasicField<any> }> {
+// render() {
+// return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>;
+// }
+// }
+
+// @observer
+// class KeyViewer extends React.Component<{ field: Key }> {
+// render() {
+// return this.props.field.Name;
+// }
+// }
+
+// @observer
+// class ListViewer extends React.Component<{ field: ListField<Field> }>{
+// @observable
+// expanded = false;
+
+// render() {
+// let content;
+// if (this.expanded) {
+// content = (
+// <div>
+// {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)}
+// </div>
+// );
+// } else {
+// content = <>[...] ({this.props.field.Id})</>;
+// }
+// return (
+// <div>
+// <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button>
+// {content}
+// </div >
+// );
+// }
+// }
+
+// @observer
+// class DocumentViewer extends React.Component<{ field: Document }> {
+// private keyMap: ObservableMap<string, Key> = new ObservableMap;
+
+// private disposer?: Lambda;
+
+// componentDidMount() {
+// let f = () => {
+// Array.from(this.props.field._proxies.keys()).forEach(id => {
+// if (!this.keyMap.has(id)) {
+// Server.GetField(id, (field) => {
+// if (field && field instanceof Key) {
+// this.keyMap.set(id, field);
+// }
+// });
+// }
+// });
+// };
+// this.disposer = this.props.field._proxies.observe(f);
+// f();
+// }
+
+// componentWillUnmount() {
+// if (this.disposer) {
+// this.disposer();
+// }
+// }
+
+// render() {
+// let fields = Array.from(this.props.field._proxies.entries()).map(kv => {
+// let key = this.keyMap.get(kv[0]);
+// return (
+// <div key={kv[0]}>
+// <b>({key ? key.Name : kv[0]}): </b>
+// <DebugViewer fieldId={kv[1]}></DebugViewer>
+// </div>
+// );
+// });
+// return (
+// <div>
+// Document ({this.props.field.Id})
+// <div style={{ paddingLeft: "25px" }}>
+// {fields}
+// </div>
+// </div>
+// );
+// }
+// }
+
+// @observer
+// class DebugViewer extends React.Component<{ fieldId: string }> {
+// @observable
+// private field?: Field;
+
+// @observable
+// private error?: string;
+
+// constructor(props: { fieldId: string }) {
+// super(props);
+// this.update();
+// }
+
+// update() {
+// Server.GetField(this.props.fieldId, action((field: Opt<Field>) => {
+// this.field = field;
+// if (!field) {
+// this.error = `Field with id ${this.props.fieldId} not found`;
+// }
+// }));
+
+// }
+
+// render() {
+// let content;
+// if (this.field) {
+// // content = this.field.ToJson();
+// if (this.field instanceof ListField) {
+// content = (<ListViewer field={this.field} />);
+// } else if (this.field instanceof Document) {
+// content = (<DocumentViewer field={this.field} />);
+// } else if (this.field instanceof BasicField) {
+// content = (<FieldViewer field={this.field} />);
+// } else if (this.field instanceof Key) {
+// content = (<KeyViewer field={this.field} />);
+// } else {
+// content = (<span>Unrecognized field type</span>);
+// }
+// } else if (this.error) {
+// content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>;
+// } else {
+// content = <span>Field loading: {this.props.fieldId}</span>;
+// }
+// return content;
+// }
+// }
+
+// @observer
+// class Viewer extends React.Component {
+// @observable
+// private idToAdd: string = '';
+
+// @observable
+// private ids: string[] = [];
+
+// @action
+// inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+// this.idToAdd = e.target.value;
+// }
+
+// @action
+// onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+// if (e.key === "Enter") {
+// this.ids.push(this.idToAdd);
+// this.idToAdd = "";
+// }
+// }
+
+// render() {
+// return (
+// <>
+// <input value={this.idToAdd}
+// onChange={this.inputOnChange}
+// onKeyDown={this.onKeyPress} />
+// <div>
+// {this.ids.map(id => <DebugViewer fieldId={id} key={id}></DebugViewer>)}
+// </div>
+// </>
+// );
+// }
+// }
+
+// ReactDOM.render((
+// <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+// <Viewer />
+// </div>),
+// document.getElementById('root')
+// ); \ No newline at end of file
diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts
deleted file mode 100644
index 87e47a715..000000000
--- a/src/fields/AudioField.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class AudioField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
-
- ToScriptString(): string {
- return `new AudioField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new AudioField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Audio,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
deleted file mode 100644
index 17b1fc4e8..000000000
--- a/src/fields/BasicField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Field, FieldId } from "./Field";
-import { observable, computed, action } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-
-export abstract class BasicField<T> extends Field {
- constructor(data: T, save: boolean, id?: FieldId) {
- super(id);
-
- this.data = data;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: any) {
- if (this.data !== data) {
- this.data = data;
- }
- }
-
- @observable
- protected data: T;
-
- @computed
- get Data(): T {
- return this.data;
- }
-
- set Data(value: T) {
- if (this.data === value) {
- return;
- }
- let oldValue = this.data;
- this.setData(value);
- UndoManager.AddEvent({
- undo: () => this.Data = oldValue,
- redo: () => this.Data = value
- });
- Server.UpdateField(this);
- }
-
- protected setData(value: T) {
- this.data = value;
- }
-
- @action
- TrySetValue(value: any): boolean {
- if (typeof value === typeof this.data) {
- this.Data = value;
- return true;
- }
- return false;
- }
-
- GetValue(): any {
- return this.Data;
- }
-}
diff --git a/src/fields/BooleanField.ts b/src/fields/BooleanField.ts
deleted file mode 100644
index d49bfe82b..000000000
--- a/src/fields/BooleanField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class BooleanField extends BasicField<boolean> {
- constructor(data: boolean = false as boolean, id?: FieldId, save: boolean = true as boolean) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new BooleanField("${this.Data}")`;
- }
-
- Copy() {
- return new BooleanField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Boolean,
- data: this.Data,
- id: this.Id
- };
- }
-}
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
deleted file mode 100644
index 7cf784f0e..000000000
--- a/src/fields/Document.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-import { Key } from "./Key";
-import { KeyStore } from "./KeyStore";
-import { Field, Cast, FieldWaiting, FieldValue, FieldId, Opt } from "./Field";
-import { NumberField } from "./NumberField";
-import { ObservableMap, computed, action, runInAction } from "mobx";
-import { TextField } from "./TextField";
-import { ListField } from "./ListField";
-import { Server } from "../client/Server";
-import { Types } from "../server/Message";
-import { UndoManager } from "../client/util/UndoManager";
-import { HtmlField } from "./HtmlField";
-import { BooleanField } from "./BooleanField";
-import { allLimit } from "async";
-import { prototype } from "nodemailer/lib/smtp-pool";
-import { HistogramField } from "../client/northstar/dash-fields/HistogramField";
-
-export class Document extends Field {
- //TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field
- public fields: ObservableMap<string, { key: Key; field: Field }> = new ObservableMap();
- public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
-
- constructor(id?: string, save: boolean = true) {
- super(id);
-
- if (save) {
- 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) {
- const element = data[key];
- this._proxies.set(element[0], element[1]);
- }
- }
-
- public Width = () => this.GetNumber(KeyStore.Width, 0);
- public Height = () => this.GetNumber(KeyStore.Height, this.GetNumber(KeyStore.NativeWidth, 0) ? (this.GetNumber(KeyStore.NativeHeight, 0) / this.GetNumber(KeyStore.NativeWidth, 0)) * this.GetNumber(KeyStore.Width, 0) : 0);
- public Scale = () => this.GetNumber(KeyStore.Scale, 1);
-
- @computed
- public get Title(): string {
- let title = this.Get(KeyStore.Title, true);
- 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 || parTitle === FieldWaiting) {
- if (parTitle !== FieldWaiting) return parTitle.Data + ".alias";
- else return "-waiting-.alias";
- }
- return "-untitled-";
- }
-
- @computed
- public get Fields() {
- return this.fields;
- }
-
- /**
- * Get the field in the document associated with the given key. If the
- * associated field has not yet been filled in from the server, a request
- * to the server will automatically be sent, the value will be filled in
- * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned.
- * @param key - The key of the value to get
- * @param ignoreProto - If true, ignore any prototype this document
- * might have and only search for the value on this immediate document.
- * If false (default), search up the prototype chain, starting at this document,
- * for a document that has a field associated with the given key, and return the first
- * one found.
- *
- * @returns If the document does not have a field associated with the given key, returns `undefined`.
- * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}.
- * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field.
- */
- Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> {
- let field: FieldValue<Field>;
- if (ignoreProto) {
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key);
- /*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else {
- field = FieldWaiting;
- }
- }
- } else {
- let doc: FieldValue<Document> = this;
- while (doc && field !== FieldWaiting) {
- let curField = doc.fields.get(key.Id);
- let curProxy = doc._proxies.get(key.Id);
- if (!curField || (curProxy && curField.field.Id !== curProxy)) {
- if (curProxy) {
- Server.GetDocumentField(doc, key);
- /*
- The field might have been instantly filled from the cache
- Maybe we want to just switch back to returning the value
- from Server.GetDocumentField if it's in the cache
- */
- if (this.fields.has(key.Id)) {
- field = this.fields.get(key.Id)!.field;
- } else {
- field = FieldWaiting;
- }
- break;
- }
- if (
- doc.fields.has(KeyStore.Prototype.Id) ||
- doc._proxies.has(KeyStore.Prototype.Id)
- ) {
- doc = doc.GetPrototype();
- } else {
- break;
- }
- } else {
- field = curField.field;
- break;
- }
- }
- if (doc === FieldWaiting) field = FieldWaiting;
- }
-
- return field;
- }
-
- /**
- * Tries to get the field associated with the given key, and if there is an
- * associated field, calls the given callback with that field.
- * @param key - The key of the value to get
- * @param callback - A function that will be called with the associated field, if it exists,
- * once it is fetched from the server (this may be immediately if the field has already been fetched).
- * Note: The callback will not be called if there is no associated field.
- * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise
- */
- GetAsync(key: Key, callback: (field: Opt<Field>) => void): void {
- //TODO: This currently doesn't deal with prototypes
- let field = this.fields.get(key.Id);
- if (field && field.field) {
- callback(field.field);
- } else if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key, callback);
- } else if (this._proxies.has(KeyStore.Prototype.Id)) {
- this.GetTAsync(KeyStore.Prototype, Document, proto => {
- if (proto) {
- proto.GetAsync(key, callback);
- } else {
- callback(undefined);
- }
- });
- } else {
- callback(undefined);
- }
- }
-
- GetTAsync<T extends Field>(key: Key, ctor: { new(): T }): Promise<Opt<T>>;
- GetTAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback: (field: Opt<T>) => void
- ): void;
- GetTAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback?: (field: Opt<T>) => void
- ): Promise<Opt<T>> | void {
- let fn = (cb: (field: Opt<T>) => void) => {
- return this.GetAsync(key, field => {
- cb(Cast(field, ctor));
- });
- };
- if (callback) {
- fn(callback);
- } else {
- return new Promise(fn);
- }
- }
-
- /**
- * Same as {@link Document#GetAsync}, except a field of the given type
- * will be created if there is no field associated with the given key,
- * or the field associated with the given key is not of the given type.
- * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc.
- */
- GetOrCreateAsync<T extends Field>(
- key: Key,
- ctor: { new(): T },
- callback: (field: T) => void
- ): void {
- //This currently doesn't deal with prototypes
- if (this._proxies.has(key.Id)) {
- Server.GetDocumentField(this, key, field => {
- if (field && field instanceof ctor) {
- callback(field);
- } else {
- let newField = new ctor();
- this.Set(key, newField);
- callback(newField);
- }
- });
- } else {
- let newField = new ctor();
- this.Set(key, newField);
- callback(newField);
- }
- }
-
- /**
- * Same as {@link Document#Get}, except that it will additionally
- * check if the field is of the given type.
- * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc.
- * @returns Same as {@link Document#Get}, except will return `undefined`
- * if there is an associated field but it is of the wrong type.
- */
- GetT<T extends Field = Field>(
- key: Key,
- ctor: { new(...args: any[]): T },
- ignoreProto: boolean = false
- ): FieldValue<T> {
- var getfield = this.Get(key, ignoreProto);
- if (getfield !== FieldWaiting) {
- return Cast(getfield, ctor);
- }
- return FieldWaiting;
- }
-
- GetOrCreate<T extends Field>(
- key: Key,
- ctor: { new(): T },
- ignoreProto: boolean = false
- ): T {
- const field = this.GetT(key, ctor, ignoreProto);
- if (field && field !== FieldWaiting) {
- return field;
- }
- const newField = new ctor();
- this.Set(key, newField);
- return newField;
- }
-
- GetData<T, U extends Field & { Data: T }>(
- key: Key,
- ctor: { new(): U },
- defaultVal: T
- ): T {
- let val = this.Get(key);
- let vval = val && val instanceof ctor ? val.Data : defaultVal;
- return vval;
- }
-
- GetHtml(key: Key, defaultVal: string): string {
- return this.GetData(key, HtmlField, defaultVal);
- }
-
- GetBoolean(key: Key, defaultVal: boolean): boolean {
- return this.GetData(key, BooleanField, defaultVal);
- }
-
- GetNumber(key: Key, defaultVal: number): number {
- return this.GetData(key, NumberField, defaultVal);
- }
-
- GetText(key: Key, defaultVal: string): string {
- return this.GetData(key, TextField, defaultVal);
- }
-
- GetList<T extends Field>(key: Key, defaultVal: T[]): T[] {
- return this.GetData<T[], ListField<T>>(key, ListField, defaultVal);
- }
-
- @action
- Set(key: Key, field: Field | undefined, setOnPrototype = false): void {
- let old = this.fields.get(key.Id);
- let oldField = old ? old.field : undefined;
- if (setOnPrototype) {
- this.SetOnPrototype(key, field);
- } else {
- if (field) {
- this.fields.set(key.Id, { key, field });
- this._proxies.set(key.Id, field.Id);
- // Server.AddDocumentField(this, key, field);
- } else {
- this.fields.delete(key.Id);
- this._proxies.delete(key.Id);
- // Server.DeleteDocumentField(this, key);
- }
- Server.UpdateField(this);
- }
- if (oldField || field) {
- UndoManager.AddEvent({
- undo: () => this.Set(key, oldField, setOnPrototype),
- redo: () => this.Set(key, field, setOnPrototype)
- });
- }
- }
-
- @action
- SetOnPrototype(key: Key, field: Field | undefined): void {
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && f.Set(key, field);
- });
- }
-
- @action
- SetDataOnPrototype<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }, replaceWrongType = true) {
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && f.SetData(key, value, ctor, replaceWrongType);
- });
- }
-
- @action
- SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(data: T): U }, replaceWrongType = true) {
- let field = this.Get(key, true);
- if (field instanceof ctor) {
- field.Data = value;
- } else if (!field || replaceWrongType) {
- let newField = new ctor(value);
- // newField.Data = value;
- this.Set(key, newField);
- }
- }
-
- @action
- SetText(key: Key, value: string, replaceWrongType = true) {
- this.SetData(key, value, TextField, replaceWrongType);
- }
- @action
- SetBoolean(key: Key, value: boolean, replaceWrongType = true) {
- this.SetData(key, value, BooleanField, replaceWrongType);
- }
- @action
- SetNumber(key: Key, value: number, replaceWrongType = true) {
- this.SetData(key, value, NumberField, replaceWrongType);
- }
-
- GetPrototype(): FieldValue<Document> {
- return this.GetT(KeyStore.Prototype, Document, true);
- }
-
- GetAllPrototypes(): Document[] {
- let protos: Document[] = [];
- let doc: FieldValue<Document> = this;
- while (doc && doc !== FieldWaiting) {
- protos.push(doc);
- doc = doc.GetPrototype();
- }
- return protos;
- }
-
- CreateAlias(id?: string): Document {
- let alias = new Document(id);
- this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
- f && alias.Set(KeyStore.Prototype, f);
- });
-
- return alias;
- }
-
- MakeDelegate(id?: string): Document {
- let delegate = new Document(id);
-
- delegate.Set(KeyStore.Prototype, this);
-
- return delegate;
- }
-
- ToScriptString(): string {
- return "";
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
- GetValue() {
- return this.Title;
- var title = (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) + "(" + this.Id + ")";
- return title;
- //throw new Error("Method not implemented.");
- }
- Copy(copyProto?: boolean, id?: string): Field {
- let copy = new Document();
- this._proxies.forEach((fieldid, keyid) => { // copy each prototype field
- let key = KeyStore.KeyLookup(keyid);
- if (key) {
- this.GetAsync(key, (field: Opt<Field>) => {
- if (key === KeyStore.Prototype && copyProto) { // handle prototype field specially
- if (field instanceof Document) {
- copy.Set(key, field.Copy(false)); // only copying one level of prototypes for now...
- }
- }
- else
- if (field instanceof Document) { // ... TODO bcz: should we copy documents or reference them
- copy.Set(key!, field);
- }
- else if (field) {
- copy.Set(key!, field.Copy());
- }
- });
- }
- });
- return copy;
- }
-
- ToJson() {
- let fields: [string, string][] = [];
- this._proxies.forEach((field, key) =>
- field && fields.push([key, field]));
-
- return {
- type: Types.Document,
- data: fields,
- id: this.Id
- };
- }
-}
diff --git a/src/fields/DocumentReference.ts b/src/fields/DocumentReference.ts
deleted file mode 100644
index 303754177..000000000
--- a/src/fields/DocumentReference.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Field, Opt, FieldValue, FieldId } from "./Field";
-import { Document } from "./Document";
-import { Key } from "./Key";
-import { Types } from "../server/Message";
-import { ObjectID } from "bson";
-
-export class DocumentReference extends Field {
- get Key(): Key {
- return this.key;
- }
-
- get Document(): Document {
- return this.document;
- }
-
- constructor(private document: Document, private key: Key) {
- super();
- }
-
- UpdateFromServer() {
-
- }
-
- Dereference(): FieldValue<Field> {
- return this.document.Get(this.key);
- }
-
- DereferenceToRoot(): FieldValue<Field> {
- let field: FieldValue<Field> = this;
- while (field instanceof DocumentReference) {
- field = field.Dereference();
- }
- return field;
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
- GetValue() {
- throw new Error("Method not implemented.");
- }
- Copy(): Field {
- throw new Error("Method not implemented.");
- }
-
- ToScriptString(): string {
- return "";
- }
-
- ToJson() {
- return {
- type: Types.DocumentReference,
- data: this.document.Id,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/Field.ts b/src/fields/Field.ts
deleted file mode 100644
index 3b3e95c2b..000000000
--- a/src/fields/Field.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-
-import { Utils } from "../Utils";
-import { Types, Transferable } from "../server/Message";
-import { computed } from "mobx";
-
-export function Cast<T extends Field>(field: FieldValue<Field>, ctor: { new(): T }): Opt<T> {
- if (field) {
- if (ctor && field instanceof ctor) {
- return field;
- }
- }
- return undefined;
-}
-
-export const FieldWaiting: FIELD_WAITING = null;
-export type FIELD_WAITING = null;
-export type FieldId = string;
-export type Opt<T> = T | undefined;
-export type FieldValue<T> = Opt<T> | FIELD_WAITING;
-
-export abstract class Field {
- //FieldUpdated: TypedEvent<Opt<FieldUpdatedArgs>> = new TypedEvent<Opt<FieldUpdatedArgs>>();
-
- init(callback: (res: Field) => any) {
- callback(this);
- }
-
- private id: FieldId;
-
- @computed
- get Id(): FieldId {
- return this.id;
- }
-
- constructor(id: Opt<FieldId> = undefined) {
- this.id = id || Utils.GenerateGuid();
- }
-
- Dereference(): FieldValue<Field> {
- return this;
- }
- DereferenceToRoot(): FieldValue<Field> {
- return this;
- }
-
- DereferenceT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.Dereference(), ctor);
- }
-
- DereferenceToRootT<T extends Field = Field>(ctor: { new(): T }): FieldValue<T> {
- return Cast(this.DereferenceToRoot(), ctor);
- }
-
- Equals(other: Field): boolean {
- return this.id === other.id;
- }
-
- abstract UpdateFromServer(serverData: any): void;
-
- abstract ToScriptString(): string;
-
- abstract TrySetValue(value: any): boolean;
-
- abstract GetValue(): any;
-
- abstract Copy(): Field;
-
- abstract ToJson(): Transferable;
-} \ No newline at end of file
diff --git a/src/fields/FieldUpdatedArgs.ts b/src/fields/FieldUpdatedArgs.ts
deleted file mode 100644
index 23ccf2a5a..000000000
--- a/src/fields/FieldUpdatedArgs.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Field, Opt } from "./Field";
-import { Document } from "./Document";
-import { Key } from "./Key";
-
-export enum FieldUpdatedAction {
- Add,
- Remove,
- Replace,
- Update
-}
-
-export interface FieldUpdatedArgs {
- field: Field;
- action: FieldUpdatedAction;
-}
-
-export interface DocumentUpdatedArgs {
- field: Document;
- key: Key;
-
- oldValue: Opt<Field>;
- newValue: Opt<Field>;
-
- fieldArgs?: FieldUpdatedArgs;
-
- action: FieldUpdatedAction;
-}
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
deleted file mode 100644
index a1d880070..000000000
--- a/src/fields/HtmlField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class HtmlField extends BasicField<string> {
- constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new HtmlField("${this.Data}")`;
- }
-
- Copy() {
- return new HtmlField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Html,
- data: this.Data,
- id: this.Id,
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts
deleted file mode 100644
index bce20f242..000000000
--- a/src/fields/ImageField.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class ImageField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new ImageField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new ImageField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Image,
- data: this.Data.href,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
deleted file mode 100644
index 2eacd7d0c..000000000
--- a/src/fields/InkField.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-import { observable, ObservableMap } from "mobx";
-
-export enum InkTool {
- None,
- Pen,
- Highlighter,
- Eraser
-}
-export interface StrokeData {
- pathData: Array<{ x: number, y: number }>;
- color: string;
- width: string;
- tool: InkTool;
- page: number;
-}
-export type StrokeMap = Map<string, StrokeData>;
-
-export class InkField extends BasicField<StrokeMap> {
- constructor(data: StrokeMap = new Map, id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new InkField("${this.Data}")`;
- }
-
- Copy() {
- return new InkField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Ink,
- data: this.Data,
- id: this.Id,
- };
- }
-
- UpdateFromServer(data: any) {
- this.data = new ObservableMap(data);
- }
-
- static FromJson(id: string, data: any): InkField {
- let map: StrokeMap = new Map<string, StrokeData>();
- Object.keys(data).forEach(key => {
- map.set(key, data[key]);
- });
- return new InkField(map, id, false);
- }
-} \ No newline at end of file
diff --git a/src/fields/Key.ts b/src/fields/Key.ts
deleted file mode 100644
index 57e2dadf0..000000000
--- a/src/fields/Key.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Field, FieldId } from "./Field";
-import { Utils } from "../Utils";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-import { Server } from "../client/Server";
-
-export class Key extends Field {
- private name: string;
-
- get Name(): string {
- return this.name;
- }
-
- constructor(name: string, id?: string, save: boolean = true) {
- super(id || Utils.GenerateDeterministicGuid(name));
-
- this.name = name;
- if (save) {
- Server.UpdateField(this);
- }
- }
-
- UpdateFromServer(data: string) {
- this.name = data;
- }
-
- TrySetValue(value: any): boolean {
- throw new Error("Method not implemented.");
- }
-
- GetValue() {
- return this.Name;
- }
-
- Copy(): Field {
- return this;
- }
-
- ToScriptString(): string {
- return name;
- }
-
- ToJson() {
- return {
- type: Types.Key,
- data: this.name,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
deleted file mode 100644
index 16a909eb8..000000000
--- a/src/fields/KeyStore.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Key } from "./Key";
-
-export namespace KeyStore {
- export const Prototype = new Key("Prototype");
- export const X = new Key("X");
- export const Y = new Key("Y");
- export const Page = new Key("Page");
- export const Title = new Key("Title");
- export const Author = new Key("Author");
- export const PanX = new Key("PanX");
- export const PanY = new Key("PanY");
- export const Scale = new Key("Scale");
- export const NativeWidth = new Key("NativeWidth");
- export const NativeHeight = new Key("NativeHeight");
- export const Width = new Key("Width");
- export const Height = new Key("Height");
- export const ZIndex = new Key("ZIndex");
- export const Zoom = new Key("Zoom");
- export const Data = new Key("Data");
- export const Annotations = new Key("Annotations");
- export const ViewType = new Key("ViewType");
- export const Layout = new Key("Layout");
- export const BackgroundColor = new Key("BackgroundColor");
- export const BackgroundLayout = new Key("BackgroundLayout");
- export const OverlayLayout = new Key("OverlayLayout");
- export const LayoutKeys = new Key("LayoutKeys");
- export const LayoutFields = new Key("LayoutFields");
- export const ColumnsKey = new Key("SchemaColumns");
- export const SchemaSplitPercentage = new Key("SchemaSplitPercentage");
- export const Caption = new Key("Caption");
- export const ActiveWorkspace = new Key("ActiveWorkspace");
- export const DocumentText = new Key("DocumentText");
- export const BrushingDocs = new Key("BrushingDocs");
- export const LinkedToDocs = new Key("LinkedToDocs");
- export const LinkedFromDocs = new Key("LinkedFromDocs");
- export const LinkDescription = new Key("LinkDescription");
- export const LinkTags = new Key("LinkTag");
- export const Thumbnail = new Key("Thumbnail");
- export const ThumbnailPage = new Key("ThumbnailPage");
- export const CurPage = new Key("CurPage");
- export const AnnotationOn = new Key("AnnotationOn");
- export const NumPages = new Key("NumPages");
- export const Ink = new Key("Ink");
- export const Cursors = new Key("Cursors");
- export const OptionalRightCollection = new Key("OptionalRightCollection");
- export const Archives = new Key("Archives");
- export const Workspaces = new Key("Workspaces");
- export const Minimized = new Key("Minimized");
- export const CopyDraggedItems = new Key("CopyDraggedItems");
-
- export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight,
- Width, Height, ZIndex, Zoom, 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
- ];
- export function KeyLookup(keyid: string) {
- for (const key of KeyList) {
- if (key.Id === keyid) {
- return key;
- }
- }
- return undefined;
- }
-}
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
deleted file mode 100644
index e24099126..000000000
--- a/src/fields/ListField.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { FieldMap } from "../client/SocketStub";
-import { ScriptField } from "./ScriptField";
-
-export class ListField<T extends Field> extends BasicField<T[]> {
- private _proxies: string[] = [];
- private _scriptIds: string[] = [];
- private scripts: ScriptField[] = [];
-
- constructor(data: T[] = [], scripts: ScriptField[] = [], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- this.scripts = scripts;
- this.updateProxies();
- this._scriptIds = this.scripts.map(script => script.Id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeList();
- }
-
- private _processingServerUpdate: boolean = false;
-
- private observeDisposer: Lambda | undefined;
- private observeList(): void {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
- const target = change.object;
- this.updateProxies();
- if (change.type === "splice") {
- this.runScripts(change.removed, false);
- UndoManager.AddEvent({
- undo: () => target.splice(change.index, change.addedCount, ...change.removed),
- redo: () => target.splice(change.index, change.removedCount, ...change.added)
- });
- this.runScripts(change.added, true);
- } else {
- this.runScripts([change.oldValue], false);
- UndoManager.AddEvent({
- undo: () => target[change.index] = change.oldValue,
- redo: () => target[change.index] = change.newValue
- });
- this.runScripts([change.newValue], true);
- }
- if (!this._processingServerUpdate) {
- Server.UpdateField(this);
- }
- });
- }
-
- private runScripts(fields: T[], added: boolean) {
- for (const script of this.scripts) {
- this.runScript(fields, script, added);
- }
- }
-
- private runScript(fields: T[], script: ScriptField, added: boolean) {
- if (!this._processingServerUpdate) {
- for (const field of fields) {
- script.script.run({ field, added });
- }
- }
- }
-
- addScript(script: ScriptField) {
- this.scripts.push(script);
- this._scriptIds.push(script.Id);
-
- this.runScript(this.Data, script, true);
- UndoManager.AddEvent({
- undo: () => this.removeScript(script),
- redo: () => this.addScript(script),
- });
- Server.UpdateField(this);
- }
-
- removeScript(script: ScriptField) {
- const index = this.scripts.indexOf(script);
- if (index === -1) {
- return;
- }
- this.scripts.splice(index, 1);
- this._scriptIds.splice(index, 1);
- UndoManager.AddEvent({
- undo: () => this.addScript(script),
- redo: () => this.removeScript(script),
- });
- this.runScript(this.Data, script, false);
- Server.UpdateField(this);
- }
-
- protected setData(value: T[]) {
- this.runScripts(this.data, false);
-
- this.data = observable(value);
- this.updateProxies();
- this.observeList();
- this.runScripts(this.data, true);
- }
-
- private updateProxies() {
- this._proxies = this.Data.map(field => field.Id);
- }
-
- private arraysEqual(a: any[], b: any[]) {
- if (a === b) return true;
- if (a === null || b === null) return false;
- if (a.length !== b.length) return false;
-
- // If you don't care about the order of the elements inside
- // the array, you should sort both arrays here.
- // Please note that calling sort on an array will modify that array.
- // you might want to clone your array first.
-
- for (var i = 0; i < a.length; ++i) {
- if (a[i] !== b[i]) return false;
- }
- return true;
- }
-
- init(callback: (field: Field) => any) {
- const fieldsPromise = Server.GetFields(this._proxies).then(action((fields: FieldMap) => {
- if (!this.arraysEqual(this._proxies, this.data.map(field => field.Id))) {
- var dataids = this.data.map(d => d.Id);
- var proxies = this._proxies.map(p => p);
- var added = this.data.length < this._proxies.length;
- var deleted = this.data.length > this._proxies.length;
- for (let i = 0; i < dataids.length && added; i++) {
- added = proxies.indexOf(dataids[i]) !== -1;
- }
- for (let i = 0; i < this._proxies.length && deleted; i++) {
- deleted = dataids.indexOf(proxies[i]) !== -1;
- }
-
- this._processingServerUpdate = true;
- for (let i = 0; i < proxies.length && added; i++) {
- if (dataids.indexOf(proxies[i]) === -1) {
- this.Data.splice(i, 0, fields[proxies[i]] as T);
- }
- }
- for (let i = dataids.length - 1; i >= 0 && deleted; i--) {
- if (proxies.indexOf(dataids[i]) === -1) {
- this.Data.splice(i, 1);
- }
- }
- if (!added && !deleted) {// otherwise, just rebuild the whole list
- this.setData(proxies.map(id => fields[id] as T));
- }
- this._processingServerUpdate = false;
- }
- }));
-
- const scriptsPromise = Server.GetFields(this._scriptIds).then((fields: FieldMap) => {
- this.scripts = this._scriptIds.map(id => fields[id] as ScriptField);
- });
-
- Promise.all([fieldsPromise, scriptsPromise]).then(() => callback(this));
- }
-
- ToScriptString(): string {
- return "new ListField([" + this.Data.map(field => field.ToScriptString()).join(", ") + "])";
- }
-
- Copy(): Field {
- return new ListField<T>(this.Data);
- }
-
-
- UpdateFromServer(data: { fields: string[], scripts: string[] }) {
- this._proxies = data.fields;
- this._scriptIds = data.scripts;
- }
- ToJson() {
- return {
- type: Types.List,
- data: {
- fields: this._proxies,
- scripts: this._scriptIds,
- },
- id: this.Id
- };
- }
-
- static FromJson(id: string, data: { fields: string[], scripts: string[] }): ListField<Field> {
- let list = new ListField([], [], id, false);
- list._proxies = data.fields;
- list._scriptIds = data.scripts;
- return list;
- }
-} \ No newline at end of file
diff --git a/src/fields/NumberField.ts b/src/fields/NumberField.ts
deleted file mode 100644
index 7eea360c0..000000000
--- a/src/fields/NumberField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class NumberField extends BasicField<number> {
- constructor(data: number = 0, id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new NumberField(${this.Data})`;
- }
-
- Copy() {
- return new NumberField(this.Data);
- }
-
- ToJson() {
- return {
- id: this.Id,
- type: Types.Number,
- data: this.Data
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts
deleted file mode 100644
index 718a1a4c0..000000000
--- a/src/fields/PDFField.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { observable } from "mobx";
-import { Types } from "../server/Message";
-
-
-
-export class PDFField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- Copy(): Field {
- return new PDFField(this.Data);
- }
-
- ToScriptString(): string {
- return `new PDFField("${this.Data}")`;
- }
-
- ToJson() {
- return {
- type: Types.PDF,
- data: this.Data.href,
- id: this.Id
- };
- }
-
- @observable
- Page: Number = 1;
-
-} \ No newline at end of file
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
deleted file mode 100644
index f53f48ca6..000000000
--- a/src/fields/RichTextField.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Types } from "../server/Message";
-import { FieldId } from "./Field";
-
-export class RichTextField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new RichTextField(${this.Data})`;
- }
-
- Copy() {
- return new RichTextField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.RichText,
- data: this.Data,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 7f87be45d..ae532c9e2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,101 +1,101 @@
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-import { CompileScript, ScriptOptions, CompiledScript } from "../client/util/Scripting";
-import { Server } from "../client/Server";
-import { Without } from "../Utils";
+// import { 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 SerializableOptions extends Without<ScriptOptions, "capturedVariables"> {
+// capturedIds: { [id: string]: string };
+// }
-export interface ScriptData {
- script: string;
- options: SerializableOptions;
-}
+// export interface ScriptData {
+// script: string;
+// options: SerializableOptions;
+// }
-export class ScriptField extends Field {
- private _script?: CompiledScript;
- get script(): CompiledScript {
- return this._script!;
- }
- private options?: ScriptData;
+// export class ScriptField extends Field {
+// private _script?: CompiledScript;
+// get script(): CompiledScript {
+// return this._script!;
+// }
+// private options?: ScriptData;
- constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
- super(id);
+// constructor(script?: CompiledScript, id?: FieldId, save: boolean = true) {
+// super(id);
- this._script = script;
+// this._script = script;
- if (save) {
- Server.UpdateField(this);
- }
- }
+// if (save) {
+// Server.UpdateField(this);
+// }
+// }
- ToScriptString() {
- return "new ScriptField(...)";
- }
+// ToScriptString() {
+// return "new ScriptField(...)";
+// }
- GetValue() {
- return this.script;
- }
+// GetValue() {
+// return this.script;
+// }
- TrySetValue(): boolean {
- throw new Error("Script fields currently can't be modified");
- }
+// TrySetValue(): boolean {
+// throw new Error("Script fields currently can't be modified");
+// }
- UpdateFromServer() {
- throw new Error("Script fields currently can't be updated");
- }
+// UpdateFromServer() {
+// throw new Error("Script fields currently can't be updated");
+// }
- static FromJson(id: string, data: ScriptData): ScriptField {
- let field = new ScriptField(undefined, id, false);
- field.options = data;
- return field;
- }
+// 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);
- });
- }
+// init(callback: (res: Field) => any) {
+// const options = this.options!;
+// const keys = Object.keys(options.options.capturedIds);
+// Server.GetFields(keys).then(fields => {
+// let captured: { [name: string]: Field } = {};
+// keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
+// const opts: ScriptOptions = {
+// addReturn: options.options.addReturn,
+// params: options.options.params,
+// requiredType: options.options.requiredType,
+// capturedVariables: captured
+// };
+// const script = CompileScript(options.script, opts);
+// if (!script.compiled) {
+// throw new Error("Can't compile script");
+// }
+// this._script = script;
+// callback(this);
+// });
+// }
- ToJson() {
- const { options, originalScript } = this.script;
- let capturedIds: { [id: string]: string } = {};
- for (const capt in options.capturedVariables) {
- capturedIds[options.capturedVariables[capt].Id] = capt;
- }
- const opts: SerializableOptions = {
- ...options,
- capturedIds
- };
- delete (opts as any).capturedVariables;
- return {
- id: this.Id,
- type: Types.Script,
- data: {
- script: originalScript,
- options: opts,
- },
- };
- }
+// ToJson() {
+// const { options, originalScript } = this.script;
+// let capturedIds: { [id: string]: string } = {};
+// for (const capt in options.capturedVariables) {
+// capturedIds[options.capturedVariables[capt].Id] = capt;
+// }
+// const opts: SerializableOptions = {
+// ...options,
+// capturedIds
+// };
+// delete (opts as any).capturedVariables;
+// return {
+// id: this.Id,
+// type: Types.Script,
+// data: {
+// script: originalScript,
+// options: opts,
+// },
+// };
+// }
- Copy(): Field {
- //Script fields are currently immutable, so we can fake copy them
- return this;
- }
-} \ No newline at end of file
+// Copy(): Field {
+// //Script fields are currently immutable, so we can fake copy them
+// return this;
+// }
+// } \ No newline at end of file
diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts
deleted file mode 100644
index ddedec9b1..000000000
--- a/src/fields/TextField.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BasicField } from "./BasicField";
-import { FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class TextField extends BasicField<string> {
- constructor(data: string = "", id?: FieldId, save: boolean = true) {
- super(data, save, id);
- }
-
- ToScriptString(): string {
- return `new TextField("${this.Data}")`;
- }
-
- Copy() {
- return new TextField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Text,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
deleted file mode 100644
index 347f1fa05..000000000
--- a/src/fields/TupleField.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
-import { Server } from "../client/Server";
-import { UndoManager } from "../client/util/UndoManager";
-import { Types } from "../server/Message";
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-
-export class TupleField<T, U> extends BasicField<[T, U]> {
- constructor(data: [T, U], id?: FieldId, save: boolean = true) {
- super(data, save, id);
- if (save) {
- Server.UpdateField(this);
- }
- this.observeTuple();
- }
-
- private observeDisposer: Lambda | undefined;
- private observeTuple(): void {
- this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
- if (change.type === "update") {
- UndoManager.AddEvent({
- undo: () => this.Data[change.index] = change.oldValue,
- redo: () => this.Data[change.index] = change.newValue
- });
- Server.UpdateField(this);
- } else {
- throw new Error("Why are you messing with the length of a tuple, huh?");
- }
- });
- }
-
- protected setData(value: [T, U]) {
- if (this.observeDisposer) {
- this.observeDisposer();
- }
- this.data = observable(value) as (T | U)[] as [T, U];
- this.observeTuple();
- }
-
- UpdateFromServer(values: [T, U]) {
- this.setData(values);
- }
-
- ToScriptString(): string {
- return `new TupleField([${this.Data[0], this.Data[1]}])`;
- }
-
- Copy(): Field {
- return new TupleField<T, U>(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Tuple,
- data: this.Data,
- id: this.Id
- };
- }
-} \ No newline at end of file
diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts
deleted file mode 100644
index 838b811b1..000000000
--- a/src/fields/VideoField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class VideoField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new VideoField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new VideoField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Video,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/fields/WebField.ts b/src/fields/WebField.ts
deleted file mode 100644
index 8b276a552..000000000
--- a/src/fields/WebField.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BasicField } from "./BasicField";
-import { Field, FieldId } from "./Field";
-import { Types } from "../server/Message";
-
-export class WebField extends BasicField<URL> {
- constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) {
- super(data === undefined ? new URL("https://crossorigin.me/" + "https://cs.brown.edu/") : data, save, id);
- }
-
- toString(): string {
- return this.Data.href;
- }
-
- ToScriptString(): string {
- return `new WebField("${this.Data}")`;
- }
-
- Copy(): Field {
- return new WebField(this.Data);
- }
-
- ToJson() {
- return {
- type: Types.Web,
- data: this.Data.href,
- id: this.Id
- };
- }
-
-} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index ec89a1194..1f9e160ce 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,15 +1,14 @@
import * as ReactDOM from 'react-dom';
import * as rp from 'request-promise';
-import { Documents } from '../client/documents/Documents';
-import { Server } from '../client/Server';
-import { Document } from '../fields/Document';
-import { KeyStore } from '../fields/KeyStore';
-import { ListField } from '../fields/ListField';
+import { Docs } from '../client/documents/Documents';
import { RouteStore } from '../server/RouteStore';
-import { ServerUtils } from '../server/ServerUtil';
import "./ImageUpload.scss";
import React = require('react');
-import { Opt } from '../fields/Field';
+import { DocServer } from '../client/DocServer';
+import { Opt, Doc } from '../new_fields/Doc';
+import { Cast } from '../new_fields/Types';
+import { listSpec } from '../new_fields/Schema';
+import { List } from '../new_fields/List';
@@ -38,21 +37,24 @@ const onFileLoad = async (file: any) => {
const json = await res.json();
json.map(async (file: any) => {
let path = window.location.origin + file;
- var doc: Document = Documents.ImageDocument(path, { nativeWidth: 200, width: 200 });
+ var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200 });
- const res = await rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId));
if (!res) {
throw new Error("No user id returned");
}
- const field = await Server.GetField(res);
- let pending: Opt<Document>;
- if (field instanceof Document) {
- pending = await field.GetTAsync(KeyStore.OptionalRightCollection, Document);
+ const field = await DocServer.GetRefField(res);
+ let pending: Opt<Doc>;
+ if (field instanceof Doc) {
+ pending = await Cast(field.optionalRightCollection, Doc);
}
if (pending) {
- pending.GetOrCreateAsync(KeyStore.Data, ListField, list => {
- list.Data.push(doc);
- });
+ const data = await Cast(pending.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ pending.data = new List([doc]);
+ }
}
});
diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts
new file mode 100644
index 000000000..7fd326a5f
--- /dev/null
+++ b/src/new_fields/CursorField.ts
@@ -0,0 +1,55 @@
+import { ObjectField, Copy, OnUpdate } from "./ObjectField";
+import { observable } from "mobx";
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, object } from "serializr";
+
+export type CursorPosition = {
+ x: number,
+ y: number
+}
+
+export type CursorMetadata = {
+ id: string,
+ identifier: string
+}
+
+export type CursorData = {
+ metadata: CursorMetadata,
+ position: CursorPosition
+}
+
+const PositionSchema = createSimpleSchema({
+ x: true,
+ y: true
+});
+
+const MetadataSchema = createSimpleSchema({
+ id: true,
+ identifier: true
+});
+
+const CursorSchema = createSimpleSchema({
+ metadata: object(MetadataSchema),
+ position: object(PositionSchema)
+});
+
+@Deserializable("cursor")
+export default class CursorField extends ObjectField {
+
+ @serializable(object(CursorSchema))
+ readonly data: CursorData;
+
+ constructor(data: CursorData) {
+ super();
+ this.data = data;
+ }
+
+ setPosition(position: CursorPosition) {
+ this.data.position = position;
+ this[OnUpdate]();
+ }
+
+ [Copy]() {
+ return new CursorField(this.data);
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
new file mode 100644
index 000000000..c0a79f267
--- /dev/null
+++ b/src/new_fields/DateField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, date } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("date")
+export class DateField extends ObjectField {
+ @serializable(date())
+ readonly date: Date;
+
+ constructor(date: Date = new Date()) {
+ super();
+ this.date = date;
+ }
+
+ [Copy]() {
+ return new DateField(this.date);
+ }
+}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
new file mode 100644
index 000000000..3ccc06d44
--- /dev/null
+++ b/src/new_fields/Doc.ts
@@ -0,0 +1,228 @@
+import { observable, action } from "mobx";
+import { serializable, primitive, map, alias, list } from "serializr";
+import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
+import { DocServer } from "../client/DocServer";
+import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { UndoManager, undoBatch } from "../client/util/UndoManager";
+import { listSpec } from "./Schema";
+import { List } from "./List";
+import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { RefField, FieldId, Id, HandleUpdate } from "./RefField";
+import { Docs } from "../client/documents/Documents";
+
+export function IsField(field: any): field is Field {
+ return (typeof field === "string")
+ || (typeof field === "number")
+ || (typeof field === "boolean")
+ || (field instanceof ObjectField)
+ || (field instanceof RefField);
+}
+export type Field = number | string | boolean | ObjectField | RefField;
+export type Opt<T> = T | undefined;
+export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? never : Promise<T | undefined>;
+export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
+
+export const Update = Symbol("Update");
+export const Self = Symbol("Self");
+export const SelfProxy = Symbol("SelfProxy");
+export const WidthSym = Symbol("Width");
+export const HeightSym = Symbol("Height");
+
+export function DocListCast(field: FieldResult): Promise<Doc[] | undefined>;
+export function DocListCast(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
+export function DocListCast(field: FieldResult, defaultValue?: Doc[]) {
+ const list = Cast(field, listSpec(Doc));
+ return list ? Promise.all(list) : Promise.resolve(defaultValue);
+}
+
+@Deserializable("doc").withFields(["id"])
+export class Doc extends RefField {
+ constructor(id?: FieldId, forceSave?: boolean) {
+ super(id);
+ const doc = new Proxy<this>(this, {
+ set: setter,
+ get: getter,
+ has: (target, key) => key in target.__fields,
+ ownKeys: target => Object.keys(target.__fields),
+ getOwnPropertyDescriptor: (target, prop) => {
+ if (prop in target.__fields) {
+ return {
+ configurable: true,//TODO Should configurable be true?
+ enumerable: true,
+ };
+ }
+ return Reflect.getOwnPropertyDescriptor(target, prop);
+ },
+ deleteProperty: deleteProperty,
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ this[SelfProxy] = doc;
+ if (!id || forceSave) {
+ DocServer.CreateField(doc);
+ }
+ return doc;
+ }
+
+ proto: Opt<Doc>;
+ [key: string]: FieldResult;
+
+ @serializable(alias("fields", map(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ field[Parent] = this[Self];
+ field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
+ }
+
+ @observable
+ //{ [key: string]: Field | FieldWaiting | undefined }
+ private ___fields: any = {};
+
+ private [Update] = (diff: any) => {
+ DocServer.UpdateField(this[Id], diff);
+ }
+
+ private [Self] = this;
+ private [SelfProxy]: any;
+ public [WidthSym] = () => NumCast(this.__fields.width); // bcz: is this the right way to access width/height? it didn't work with : this.width
+ public [HeightSym] = () => NumCast(this.__fields.height);
+
+ public [HandleUpdate](diff: any) {
+ const set = diff.$set;
+ if (set) {
+ for (const key in set) {
+ if (!key.startsWith("fields.")) {
+ continue;
+ }
+ const value = SerializationHelper.Deserialize(set[key]);
+ const fKey = key.substring(7);
+ this[fKey] = value;
+ }
+ }
+ }
+}
+
+export namespace Doc {
+ // export function GetAsync(doc: Doc, key: string, ignoreProto: boolean = false): Promise<Field | undefined> {
+ // const self = doc[Self];
+ // return new Promise(res => getField(self, key, ignoreProto, res));
+ // }
+ // export function GetTAsync<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): Promise<T | undefined> {
+ // return new Promise(async res => {
+ // const field = await GetAsync(doc, key, ignoreProto);
+ // return Cast(field, ctor);
+ // });
+ // }
+ export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
+ const self = doc[Self];
+ return getField(self, key, ignoreProto);
+ }
+ export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): T | null | undefined {
+ return Cast(Get(doc, key, ignoreProto), ctor) as T | null | undefined;
+ }
+ export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
+ const proto = doc.proto;
+ if (proto) {
+ proto[key] = value;
+ }
+ }
+ export function GetAllPrototypes(doc: Doc): Doc[] {
+ const protos: Doc[] = [];
+ let d: Opt<Doc> = doc;
+ while (d) {
+ protos.push(d);
+ d = FieldValue(d.proto);
+ }
+ return protos;
+ }
+ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
+ for (const key in fields) {
+ if (fields.hasOwnProperty(key)) {
+ const value = fields[key];
+ if (value !== undefined) {
+ doc[key] = value;
+ }
+ }
+ }
+ return doc;
+ }
+
+ export function MakeAlias(doc: Doc) {
+ const alias = new Doc;
+
+ PromiseValue(Cast(doc.proto, Doc)).then(proto => {
+ if (proto) {
+ alias.proto = proto;
+ }
+ });
+
+ return alias;
+ }
+
+ export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
+ const copy = new Doc;
+ Object.keys(doc).forEach(key => {
+ const field = doc[key];
+ if (key === "proto" && copyProto) {
+ if (field instanceof Doc) {
+ copy[key] = Doc.MakeCopy(field);
+ }
+ } else {
+ if (field instanceof RefField) {
+ copy[key] = field;
+ } else if (field instanceof ObjectField) {
+ copy[key] = ObjectField.MakeCopy(field);
+ } else {
+ copy[key] = field;
+ }
+ }
+ });
+ return copy;
+ }
+
+ export function MakeLink(source: Doc, target: Doc) {
+ UndoManager.RunInBatch(() => {
+ let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
+ //let linkDoc = new Doc;
+ linkDoc.proto!.title = "-link name-";
+ linkDoc.linkDescription = "";
+ linkDoc.linkTags = "Default";
+
+ linkDoc.linkedTo = target;
+ linkDoc.linkedFrom = source;
+
+ let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc));
+ if (!linkedFrom) {
+ target.linkedFromDocs = linkedFrom = new List<Doc>();
+ }
+ linkedFrom.push(linkDoc);
+
+ let linkedTo = Cast(source.linkedToDocs, listSpec(Doc));
+ if (!linkedTo) {
+ source.linkedToDocs = linkedTo = new List<Doc>();
+ }
+ linkedTo.push(linkDoc);
+ return linkDoc;
+ }, "make link");
+ }
+
+ export function MakeDelegate(doc: Doc): Doc;
+ export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>;
+ export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> {
+ if (!doc) {
+ return undefined;
+ }
+ const delegate = new Doc();
+ delegate.proto = doc;
+ return delegate;
+ }
+ export const Prototype = Symbol("Prototype");
+} \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
new file mode 100644
index 000000000..d998746bb
--- /dev/null
+++ b/src/new_fields/HtmlField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("html")
+export class HtmlField extends ObjectField {
+ @serializable(primitive())
+ readonly html: string;
+
+ constructor(html: string) {
+ super();
+ this.html = html;
+ }
+
+ [Copy]() {
+ return new HtmlField(this.html);
+ }
+}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
new file mode 100644
index 000000000..1a928389d
--- /dev/null
+++ b/src/new_fields/IconField.ts
@@ -0,0 +1,18 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("icon")
+export class IconField extends ObjectField {
+ @serializable(primitive())
+ readonly icon: string;
+
+ constructor(icon: string) {
+ super();
+ this.icon = icon;
+ }
+
+ [Copy]() {
+ return new IconField(this.icon);
+ }
+}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
new file mode 100644
index 000000000..2d75f8a19
--- /dev/null
+++ b/src/new_fields/InkField.ts
@@ -0,0 +1,43 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+import { deepCopy } from "../Utils";
+
+export enum InkTool {
+ None,
+ Pen,
+ Highlighter,
+ Eraser
+}
+
+export interface StrokeData {
+ pathData: Array<{ x: number, y: number }>;
+ color: string;
+ width: string;
+ tool: InkTool;
+ page: number;
+}
+
+const pointSchema = createSimpleSchema({
+ x: true, y: true
+});
+
+const strokeDataSchema = createSimpleSchema({
+ pathData: list(object(pointSchema)),
+ "*": true
+});
+
+@Deserializable("ink")
+export class InkField extends ObjectField {
+ @serializable(map(object(strokeDataSchema)))
+ readonly inkData: Map<string, StrokeData>;
+
+ constructor(data?: Map<string, StrokeData>) {
+ super();
+ this.inkData = data || new Map;
+ }
+
+ [Copy]() {
+ return new InkField(deepCopy(this.inkData));
+ }
+}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
new file mode 100644
index 000000000..88a65eba4
--- /dev/null
+++ b/src/new_fields/List.ts
@@ -0,0 +1,279 @@
+import { Deserializable, autoObject } from "../client/util/SerializationHelper";
+import { Field, Update, Self, FieldResult, SelfProxy } from "./Doc";
+import { setter, getter, deleteProperty, updateFunction } from "./util";
+import { serializable, alias, list } from "serializr";
+import { observable, action } from "mobx";
+import { ObjectField, OnUpdate, Copy, Parent } from "./ObjectField";
+import { RefField } from "./RefField";
+import { ProxyField } from "./Proxy";
+
+const listHandlers: any = {
+ /// Mutator methods
+ copyWithin() {
+ throw new Error("copyWithin not supported yet");
+ },
+ fill(value: any, start?: number, end?: number) {
+ if (value instanceof RefField) {
+ throw new Error("fill with RefFields not supported yet");
+ }
+ const res = this[Self].__fields.fill(value, start, end);
+ this[Update]();
+ return res;
+ },
+ pop(): any {
+ const field = toRealField(this[Self].__fields.pop());
+ this[Update]();
+ return field;
+ },
+ push: action(function (this: any, ...items: any[]) {
+ items = items.map(toObjectField);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + length, item, this);
+ }
+ }
+ const res = list.__fields.push(...items);
+ this[Update]();
+ return res;
+ }),
+ reverse() {
+ const res = this[Self].__fields.reverse();
+ this[Update]();
+ return res;
+ },
+ shift() {
+ const res = toRealField(this[Self].__fields.shift());
+ this[Update]();
+ return res;
+ },
+ sort(cmpFunc: any) {
+ const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
+ this[Update]();
+ return res;
+ },
+ splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ items = items.map(toObjectField);
+ const list = this[Self];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i + start, item, this);
+ }
+ }
+ const res = list.__fields.splice(start, deleteCount, ...items);
+ this[Update]();
+ return res.map(toRealField);
+ }),
+ unshift(...items: any[]) {
+ items = items.map(toObjectField);
+ const list = this[Self];
+ const length = list.__fields.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[OnUpdate] = updateFunction(list, i, item, this);
+ }
+ }
+ const res = this[Self].__fields.unshift(...items);
+ this[Update]();
+ return res;
+
+ },
+ /// Accessor methods
+ concat: action(function (this: any, ...items: any[]) {
+ return this[Self].__fields.map(toRealField).concat(...items);
+ }),
+ includes(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).includes(valueToFind, fromIndex);
+ } else {
+ return fields.includes(valueToFind, fromIndex);
+ }
+ },
+ indexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).indexOf(valueToFind, fromIndex);
+ } else {
+ return fields.indexOf(valueToFind, fromIndex);
+ }
+ },
+ join(separator: any) {
+ return this[Self].__fields.map(toRealField).join(separator);
+ },
+ lastIndexOf(valueToFind: any, fromIndex: number) {
+ const fields = this[Self].__fields;
+ if (valueToFind instanceof RefField) {
+ return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex);
+ } else {
+ return fields.lastIndexOf(valueToFind, fromIndex);
+ }
+ },
+ slice(begin: number, end: number) {
+ return this[Self].__fields.slice(begin, end).map(toRealField);
+ },
+
+ /// Iteration methods
+ entries() {
+ return this[Self].__fields.map(toRealField).entries();
+ },
+ every(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).every(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ filter(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).filter(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ find(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).find(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ findIndex(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).findIndex(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ forEach(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).forEach(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ map(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).map(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ reduce(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduce(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ reduceRight(callback: any, initialValue: any) {
+ return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ some(callback: any, thisArg: any) {
+ return this[Self].__fields.map(toRealField).some(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ values() {
+ return this[Self].__fields.map(toRealField).values();
+ },
+ [Symbol.iterator]() {
+ return this[Self].__fields.map(toRealField).values();
+ }
+};
+
+function toObjectField(field: Field) {
+ return field instanceof RefField ? new ProxyField(field) : field;
+}
+
+function toRealField(field: Field) {
+ return field instanceof ProxyField ? field.value() : field;
+}
+
+function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+ if (listHandlers.hasOwnProperty(prop)) {
+ return listHandlers[prop];
+ }
+ return getter(target, prop, receiver);
+}
+
+interface ListSpliceUpdate<T> {
+ type: "splice";
+ index: number;
+ added: T[];
+ removedCount: number;
+}
+
+interface ListIndexUpdate<T> {
+ type: "update";
+ index: number;
+ newValue: T;
+}
+
+type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
+
+type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
+
+@Deserializable("list")
+class ListImpl<T extends Field> extends ObjectField {
+ constructor(fields: T[] = []) {
+ super();
+ const list = new Proxy<this>(this, {
+ set: setter,
+ get: listGetter,
+ deleteProperty: deleteProperty,
+ defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); },
+ });
+ this[SelfProxy] = list;
+ (list as any).push(...fields);
+ return list;
+ }
+
+ [key: number]: T | (T extends RefField ? Promise<T> : never);
+
+ @serializable(alias("fields", list(autoObject())))
+ private get __fields() {
+ return this.___fields;
+ }
+
+ private set __fields(value) {
+ this.___fields = value;
+ for (const key in value) {
+ const field = value[key];
+ if (!(field instanceof ObjectField)) continue;
+ (field as ObjectField)[Parent] = this[Self];
+ (field as ObjectField)[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ }
+ }
+
+ [Copy]() {
+ let copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
+ let deepCopy = new ListImpl<T>(copiedData as any);
+ return deepCopy;
+ }
+
+ // @serializable(alias("fields", list(autoObject())))
+ @observable
+ private ___fields: StoredType<T>[] = [];
+
+ private [Update] = (diff: any) => {
+ // console.log(diff);
+ const update = this[OnUpdate];
+ // update && update(diff);
+ update && update();
+ }
+
+ private [Self] = this;
+ private [SelfProxy]: any;
+}
+export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
+export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; \ No newline at end of file
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
new file mode 100644
index 000000000..f276bfa67
--- /dev/null
+++ b/src/new_fields/ObjectField.ts
@@ -0,0 +1,18 @@
+import { Doc } from "./Doc";
+import { RefField } from "./RefField";
+
+export const OnUpdate = Symbol("OnUpdate");
+export const Parent = Symbol("Parent");
+export const Copy = Symbol("Copy");
+
+export abstract class ObjectField {
+ protected [OnUpdate](diff?: any) { };
+ private [Parent]?: RefField | ObjectField;
+ abstract [Copy](): ObjectField;
+}
+
+export namespace ObjectField {
+ export function MakeCopy<T extends ObjectField>(field: T) {
+ return field[Copy]();
+ }
+}
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
new file mode 100644
index 000000000..fd99ae1c0
--- /dev/null
+++ b/src/new_fields/Proxy.ts
@@ -0,0 +1,65 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { FieldWaiting } from "./Doc";
+import { primitive, serializable } from "serializr";
+import { observable, action } from "mobx";
+import { DocServer } from "../client/DocServer";
+import { RefField, Id } from "./RefField";
+import { ObjectField, Copy } from "./ObjectField";
+
+@Deserializable("proxy")
+export class ProxyField<T extends RefField> extends ObjectField {
+ constructor();
+ constructor(value: T);
+ constructor(fieldId: string);
+ constructor(value?: T | string) {
+ super();
+ if (typeof value === "string") {
+ this.fieldId = value;
+ } else if (value) {
+ this.cache = value;
+ this.fieldId = value[Id];
+ }
+ }
+
+ [Copy]() {
+ if (this.cache) return new ProxyField<T>(this.cache);
+ return new ProxyField<T>(this.fieldId);
+ }
+
+ @serializable(primitive())
+ readonly fieldId: string = "";
+
+ // This getter/setter and nested object thing is
+ // because mobx doesn't play well with observable proxies
+ @observable.ref
+ private _cache: { readonly field: T | undefined } = { field: undefined };
+ private get cache(): T | undefined {
+ return this._cache.field;
+ }
+ private set cache(field: T | undefined) {
+ this._cache = { field };
+ }
+
+ private failed = false;
+ private promise?: Promise<any>;
+
+ value(callback?: ((field: T | undefined) => void)): T | undefined | FieldWaiting {
+ if (this.cache) {
+ callback && callback(this.cache);
+ return this.cache;
+ }
+ if (this.failed) {
+ return undefined;
+ }
+ if (!this.promise) {
+ this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => {
+ this.promise = undefined;
+ this.cache = field;
+ if (field === undefined) this.failed = true;
+ return field;
+ }));
+ }
+ callback && this.promise.then(callback);
+ return this.promise;
+ }
+}
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
new file mode 100644
index 000000000..202c65f21
--- /dev/null
+++ b/src/new_fields/RefField.ts
@@ -0,0 +1,18 @@
+import { serializable, primitive, alias } from "serializr";
+import { Utils } from "../Utils";
+
+export type FieldId = string;
+export const HandleUpdate = Symbol("HandleUpdate");
+export const Id = Symbol("Id");
+export abstract class RefField {
+ @serializable(alias("id", primitive()))
+ private __id: FieldId;
+ readonly [Id]: FieldId;
+
+ constructor(id?: FieldId) {
+ this.__id = id || Utils.GenerateGuid();
+ this[Id] = this.__id;
+ }
+
+ protected [HandleUpdate]?(diff: any): void;
+}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
new file mode 100644
index 000000000..eb30e76de
--- /dev/null
+++ b/src/new_fields/RichTextField.ts
@@ -0,0 +1,18 @@
+import { ObjectField, Copy } from "./ObjectField";
+import { serializable } from "serializr";
+import { Deserializable } from "../client/util/SerializationHelper";
+
+@Deserializable("RichTextField")
+export class RichTextField extends ObjectField {
+ @serializable(true)
+ readonly Data: string;
+
+ constructor(data: string) {
+ super();
+ this.Data = data;
+ }
+
+ [Copy]() {
+ return new RichTextField(this.Data);
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
new file mode 100644
index 000000000..b821baec9
--- /dev/null
+++ b/src/new_fields/Schema.ts
@@ -0,0 +1,82 @@
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType } from "./Types";
+import { Doc, Field } from "./Doc";
+
+type AllToInterface<T extends Interface[]> = {
+ 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
+ 0: ToInterface<Head<T>>
+}[HasTail<T> extends true ? 1 : 0];
+
+export const emptySchema = createSchema({});
+export const Document = makeInterface(emptySchema);
+export type Document = makeInterface<[typeof emptySchema]>;
+
+export type makeInterface<T extends Interface[]> = Partial<AllToInterface<T>> & Doc & { proto: Doc | undefined };
+// export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
+// export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>;
+export function makeInterface<T extends Interface[]>(...schemas: T): (doc?: Doc) => makeInterface<T> {
+ let schema: Interface = {};
+ for (const s of schemas) {
+ for (const key in s) {
+ schema[key] = s[key];
+ }
+ }
+ const proto = new Proxy({}, {
+ get(target: any, prop, receiver) {
+ const field = receiver.doc[prop];
+ if (prop in schema) {
+ return Cast(field, (schema as any)[prop]);
+ }
+ return field;
+ },
+ set(target: any, prop, value, receiver) {
+ receiver.doc[prop] = value;
+ return true;
+ }
+ });
+ return function (doc?: Doc) {
+ doc = doc || new Doc;
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto, { doc: { value: doc, writable: false } });
+ return obj;
+ };
+}
+
+export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
+export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) => makeStrictInterface<T> {
+ const proto = {};
+ for (const key in schema) {
+ const type = schema[key];
+ Object.defineProperty(proto, key, {
+ get() {
+ return Cast(this.__doc[key], type as any);
+ },
+ set(value) {
+ value = Cast(value, type as any);
+ if (value !== undefined) {
+ this.__doc[key] = value;
+ return;
+ }
+ throw new TypeError("Expected type " + type);
+ }
+ });
+ }
+ return function (doc: any) {
+ if (!(doc instanceof Doc)) {
+ throw new Error("Currently wrapping a schema in another schema isn't supported");
+ }
+ const obj = Object.create(proto);
+ obj.__doc = doc;
+ return obj;
+ };
+}
+
+export function createSchema<T extends Interface>(schema: T): T & { proto: ToConstructor<Doc> } {
+ schema.proto = Doc;
+ return schema as any;
+}
+
+export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
+ return { List: type as any };//TODO Types
+} \ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
new file mode 100644
index 000000000..4b4c58eb8
--- /dev/null
+++ b/src/new_fields/Types.ts
@@ -0,0 +1,88 @@
+import { Field, Opt, FieldResult, Doc } from "./Doc";
+import { List } from "./List";
+import { RefField } from "./RefField";
+
+export type ToType<T extends ToConstructor<Field> | ListSpec<Field>> =
+ T extends "string" ? string :
+ T extends "number" ? number :
+ T extends "boolean" ? boolean :
+ T extends ListSpec<infer U> ? List<U> :
+ // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+ T extends { new(...args: any[]): List<Field> } ? never :
+ T extends { new(...args: any[]): infer R } ? R : never;
+
+export type ToConstructor<T extends Field> =
+ T extends string ? "string" :
+ T extends number ? "number" :
+ T extends boolean ? "boolean" :
+ T extends List<infer U> ? ListSpec<U> :
+ new (...args: any[]) => T;
+
+export type ToInterface<T extends Interface> = {
+ [P in Exclude<keyof T, "proto">]: FieldResult<ToType<T[P]>>;
+};
+
+// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
+export type ListSpec<T extends Field> = { List: ToConstructor<T> };
+
+// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
+
+export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
+export type Tail<T extends any[]> =
+ ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
+export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+
+//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
+export interface Interface {
+ [key: string]: ToConstructor<Field> | ListSpec<Field>;
+ // [key: string]: ToConstructor<Field> | ListSpec<Field[]>;
+}
+
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<ToType<T>> | null): WithoutList<ToType<T>>;
+export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
+ if (field instanceof Promise) {
+ return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
+ }
+ if (field !== undefined && !(field instanceof Promise)) {
+ if (typeof ctor === "string") {
+ if (typeof field === ctor) {
+ return field as ToType<T>;
+ }
+ } else if (typeof ctor === "object") {
+ if (field instanceof List) {
+ return field as any;
+ }
+ } else if (field instanceof (ctor as any)) {
+ return field as ToType<T>;
+ }
+ }
+ return defaultVal === null ? undefined : defaultVal;
+}
+
+export function NumCast(field: FieldResult, defaultVal: number | null = 0) {
+ return Cast(field, "number", defaultVal);
+}
+
+export function StrCast(field: FieldResult, defaultVal: string | null = "") {
+ return Cast(field, "string", defaultVal);
+}
+
+export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) {
+ return Cast(field, "boolean", defaultVal);
+}
+
+type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
+
+export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
+export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>;
+export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
+ return (field instanceof Promise || field === undefined) ? defaultValue : field;
+}
+
+export interface PromiseLike<T> {
+ then(callback: (field: Opt<T>) => void): void;
+}
+export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> {
+ return field instanceof Promise ? field : { then(cb: ((field: Opt<T>) => void)) { return cb(field); } };
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
new file mode 100644
index 000000000..d00a95a16
--- /dev/null
+++ b/src/new_fields/URLField.ts
@@ -0,0 +1,34 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, custom } from "serializr";
+import { ObjectField, Copy } from "./ObjectField";
+
+function url() {
+ return custom(
+ function (value: URL) {
+ return value.href;
+ },
+ function (jsonValue: string) {
+ return new URL(jsonValue);
+ }
+ );
+}
+
+export class URLField extends ObjectField {
+ @serializable(url())
+ readonly url: URL;
+
+ constructor(url: URL) {
+ super();
+ this.url = url;
+ }
+
+ [Copy](): this {
+ return new (this.constructor as any)(this.url);
+ }
+}
+
+@Deserializable("audio") export class AudioField extends URLField { }
+@Deserializable("image") export class ImageField extends URLField { }
+@Deserializable("video") export class VideoField extends URLField { }
+@Deserializable("pdf") export class PdfField extends URLField { }
+@Deserializable("web") export class WebField extends URLField { } \ No newline at end of file
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
new file mode 100644
index 000000000..bbd8157f6
--- /dev/null
+++ b/src/new_fields/util.ts
@@ -0,0 +1,104 @@
+import { UndoManager } from "../client/util/UndoManager";
+import { Update, Doc, Field } from "./Doc";
+import { SerializationHelper } from "../client/util/SerializationHelper";
+import { ProxyField } from "./Proxy";
+import { FieldValue } from "./Types";
+import { RefField, Id } from "./RefField";
+import { ObjectField, Parent, OnUpdate } from "./ObjectField";
+import { action } from "mobx";
+
+export const setter = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+ if (SerializationHelper.IsSerializing()) {
+ target[prop] = value;
+ return true;
+ }
+ if (typeof prop === "symbol") {
+ target[prop] = value;
+ return true;
+ }
+ const curValue = target.__fields[prop];
+ if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
+ // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
+ // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
+ return true;
+ }
+ if (value instanceof RefField) {
+ value = new ProxyField(value);
+ }
+ if (value instanceof ObjectField) {
+ //TODO Instead of target, maybe use target[Self]
+ if (value[Parent] && value[Parent] !== target) {
+ throw new Error("Can't put the same object in multiple documents at the same time");
+ }
+ value[Parent] = target;
+ value[OnUpdate] = updateFunction(target, prop, value, receiver);
+ }
+ if (curValue instanceof ObjectField) {
+ delete curValue[Parent];
+ delete curValue[OnUpdate];
+ }
+ target.__fields[prop] = value;
+ target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } });
+ UndoManager.AddEvent({
+ redo: () => receiver[prop] = value,
+ undo: () => receiver[prop] = curValue
+ });
+ return true;
+});
+
+export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+ if (typeof prop === "symbol") {
+ return target.__fields[prop] || target[prop];
+ }
+ if (SerializationHelper.IsSerializing()) {
+ return target[prop];
+ }
+ return getField(target, prop);
+}
+
+export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any {
+ const field = target.__fields[prop];
+ if (field instanceof ProxyField) {
+ return field.value(callback);
+ }
+ if (field === undefined && !ignoreProto) {
+ const proto = getField(target, "proto", true);
+ if (proto instanceof Doc) {
+ let field = proto[prop];
+ if (field instanceof Promise) {
+ callback && field.then(callback);
+ return undefined;
+ } else {
+ callback && callback(field);
+ return field;
+ }
+ }
+ }
+ callback && callback(field);
+ return field;
+}
+
+export function deleteProperty(target: any, prop: string | number | symbol) {
+ if (typeof prop === "symbol") {
+ delete target[prop];
+ return true;
+ }
+ throw new Error("Currently properties can't be deleted from documents, assign to undefined instead");
+}
+
+export function updateFunction(target: any, prop: any, value: any, receiver: any) {
+ let current = ObjectField.MakeCopy(value);
+ return (diff?: any) => {
+ if (true || !diff) {
+ diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ const oldValue = current;
+ const newValue = ObjectField.MakeCopy(value);
+ current = newValue;
+ UndoManager.AddEvent({
+ redo() { receiver[prop] = newValue; },
+ undo() { receiver[prop] = oldValue; }
+ });
+ }
+ target[Update](diff);
+ };
+} \ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index bbe4ffcad..e9a8b0f0c 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -14,8 +14,8 @@ export class Message<T> {
}
export enum Types {
- Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference,
- Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script,
+ Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference,
+ Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, Templates
}
export interface Transferable {
@@ -24,6 +24,14 @@ export interface Transferable {
readonly data?: any;
}
+export interface Reference {
+ readonly id: string;
+}
+
+export interface Diff extends Reference {
+ readonly diff: any;
+}
+
export namespace MessageStore {
export const Foo = new Message<string>("Foo");
export const Bar = new Message<string>("Bar");
@@ -32,4 +40,9 @@ export namespace MessageStore {
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");
+
+ export const GetRefField = new Message<string>("Get Ref Field");
+ export const GetRefFields = new Message<string[]>("Get Ref Fields");
+ export const UpdateField = new Message<Diff>("Update Ref Field");
+ export const CreateField = new Message<Reference>("Create Ref Field");
}
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
deleted file mode 100644
index 818230c1a..000000000
--- a/src/server/ServerUtil.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { HistogramField } from "../client/northstar/dash-fields/HistogramField";
-import { AudioField } from "../fields/AudioField";
-import { BooleanField } from "../fields/BooleanField";
-import { HtmlField } from "../fields/HtmlField";
-import { InkField } from "../fields/InkField";
-import { PDFField } from "../fields/PDFField";
-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";
-
-export class ServerUtils {
- public static prepend(extension: string): string {
- return window.location.origin + extension;
- }
-
- public static FromJson(json: Transferable): Field {
-
- 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 (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.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.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/controllers/WorkspacesMenu.css b/src/server/authentication/controllers/WorkspacesMenu.css
deleted file mode 100644
index b89039965..000000000
--- a/src/server/authentication/controllers/WorkspacesMenu.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.ids:hover {
- color: darkblue;
-} \ No newline at end of file
diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx
deleted file mode 100644
index b08c1aebe..000000000
--- a/src/server/authentication/controllers/WorkspacesMenu.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import * as React from 'react';
-import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx';
-import { observer } from "mobx-react";
-import './WorkspacesMenu.css';
-import { Document } from '../../../fields/Document';
-import { EditableView } from '../../../client/views/EditableView';
-import { KeyStore } from '../../../fields/KeyStore';
-
-export interface WorkspaceMenuProps {
- active: Document | undefined;
- open: (workspace: Document) => void;
- new: () => void;
- allWorkspaces: Document[];
- isShown: () => boolean;
- toggle: () => void;
-}
-
-@observer
-export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> {
- constructor(props: WorkspaceMenuProps) {
- super(props);
- this.addNewWorkspace = this.addNewWorkspace.bind(this);
- }
-
- @action
- addNewWorkspace() {
- this.props.new();
- this.props.toggle();
- }
-
- render() {
- return (
- <div
- style={{
- width: "auto",
- maxHeight: '200px',
- overflow: 'scroll',
- borderRadius: 5,
- position: "absolute",
- top: 78,
- left: this.props.isShown() ? 11 : -500,
- background: "white",
- border: "black solid 2px",
- transition: "all 1s ease",
- zIndex: 15,
- padding: 10,
- paddingRight: 12,
- }}>
- <img
- src="https://bit.ly/2IBBkxk"
- style={{
- width: 20,
- height: 20,
- marginTop: 3,
- marginLeft: 3,
- marginBottom: 3,
- cursor: "grab"
- }}
- onClick={this.addNewWorkspace}
- />
- {this.props.allWorkspaces.map((s, i) =>
- <div
- key={s.Id}
- onContextMenu={(e) => {
- e.preventDefault();
- this.props.open(s);
- }}
- style={{
- marginTop: 10,
- color: s === this.props.active ? "red" : "black"
- }}
- >
- <span>{i + 1} - </span>
- <EditableView
- display={"inline"}
- GetValue={() => s.Title}
- SetValue={(title: string): boolean => {
- s.SetText(KeyStore.Title, title);
- return true;
- }}
- contents={s.Title}
- height={20}
- />
- </div>
- )}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5d4479c88..5f45d7bcc 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,19 +1,20 @@
import { computed, observable, action, runInAction } from "mobx";
import * as rp from 'request-promise';
-import { Documents } from "../../../client/documents/Documents";
+import { Docs } 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 { RouteStore } from "../../RouteStore";
-import { ServerUtils } from "../../ServerUtil";
+import { DocServer } from "../../../client/DocServer";
+import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
+import { CollectionTreeView } from "../../../client/views/collections/CollectionTreeView";
+import { CollectionView } from "../../../client/views/collections/CollectionView";
export class CurrentUserUtils {
private static curr_email: string;
private static curr_id: string;
- @observable private static user_document: Document;
+ @observable private static user_document: Doc;
//TODO tfs: these should be temporary...
private static mainDocId: string | undefined;
@@ -23,15 +24,21 @@ export class CurrentUserUtils {
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" }));
+ private static createUserDocument(id: string): Doc {
+ let doc = new Doc(id, true);
+ doc.viewType = CollectionViewType.Tree;
+ doc.layout = CollectionView.LayoutString();
+ doc.title = this.email;
+ doc.data = new List<Doc>();
+ doc.excludeFromLibrary = true;
+ doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" });
+ // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
+ // (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
public static loadCurrentUser(): Promise<any> {
- let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then(response => {
+ let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => {
if (response) {
let obj = JSON.parse(response);
CurrentUserUtils.curr_id = obj.id as string;
@@ -40,10 +47,10 @@ export class CurrentUserUtils {
throw new Error("There should be a user! Why does Dash think there isn't one?");
}
});
- let userDocPromise = rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(id => {
+ let userDocPromise = rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
- return Server.GetField(id).then(field =>
- runInAction(() => this.user_document = field instanceof Document ? field : this.createUserDocument(id)));
+ return DocServer.GetRefField(id).then(field =>
+ runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id)));
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/server/database.ts b/src/server/database.ts
index 5457e4dd5..69005d2d3 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -13,22 +13,15 @@ export class Database {
this.MongoClient.connect(this.url, (err, client) => this.db = client.db());
}
- public update(id: string, value: any, callback: () => void) {
+ public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) {
if (this.db) {
- let collection = this.db.collection('documents');
+ let collection = this.db.collection(collectionName);
const prom = this.currentWrites[id];
let newProm: Promise<void>;
const run = (): Promise<void> => {
return new Promise<void>(resolve => {
- collection.updateOne({ _id: id }, { $set: value }, { upsert: true }
+ collection.updateOne({ _id: id }, value, { upsert }
, (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];
}
@@ -51,24 +44,53 @@ export class Database {
this.db && this.db.collection(collectionName).deleteMany({}, res));
}
- public insert(kvpairs: any, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).insertOne(kvpairs, (err, res) =>
- err // && console.log(err)
- );
+ public insert(value: any, collectionName = Database.DocumentsCollection) {
+ if (!this.db) { return; }
+ if ("id" in value) {
+ value._id = value.id;
+ delete value.id;
+ }
+ const id = value._id;
+ const collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.insertOne(value, (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
}
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));
+ this.db && this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
+ if (result) {
+ result.id = result._id;
+ delete result._id;
+ fn(result);
+ } else {
+ fn(undefined);
+ }
+ });
}
public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
- this.db && this.db.collection(collectionName).find({ id: { "$in": ids } }).toArray((err, docs) => {
+ 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 })));
+ fn(docs.map(doc => {
+ doc.id = doc._id;
+ delete doc._id;
+ return doc;
+ }));
});
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 70a7d266c..6801b3132 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -16,13 +16,12 @@ 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 { 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 { MessageStore, Transferable } from "./Message";
+import { MessageStore, Transferable, Diff } from "./Message";
import { RouteStore } from './RouteStore';
const app = express();
const config = require('../../webpack.config');
@@ -232,14 +231,21 @@ server.on("connection", function (socket: Socket) {
Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+
+ Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
});
-function deleteFields() {
- return Database.Instance.deleteAll();
+async function deleteFields() {
+ await Database.Instance.deleteAll();
+ await Database.Instance.deleteAll('newDocuments');
}
async function deleteAll() {
await Database.Instance.deleteAll();
+ await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
}
@@ -262,5 +268,22 @@ function setField(socket: Socket, newValue: Transferable) {
socket.broadcast.emit(MessageStore.SetField.Message, newValue));
}
+function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+ Database.Instance.getDocument(id, callback, "newDocuments");
+}
+
+function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
+ Database.Instance.getDocuments(ids, callback, "newDocuments");
+}
+
+function UpdateField(socket: Socket, diff: Diff) {
+ Database.Instance.update(diff.id, diff.diff,
+ () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+}
+
+function CreateField(newValue: any) {
+ Database.Instance.insert(newValue, "newDocuments");
+}
+
server.listen(serverPort);
console.log(`listening on port ${serverPort}`); \ No newline at end of file