From 95dff75290edc9b13e9741eefe2bc5851a60dde6 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 18 Apr 2019 13:51:25 -0400 Subject: fixed workspace switching exception. started to refresh minimize functionality. --- src/client/views/collections/CollectionTreeView.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 51a02fc25..e0387f4b4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,20 +1,17 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { FieldWaiting } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { SetupDrag, DragManager } from "../../util/DragManager"; +import { DragManager, SetupDrag } from "../../util/DragManager"; import { EditableView } from "../EditableView"; -import "./CollectionTreeView.scss"; -import { CollectionView } from "./CollectionView"; -import * as globalCssVariables from "../../views/globalCssVariables.scss"; import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss"; import React = require("react"); -import { props } from 'bluebird'; export interface TreeViewProps { -- cgit v1.2.3-70-g09d2 From 575d8b339cdf74cdcb8d46dcd6e627aec2dda636 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 24 Apr 2019 23:17:44 -0400 Subject: added borderRadius and fixed a bunch of broken props along the way. --- src/client/documents/Documents.ts | 2 ++ src/client/views/Main.scss | 3 ++- src/client/views/Main.tsx | 3 ++- src/client/views/MainOverlayTextBox.tsx | 4 ++-- src/client/views/collections/CollectionBaseView.tsx | 7 ++++--- src/client/views/collections/CollectionDockingView.tsx | 3 ++- src/client/views/collections/CollectionSchemaView.tsx | 6 +++++- src/client/views/collections/CollectionSubView.tsx | 1 + src/client/views/collections/CollectionTreeView.tsx | 5 ++++- .../collectionFreeForm/CollectionFreeFormView.scss | 1 + .../collectionFreeForm/CollectionFreeFormView.tsx | 14 +++++++++++--- .../views/collections/collectionFreeForm/MarqueeView.tsx | 7 ++++--- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 11 +++++++++++ src/client/views/nodes/DocumentView.tsx | 2 ++ src/client/views/nodes/FieldView.tsx | 7 ++++++- src/client/views/nodes/FormattedTextBox.scss | 3 +-- src/client/views/nodes/FormattedTextBox.tsx | 5 ++++- src/client/views/nodes/ImageBox.scss | 3 --- src/client/views/nodes/ImageBox.tsx | 2 +- src/fields/KeyStore.ts | 3 ++- 20 files changed, 67 insertions(+), 25 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 0e6661819..879b114b6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -55,6 +55,7 @@ export interface DocumentOptions { backgroundColor?: string; copyDraggedItems?: boolean; documentText?: string; + borderRounding?: number; } export namespace Documents { @@ -108,6 +109,7 @@ export namespace Documents { 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); } + if (options.borderRounding !== undefined) { doc.SetNumber(KeyStore.BorderRounding, options.borderRounding); } return doc; } diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 4373534b2..3edc79d39 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -42,7 +42,8 @@ h1 { } .jsx-parser { - width:100% + width:100%; + pointer-events: none; } p { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 934616aca..057f68ed9 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -17,7 +17,7 @@ import { CurrentUserUtils } from '../../server/authentication/models/current_use import { MessageStore } from '../../server/Message'; import { RouteStore } from '../../server/RouteStore'; import { ServerUtils } from '../../server/ServerUtil'; -import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne } from '../../Utils'; +import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; import { Documents } from '../documents/Documents'; import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel'; import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; @@ -197,6 +197,7 @@ export class Main extends React.Component { isTopMost={true} selectOnLoad={false} focus={emptyDocFunction} + borderRounding={returnZero} parentActive={returnTrue} whenActiveChanged={emptyFunction} ContainingCollectionView={undefined} />} diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 9aaaad084..f98f6e8a5 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -5,7 +5,7 @@ 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 { emptyDocFunction, emptyFunction, returnTrue, returnZero } from '../../Utils'; import '../northstar/model/ModelExtensions'; import '../northstar/utils/Extensions'; import { DragManager } from '../util/DragManager'; @@ -97,7 +97,7 @@ export class MainOverlayTextBox extends React.Component style={{ width: `${this.TextDoc.Width()}px`, height: `${this.TextDoc.Height()}px` }}> + ScreenToLocalTransform={this._textXf} borderRounding={returnZero} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyDocFunction} /> ; } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 097236c1d..e65770137 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -2,13 +2,12 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Document } from '../../../fields/Document'; -import { Field, FieldValue, FieldWaiting } from '../../../fields/Field'; +import { FieldValue, FieldWaiting } from '../../../fields/Field'; import { KeyStore } from '../../../fields/KeyStore'; import { ListField } from '../../../fields/ListField'; import { NumberField } from '../../../fields/NumberField'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; -import { TextField } from '../../../fields/TextField'; export enum CollectionViewType { Invalid, @@ -190,7 +189,9 @@ export class CollectionBaseView extends React.Component { }; const viewtype = this.collectionViewType; return ( -
+
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 84159635a..eb94a4e6d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -8,7 +8,7 @@ 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 { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne, returnZero } from "../../../Utils"; import { Server } from "../../Server"; import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; @@ -368,6 +368,7 @@ export class DockedFrameRenderer extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyDocFunction} + borderRounding={returnZero} ContainingCollectionView={undefined} />
); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index cb7a67991..dc2910ede 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -12,7 +12,7 @@ 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 { emptyDocFunction, emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { Server } from "../../Server"; import { SetupDrag } from "../../util/DragManager"; import { CompileScript, ToField } from "../../util/Scripting"; @@ -77,6 +77,9 @@ export class CollectionSchemaView extends CollectionSubView { focus: emptyDocFunction, active: returnFalse, whenActiveChanged: emptyFunction, + borderRounding: this.props.borderRounding, + PanelHeight: returnZero, + PanelWidth: returnZero, }; let contents = ( @@ -275,6 +278,7 @@ export class CollectionSchemaView extends CollectionSubView { focus={emptyDocFunction} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} + borderRounding={this.props.borderRounding} /> boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; + borderRounding: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index e0387f4b4..b9d1f0bd9 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -136,7 +136,10 @@ export class CollectionTreeView extends CollectionSubView { ); return ( -
e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> +
e.stopPropagation()} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
.jsx-parser { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7492d8744..9e2c9bf9a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -257,6 +257,7 @@ export class CollectionFreeFormView extends CollectionSubView { focus: this.focusDocument, parentActive: this.props.active, whenActiveChanged: this.props.active, + borderRounding: this.props.borderRounding, }; } @@ -288,11 +289,17 @@ export class CollectionFreeFormView extends CollectionSubView { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; return (
- + + {this.props.Document.Title} + + */} + - @@ -340,6 +347,7 @@ interface CollectionFreeFormViewPannableContentsProps { panX: () => number; panY: () => number; zoomScaling: () => number; + borderRadius: () => number; } @observer @@ -350,7 +358,7 @@ class CollectionFreeFormViewPannableContents extends React.Component otherwise, reactions won't fire - return
+ return
{this.props.children}
; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 898fff019..e724ab47e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -14,7 +14,6 @@ import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); import { Utils } from "../../../../Utils"; -import { ListField } from "../../../../fields/ListField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -25,6 +24,7 @@ interface MarqueeViewProps { selectDocuments: (docs: Document[]) => void; removeDocument: (doc: Document) => boolean; addLiveTextDocument: (doc: Document) => void; + borderRadius: () => number; } @observer @@ -138,7 +138,7 @@ export class MarqueeView extends React.Component this.cleanupInteractions(true); e.stopPropagation(); } - if (e.key === "c" || e.key === "r") { + if (e.key === "c" || e.key === "r" || e.key === "e") { e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().map(d => { @@ -156,6 +156,7 @@ export class MarqueeView extends React.Component y: bounds.top, panx: 0, pany: 0, + borderRounding: e.key === "e" ? -1 : undefined, backgroundColor: selected.length ? "white" : "", scale: zoomBasis, width: bounds.width * zoomBasis, @@ -250,7 +251,7 @@ export class MarqueeView extends React.Component } render() { - return
+ return
{this.props.children} {!this._visible ? (null) : this.marqueeDiv}
; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 420324d40..8caf5ff31 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -65,6 +65,7 @@ export class CollectionFreeFormDocumentView extends React.Component; } @@ -148,6 +149,15 @@ export class CollectionFreeFormDocumentView extends React.Component { + let br = this.props.Document.GetNumber(KeyStore.BorderRounding, 0); + return br >= 0 ? br : + this.props.Document.GetNumber(KeyStore.NativeWidth, 0) === 0 ? + Math.min(this.props.PanelWidth(), this.props.PanelHeight()) + : + Math.min(this.props.Document.GetNumber(KeyStore.NativeWidth, 0), this.props.Document.GetNumber(KeyStore.NativeHeight, 0)); + } + render() { let maximizedDoc = this.props.Document.GetT(KeyStore.MaximizedDoc, Document); let zoomFade = 1; @@ -168,6 +178,7 @@ export class CollectionFreeFormDocumentView extends React.Component boolean; whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; + borderRounding: () => number; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key }; @@ -277,6 +278,7 @@ export class DocumentView extends React.Component {
boolean; whenActiveChanged: (isActive: boolean) => void; focus: (doc: Document) => void; + PanelWidth: () => number; + PanelHeight: () => number; + borderRounding: () => number; } @observer @@ -100,6 +103,8 @@ export class FieldView extends React.Component { layoutKey={KeyStore.Layout} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} + borderRounding={returnZero} + toggleMinimized={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} /> ); } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 5eb2bf7ce..59857bab3 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -22,11 +22,10 @@ overflow-x: hidden; color: initial; height: 100%; - pointer-events: all; + pointer-events: none; } .formattedTextBox-cont-hidden { overflow: hidden; - pointer-events: none; } .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 9cd58c73a..4b2fdbaff 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -273,7 +273,10 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte let color = this.props.Document.GetText(KeyStore.BackgroundColor, ""); return (
{ let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); let interactive = InkingControl.Instance.selectedTool ? "" : "interactive" return ( -
+
Image not found {paths.length > 1 ? this.dots(paths) : (null)} {this.lightbox(paths)} diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 0a96beb50..aa18a9f33 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -49,12 +49,13 @@ export namespace KeyStore { export const MinimizedDoc = new Key("MinimizedDoc"); export const MaximizedDoc = new Key("MaximizedDoc"); export const CopyDraggedItems = new Key("CopyDraggedItems"); + export const BorderRounding = new Key("BorderRounding"); export const KeyList: Key[] = [Prototype, X, Y, Page, Title, Author, PanX, PanY, Scale, NativeWidth, NativeHeight, Width, Height, ZIndex, ZoomBasis, Data, Annotations, ViewType, Layout, BackgroundColor, BackgroundLayout, OverlayLayout, LayoutKeys, LayoutFields, ColumnsKey, SchemaSplitPercentage, Caption, ActiveWorkspace, DocumentText, BrushingDocs, LinkedToDocs, LinkedFromDocs, LinkDescription, LinkTags, Thumbnail, ThumbnailPage, CurPage, AnnotationOn, NumPages, Ink, Cursors, OptionalRightCollection, - Archives, Workspaces, IsMinimized, MinimizedDoc, MaximizedDoc, CopyDraggedItems + Archives, Workspaces, IsMinimized, MinimizedDoc, MaximizedDoc, CopyDraggedItems, BorderRounding ]; export function KeyLookup(keyid: string) { for (const key of KeyList) { -- cgit v1.2.3-70-g09d2 From 6a62e0a16abc3bbadfb179750463e3d361f1c759 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Thu, 25 Apr 2019 09:15:22 -0400 Subject: silly me - borderRadius can be inherited --- src/client/views/Main.scss | 1 + src/client/views/Main.tsx | 1 - src/client/views/MainOverlayTextBox.tsx | 2 +- src/client/views/collections/CollectionBaseView.tsx | 2 +- src/client/views/collections/CollectionDockingView.tsx | 1 - src/client/views/collections/CollectionSchemaView.tsx | 2 -- src/client/views/collections/CollectionSubView.tsx | 1 - src/client/views/collections/CollectionTreeView.tsx | 2 +- .../collections/collectionFreeForm/CollectionFreeFormView.tsx | 10 ++++------ .../views/collections/collectionFreeForm/MarqueeView.tsx | 3 +-- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 1 - src/client/views/nodes/DocumentView.tsx | 3 +-- src/client/views/nodes/FieldView.tsx | 2 -- src/client/views/nodes/FormattedTextBox.tsx | 2 +- 14 files changed, 11 insertions(+), 22 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 3edc79d39..2f899ff28 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -44,6 +44,7 @@ h1 { .jsx-parser { width:100%; pointer-events: none; + border-radius: inherit; } p { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 057f68ed9..c6b3f06d8 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -197,7 +197,6 @@ export class Main extends React.Component { isTopMost={true} selectOnLoad={false} focus={emptyDocFunction} - borderRounding={returnZero} parentActive={returnTrue} whenActiveChanged={emptyFunction} ContainingCollectionView={undefined} />} diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index f98f6e8a5..be8d67925 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -97,7 +97,7 @@ export class MainOverlayTextBox extends React.Component style={{ width: `${this.TextDoc.Width()}px`, height: `${this.TextDoc.Height()}px` }}> + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyDocFunction} />
; } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index e65770137..87b767c93 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -190,7 +190,7 @@ export class CollectionBaseView extends React.Component { const viewtype = this.collectionViewType; return (
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index eb94a4e6d..3267f2455 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -368,7 +368,6 @@ export class DockedFrameRenderer extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyDocFunction} - borderRounding={returnZero} ContainingCollectionView={undefined} />
); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index dc2910ede..b6d5f1bfa 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -77,7 +77,6 @@ export class CollectionSchemaView extends CollectionSubView { focus: emptyDocFunction, active: returnFalse, whenActiveChanged: emptyFunction, - borderRounding: this.props.borderRounding, PanelHeight: returnZero, PanelWidth: returnZero, }; @@ -278,7 +277,6 @@ export class CollectionSchemaView extends CollectionSubView { focus={emptyDocFunction} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} - borderRounding={this.props.borderRounding} />
boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; - borderRounding: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index b9d1f0bd9..905b48db7 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -137,7 +137,7 @@ export class CollectionTreeView extends CollectionSubView { return (
e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9e2c9bf9a..56342c84c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -257,7 +257,6 @@ export class CollectionFreeFormView extends CollectionSubView { focus: this.focusDocument, parentActive: this.props.active, whenActiveChanged: this.props.active, - borderRounding: this.props.borderRounding, }; } @@ -289,17 +288,17 @@ export class CollectionFreeFormView extends CollectionSubView { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; return (
{/* {this.props.Document.Title} */} - - @@ -347,7 +346,6 @@ interface CollectionFreeFormViewPannableContentsProps { panX: () => number; panY: () => number; zoomScaling: () => number; - borderRadius: () => number; } @observer @@ -358,7 +356,7 @@ class CollectionFreeFormViewPannableContents extends React.Component otherwise, reactions won't fire - return
+ return
{this.props.children}
; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index e724ab47e..b511a6e3d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -24,7 +24,6 @@ interface MarqueeViewProps { selectDocuments: (docs: Document[]) => void; removeDocument: (doc: Document) => boolean; addLiveTextDocument: (doc: Document) => void; - borderRadius: () => number; } @observer @@ -251,7 +250,7 @@ export class MarqueeView extends React.Component } render() { - return
+ return
{this.props.children} {!this._visible ? (null) : this.marqueeDiv}
; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 8caf5ff31..4537ce3eb 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -65,7 +65,6 @@ export class CollectionFreeFormDocumentView extends React.Component; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ddf99c109..cec6e6468 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,7 +41,6 @@ export interface DocumentViewProps { parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; - borderRounding: () => number; } export interface JsxArgs extends DocumentViewProps { Keys: { [name: string]: Key }; @@ -278,7 +277,7 @@ export class DocumentView extends React.Component {
void; PanelWidth: () => number; PanelHeight: () => number; - borderRounding: () => number; } @observer @@ -103,7 +102,6 @@ export class FieldView extends React.Component { layoutKey={KeyStore.Layout} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} - borderRounding={returnZero} toggleMinimized={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} /> ); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 4b2fdbaff..5b17763e7 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -274,7 +274,7 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte return (
Date: Fri, 26 Apr 2019 21:14:50 -0400 Subject: More --- src/client/views/InkingCanvas.tsx | 29 +- src/client/views/InkingControl.tsx | 7 +- .../views/collections/CollectionTreeView.tsx | 51 ++- src/client/views/nodes/LinkEditor.tsx | 16 +- src/debug/Viewer.tsx | 368 ++++++++++----------- src/fields/ScriptField.ts | 174 +++++----- src/mobile/ImageUpload.tsx | 34 +- src/new_fields/Doc.ts | 4 +- .../authentication/controllers/WorkspacesMenu.tsx | 16 +- 9 files changed, 344 insertions(+), 355 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 47ee8eb85..1e26893c5 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 { 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; 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 { } 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 { } @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 { + 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) { + Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value)); } @action @@ -78,7 +77,7 @@ export class InkingCanvas extends React.Component { 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,7 +136,7 @@ export class InkingCanvas extends React.Component { @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( void; + document: Doc; + deleteDoc: (doc: Doc) => void; moveDocument: DragManager.MoveFunction; copyOnDrag: boolean; } @@ -43,9 +42,9 @@ class TreeView extends React.Component { @action remove = (document: Document) => { - var children = this.props.document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + var children = Cast(this.props.document.data, listSpec(Doc)); + if (children) { + children.splice(children.indexOf(document), 1); } } @@ -80,15 +79,15 @@ class TreeView extends React.Component { 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); + this.props.document.title = value; return true; }} />); return (
- {editableView(this.props.document.Title)} + {editableView(StrCast(this.props.document.title))}
); } @@ -96,12 +95,12 @@ class TreeView extends React.Component { render() { let bulletType = BulletType.List; let childElements: JSX.Element | undefined = undefined; - var children = this.props.document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { // add children for a collection + var children = Cast(this.props.document.data, listSpec(Doc)); + if (children) { // add children for a collection if (!this._collapsed) { bulletType = BulletType.Collapsible; childElements =
    - {children.Data.map(value => )} + {children.map(value => )}
; } else bulletType = BulletType.Collapsed; @@ -117,22 +116,22 @@ class TreeView extends React.Component { } @observer -export class CollectionTreeView extends CollectionSubView { +export class CollectionTreeView extends CollectionSubView(Document) { @action remove = (document: Document) => { - var children = this.props.Document.GetT>(KeyStore.Data, ListField); - if (children && children !== FieldWaiting) { - children.Data.splice(children.Data.indexOf(document), 1); + const children = this.children; + if (children) { + children.splice(children.indexOf(document), 1); } } render() { - let children = this.props.Document.GetT>(KeyStore.Data, ListField); - let copyOnDrag = this.props.Document.GetBoolean(KeyStore.CopyDraggedItems, false); - let childrenElement = !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - ) + const children = this.children; + let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); + let childrenElement = !children ? (null) : + (children.map(value => + ) ); return ( @@ -145,9 +144,9 @@ export class CollectionTreeView extends CollectionSubView { 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); + this.props.Document.title = value; return true; }} />
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index bde50fed8..f82c6e9cb 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -3,31 +3,29 @@ 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 { - @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); + this.props.linkDoc.title = this._nameInput; + this.props.linkDoc.linkDescription = this._descriptionInput; this.props.showLinks(); } 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 }> { - render() { - return {JSON.stringify(this.props.field.Data)} ({this.props.field.Id}); - } -} - -@observer -class KeyViewer extends React.Component<{ field: Key }> { - render() { - return this.props.field.Name; - } -} - -@observer -class ListViewer extends React.Component<{ field: ListField }>{ - @observable - expanded = false; - - render() { - let content; - if (this.expanded) { - content = ( -
- {this.props.field.Data.map(field => )} -
- ); - } else { - content = <>[...] ({this.props.field.Id}); - } - return ( -
- - {content} -
- ); - } -} - -@observer -class DocumentViewer extends React.Component<{ field: Document }> { - private keyMap: ObservableMap = 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 ( -
- ({key ? key.Name : kv[0]}): - -
- ); - }); - return ( -
- Document ({this.props.field.Id}) -
- {fields} -
-
- ); - } -} - -@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) => { - 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 = (); - } else if (this.field instanceof Document) { - content = (); - } else if (this.field instanceof BasicField) { - content = (); - } else if (this.field instanceof Key) { - content = (); - } else { - content = (Unrecognized field type); - } - } else if (this.error) { - content = Field {this.props.fieldId} not found ; - } else { - content = Field loading: {this.props.fieldId}; - } - return content; - } -} - -@observer -class Viewer extends React.Component { - @observable - private idToAdd: string = ''; - - @observable - private ids: string[] = []; - - @action - inputOnChange = (e: React.ChangeEvent) => { - this.idToAdd = e.target.value; - } - - @action - onKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - this.ids.push(this.idToAdd); - this.idToAdd = ""; - } - } - - render() { - return ( - <> - -
- {this.ids.map(id => )} -
- - ); - } -} - -ReactDOM.render(( -
- -
), - document.getElementById('root') -); \ No newline at end of file + +// configure({ +// enforceActions: "observed" +// }); + +// @observer +// class FieldViewer extends React.Component<{ field: BasicField }> { +// render() { +// return {JSON.stringify(this.props.field.Data)} ({this.props.field.Id}); +// } +// } + +// @observer +// class KeyViewer extends React.Component<{ field: Key }> { +// render() { +// return this.props.field.Name; +// } +// } + +// @observer +// class ListViewer extends React.Component<{ field: ListField }>{ +// @observable +// expanded = false; + +// render() { +// let content; +// if (this.expanded) { +// content = ( +//
+// {this.props.field.Data.map(field => )} +//
+// ); +// } else { +// content = <>[...] ({this.props.field.Id}); +// } +// return ( +//
+// +// {content} +//
+// ); +// } +// } + +// @observer +// class DocumentViewer extends React.Component<{ field: Document }> { +// private keyMap: ObservableMap = 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 ( +//
+// ({key ? key.Name : kv[0]}): +// +//
+// ); +// }); +// return ( +//
+// Document ({this.props.field.Id}) +//
+// {fields} +//
+//
+// ); +// } +// } + +// @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) => { +// 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 = (); +// } else if (this.field instanceof Document) { +// content = (); +// } else if (this.field instanceof BasicField) { +// content = (); +// } else if (this.field instanceof Key) { +// content = (); +// } else { +// content = (Unrecognized field type); +// } +// } else if (this.error) { +// content = Field {this.props.fieldId} not found ; +// } else { +// content = Field loading: {this.props.fieldId}; +// } +// return content; +// } +// } + +// @observer +// class Viewer extends React.Component { +// @observable +// private idToAdd: string = ''; + +// @observable +// private ids: string[] = []; + +// @action +// inputOnChange = (e: React.ChangeEvent) => { +// this.idToAdd = e.target.value; +// } + +// @action +// onKeyPress = (e: React.KeyboardEvent) => { +// if (e.key === "Enter") { +// this.ids.push(this.idToAdd); +// this.idToAdd = ""; +// } +// } + +// render() { +// return ( +// <> +// +//
+// {this.ids.map(id => )} +//
+// +// ); +// } +// } + +// ReactDOM.render(( +//
+// +//
), +// document.getElementById('root') +// ); \ 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 { - capturedIds: { [id: string]: string }; -} +// export interface SerializableOptions extends Without { +// 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/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; - if (field instanceof Document) { - pending = await field.GetTAsync(KeyStore.OptionalRightCollection, Document); + const field = await DocServer.GetRefField(res); + let pending: Opt; + 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/Doc.ts b/src/new_fields/Doc.ts index a5f495477..6dfa37c1b 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -166,6 +166,4 @@ export namespace Doc { return delegate; } export const Prototype = Symbol("Prototype"); -} - -export const GetAsync = Doc.GetAsync; \ No newline at end of file +} \ No newline at end of file diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx index b08c1aebe..29327e5ad 100644 --- a/src/server/authentication/controllers/WorkspacesMenu.tsx +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -2,15 +2,15 @@ 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'; +import { Doc, Id } from '../../../new_fields/Doc'; +import { StrCast } from '../../../new_fields/Types'; export interface WorkspaceMenuProps { - active: Document | undefined; - open: (workspace: Document) => void; + active: Doc | undefined; + open: (workspace: Doc) => void; new: () => void; - allWorkspaces: Document[]; + allWorkspaces: Doc[]; isShown: () => boolean; toggle: () => void; } @@ -60,7 +60,7 @@ export class WorkspacesMenu extends React.Component { /> {this.props.allWorkspaces.map((s, i) =>
{ e.preventDefault(); this.props.open(s); @@ -73,9 +73,9 @@ export class WorkspacesMenu extends React.Component { {i + 1} - s.Title} + GetValue={() => StrCast(s.title)} SetValue={(title: string): boolean => { - s.SetText(KeyStore.Title, title); + s.title = title; return true; }} contents={s.Title} -- cgit v1.2.3-70-g09d2 From d4a77dd055685dd81a762ef40e0c3b7606586e9c Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sat, 27 Apr 2019 21:40:17 -0400 Subject: Split more files up --- src/client/DocServer.ts | 3 +- src/client/northstar/dash-fields/HistogramField.ts | 2 +- src/client/northstar/dash-nodes/HistogramBox.tsx | 3 +- src/client/util/Scripting.ts | 1 + src/client/views/Main.tsx | 3 +- .../views/collections/CollectionBaseView.tsx | 3 +- .../views/collections/CollectionDockingView.tsx | 3 +- .../views/collections/CollectionSchemaView.tsx | 3 +- src/client/views/collections/CollectionSubView.tsx | 3 +- .../views/collections/CollectionTreeView.tsx | 3 +- .../CollectionFreeFormLinksView.tsx | 3 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 3 +- src/client/views/nodes/DocumentView.tsx | 3 +- src/client/views/nodes/FieldView.tsx | 1 + src/client/views/nodes/LinkMenu.tsx | 3 +- src/new_fields/Doc.ts | 37 +++------------------- src/new_fields/HtmlField.ts | 2 +- src/new_fields/IconField.ts | 2 +- src/new_fields/InkField.ts | 2 +- src/new_fields/List.ts | 3 +- src/new_fields/ObjectField.ts | 17 ++++++++++ src/new_fields/Proxy.ts | 4 ++- src/new_fields/RefField.ts | 18 +++++++++++ src/new_fields/RichTextField.ts | 2 +- src/new_fields/Schema.ts | 8 ++--- src/new_fields/Types.ts | 2 +- src/new_fields/URLField.ts | 2 +- src/new_fields/util.ts | 4 ++- .../authentication/controllers/WorkspacesMenu.tsx | 3 +- src/server/database.ts | 2 ++ 30 files changed, 88 insertions(+), 60 deletions(-) create mode 100644 src/new_fields/ObjectField.ts create mode 100644 src/new_fields/RefField.ts (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 3f17baec6..c7cbfce37 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,8 +1,9 @@ import * as OpenSocket from 'socket.io-client'; import { MessageStore, Types, Message } from "./../server/Message"; -import { Opt, FieldWaiting, RefField, HandleUpdate } from '../new_fields/Doc'; +import { Opt, FieldWaiting } from '../new_fields/Doc'; import { Utils } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; +import { RefField, HandleUpdate } from '../new_fields/RefField'; export namespace DocServer { const _cache: { [id: string]: RefField | Promise> } = {}; diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts index 118f4cf7f..730289536 100644 --- a/src/client/northstar/dash-fields/HistogramField.ts +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -3,7 +3,7 @@ import { custom, serializable } from "serializr"; import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel"; import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation"; -import { ObjectField } from "../../../new_fields/Doc"; +import { ObjectField } from "../../../new_fields/ObjectField"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { OmitKeys } from "../../../Utils"; import { Deserializable } from "../../util/SerializationHelper"; diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index 4a65b14cf..a9c68ccba 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -20,7 +20,8 @@ import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives"; import { StyleConstants } from "../utils/StyleContants"; import { NumCast, Cast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; -import { Doc, Id } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/RefField"; @observer diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index dbec82340..e45f61c11 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -11,6 +11,7 @@ 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; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 1e3d4e259..4a68d1c68 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -33,10 +33,11 @@ import { MainOverlayTextBox } from './MainOverlayTextBox'; import { DocumentView } from './nodes/DocumentView'; import { PreviewCursor } from './PreviewCursor'; import { SelectionManager } from '../util/SelectionManager'; -import { FieldResult, Field, Doc, Id, Opt } from '../../new_fields/Doc'; +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 diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 4807dc40a..b2fba1415 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -4,9 +4,10 @@ import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; -import { Doc, FieldResult, Opt, Id } from '../../../new_fields/Doc'; +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'; export enum CollectionViewType { Invalid, diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2ff409b9b..1574562c6 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -13,11 +13,12 @@ import React = require("react"); import { SubCollectionViewProps } from "./CollectionSubView"; import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; import { Transform } from '../../util/Transform'; -import { Doc, Id, Opt, Field, FieldId } from "../../../new_fields/Doc"; +import { Doc, Opt, Field } from "../../../new_fields/Doc"; import { Cast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; import { listSpec } from "../../../new_fields/Schema"; +import { Id, FieldId } from "../../../new_fields/RefField"; @observer export class CollectionDockingView extends React.Component { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 874170f3d..58d20819b 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -19,10 +19,11 @@ import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Opt, Field, Doc, Id } from "../../../new_fields/Doc"; +import { Opt, Field, Doc } 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 diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 558a8728f..4d090b680 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -10,12 +10,13 @@ import * as rp from 'request-promise'; import { CollectionView } from "./CollectionView"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { Doc, ObjectField, Opt } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, PromiseValue } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; +import { ObjectField } from "../../../new_fields/ObjectField"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index c9d8d83c8..7ec9a8549 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -10,7 +10,8 @@ import "./CollectionTreeView.scss"; import React = require("react"); import { Document, listSpec } from '../../../new_fields/Schema'; import { Cast, StrCast, BoolCast } from '../../../new_fields/Types'; -import { Doc, Id } from '../../../new_fields/Doc'; +import { Doc } from '../../../new_fields/Doc'; +import { Id } from '../../../new_fields/RefField'; export interface TreeViewProps { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index ce9995630..f693d55e8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -7,10 +7,11 @@ import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { Doc, Id } from "../../../../new_fields/Doc"; +import { Doc } 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 { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 047fbad18..18107e98a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -19,10 +19,11 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; -import { Doc, Id } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../new_fields/Doc"; import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types"; import { pageSchema } from "../../nodes/ImageBox"; import { List } from "../../../../new_fields/List"; +import { Id } from "../../../../new_fields/RefField"; export const panZoomSchema = createSchema({ panX: "number", diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index aabc1633e..c304b6a35 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -15,7 +15,7 @@ import { ContextMenu } from "../ContextMenu"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { Field, Opt, Doc, Id } from "../../../new_fields/Doc"; +import { Opt, Doc } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; import { FieldValue, Cast, PromiseValue } from "../../../new_fields/Types"; @@ -24,6 +24,7 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { MarqueeView } from "../collections/collectionFreeForm/MarqueeView"; import { DocServer } from "../../DocServer"; +import { Id } from "../../../new_fields/RefField"; const linkSchema = createSchema({ title: "string", diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c00c47fc4..dc36c5914 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -16,6 +16,7 @@ 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"; // diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 3ecc8555d..e21adebbc 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -5,9 +5,10 @@ import { LinkBox } from "./LinkBox"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc, Id } from "../../../new_fields/Doc"; +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; diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index fb7b6e360..4ef2a465f 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -8,38 +8,8 @@ import { Cast, ToConstructor, PromiseValue, FieldValue } from "./Types"; import { UndoManager, undoBatch } from "../client/util/UndoManager"; import { listSpec } from "./Schema"; import { List } from "./List"; - -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; -} - -export const Update = Symbol("Update"); -export const OnUpdate = Symbol("OnUpdate"); -export const Parent = Symbol("Parent"); -export class ObjectField { - protected [OnUpdate]?: (diff?: any) => void; - private [Parent]?: Doc; - readonly [Id] = ""; -} - -export namespace ObjectField { - export function MakeCopy(field: ObjectField) { - //TODO Types - return field; - } -} +import { ObjectField } from "./ObjectField"; +import { RefField, FieldId, Id } from "./RefField"; export function IsField(field: any): field is Field { return (typeof field === "string") @@ -53,6 +23,7 @@ export type Opt = T | undefined; export type FieldWaiting = T extends undefined ? never : Promise; export type FieldResult = Opt | FieldWaiting>; +export const Update = Symbol("Update"); export const Self = Symbol("Self"); @Deserializable("doc").withFields(["id"]) @@ -161,7 +132,7 @@ export namespace Doc { copy[key] = field; } } - }) + }); return copy; } diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts index 76fdb1f62..808a3499b 100644 --- a/src/new_fields/HtmlField.ts +++ b/src/new_fields/HtmlField.ts @@ -1,6 +1,6 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, primitive } from "serializr"; -import { ObjectField } from "./Doc"; +import { ObjectField } from "./ObjectField"; @Deserializable("html") export class HtmlField extends ObjectField { diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts index 32f3aa4d5..46f111f8e 100644 --- a/src/new_fields/IconField.ts +++ b/src/new_fields/IconField.ts @@ -1,6 +1,6 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, primitive } from "serializr"; -import { ObjectField } from "./Doc"; +import { ObjectField } from "./ObjectField"; @Deserializable("icon") export class IconField extends ObjectField { diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 49e6bf61e..42223c494 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -1,6 +1,6 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; -import { ObjectField } from "./Doc"; +import { ObjectField } from "./ObjectField"; export enum InkTool { None, diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index f01ac210a..428f661c9 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -1,8 +1,9 @@ import { Deserializable, autoObject } from "../client/util/SerializationHelper"; -import { Field, ObjectField, Update, OnUpdate, Self } from "./Doc"; +import { Field, Update, Self } from "./Doc"; import { setter, getter } from "./util"; import { serializable, alias, list } from "serializr"; import { observable } from "mobx"; +import { ObjectField, OnUpdate } from "./ObjectField"; @Deserializable("list") class ListImpl extends ObjectField { diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts new file mode 100644 index 000000000..9cac2c528 --- /dev/null +++ b/src/new_fields/ObjectField.ts @@ -0,0 +1,17 @@ +import { Doc } from "./Doc"; + +export const OnUpdate = Symbol("OnUpdate"); +export const Parent = Symbol("Parent"); +const Id = Symbol("Object Id"); +export class ObjectField { + protected [OnUpdate]?: (diff?: any) => void; + private [Parent]?: Doc; + readonly [Id] = ""; +} + +export namespace ObjectField { + export function MakeCopy(field: ObjectField) { + //TODO Types + return field; + } +} diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts index 2aa78731e..56e41cc0f 100644 --- a/src/new_fields/Proxy.ts +++ b/src/new_fields/Proxy.ts @@ -1,8 +1,10 @@ import { Deserializable } from "../client/util/SerializationHelper"; -import { RefField, Id, ObjectField, FieldWaiting } from "./Doc"; +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 } from "./ObjectField"; @Deserializable("proxy") export class ProxyField extends ObjectField { 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 index 156e4efd9..0fa3cf73c 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -1,4 +1,4 @@ -import { ObjectField } from "./Doc"; +import { ObjectField } from "./ObjectField"; import { serializable } from "serializr"; export class RichTextField extends ObjectField { diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts index 5081521c7..7444878fe 100644 --- a/src/new_fields/Schema.ts +++ b/src/new_fields/Schema.ts @@ -21,15 +21,15 @@ export function makeInterface(...schemas: T): (doc?: Doc) } } const proto = new Proxy({}, { - get(target: any, prop) { - const field = target.doc[prop]; + 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) { - target.doc[prop] = value; + set(target: any, prop, value, receiver) { + receiver.doc[prop] = value; return true; } }); diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 7fa18673f..3f8eabd45 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -1,4 +1,4 @@ -import { Field, Opt, FieldWaiting, FieldResult, RefField } from "./Doc"; +import { Field, Opt, FieldResult } from "./Doc"; import { List } from "./List"; export type ToType | ListSpec> = diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts index 1da245e73..95c679df7 100644 --- a/src/new_fields/URLField.ts +++ b/src/new_fields/URLField.ts @@ -1,6 +1,6 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom } from "serializr"; -import { ObjectField } from "./Doc"; +import { ObjectField } from "./ObjectField"; function url() { return custom( diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 2d9721b2e..011e8c8d9 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -1,8 +1,10 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Update, OnUpdate, Parent, ObjectField, RefField, Doc, Id, Field } from "./Doc"; +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"; export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx index 29327e5ad..91756315d 100644 --- a/src/server/authentication/controllers/WorkspacesMenu.tsx +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -3,8 +3,9 @@ import { observable, action, configure, reaction, computed, ObservableMap, runIn import { observer } from "mobx-react"; import './WorkspacesMenu.css'; import { EditableView } from '../../../client/views/EditableView'; -import { Doc, Id } from '../../../new_fields/Doc'; +import { Doc } from '../../../new_fields/Doc'; import { StrCast } from '../../../new_fields/Types'; +import { Id } from '../../../new_fields/RefField'; export interface WorkspaceMenuProps { active: Doc | undefined; diff --git a/src/server/database.ts b/src/server/database.ts index a61b4d823..6b3b6797f 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -60,11 +60,13 @@ export class Database { } public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { + console.log("getDocument"); this.db && this.db.collection(collectionName).findOne({ id: id }, (err, result) => fn(result ? ({ id: result._id, type: result.type, data: result.data }) : undefined)); } public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) { + console.log("getDocuments"); this.db && this.db.collection(collectionName).find({ id: { "$in": ids } }).toArray((err, docs) => { if (err) { console.log(err.message); -- cgit v1.2.3-70-g09d2 From ecb4c692c6f317c4108cb065874003fc51242f7a Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 May 2019 20:43:26 -0400 Subject: made tree view work for docking views --- src/client/documents/Documents.ts | 5 +++-- src/client/views/Main.tsx | 11 ++++++----- .../views/collections/CollectionDockingView.tsx | 22 +++++++++++++++++----- .../views/collections/CollectionTreeView.tsx | 9 +++++++-- src/client/views/nodes/DocumentView.tsx | 4 +++- 5 files changed, 36 insertions(+), 15 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 4045fb22b..6ca2567dc 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -57,6 +57,7 @@ export interface DocumentOptions { documentText?: string; borderRounding?: number; schemaColumns?: List; + dockingConfig?: string; // [key: string]: Opt; } const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; @@ -247,8 +248,8 @@ export namespace Docs { export function TreeDocument(documents: Array, options: DocumentOptions) { return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree }); } - export function DockDocument(config: string, options: DocumentOptions) { - return CreateInstance(collProto, config, { ...options, viewType: CollectionViewType.Docking }); + export function DockDocument(documents: Array, config: string, options: DocumentOptions) { + return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }); } export function CaptionDocument(doc: Doc) { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 3f8314d5f..2636b08bd 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -150,7 +150,7 @@ export class Main extends React.Component { if (list) { let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; - let mainDoc = Docs.DockDocument(JSON.stringify(dockingLayout), { title: `Main Container ${list.length + 1}` }); + let mainDoc = Docs.DockDocument([freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }); list.push(mainDoc); CurrentUserUtils.MainDocId = mainDoc[Id]; // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) @@ -208,8 +208,8 @@ export class Main extends React.Component { } /* 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/"; @@ -219,7 +219,8 @@ export class Main extends React.Component { 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(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); + let addTreeNode = action(() => Docs.TreeDocument([this.mainContainer!], { width: 250, height: 400, title: "Library:" + StrCast(this.mainContainer!.title), copyDraggedItems: true })); + // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); 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" })); @@ -294,7 +295,7 @@ export class Main extends React.Component { {this.mainContent} - {this.nodesMenu} + {this.nodesMenu()} {this.miscButtons} {this.workspaceMenu} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6c8f3844a..2facd404f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,7 +1,7 @@ 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, trace, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; @@ -14,7 +14,7 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; import { Transform } from '../../util/Transform'; import { Doc, Opt, Field } from "../../../new_fields/Doc"; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; import { listSpec } from "../../../new_fields/Schema"; @@ -87,6 +87,10 @@ export class CollectionDockingView extends React.Component void = () => { if (this._containerRef.current) { reaction( - () => Cast(this.props.Document.data, "string", ""), + () => StrCast(this.props.Document.dockingConfig), () => { if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { setTimeout(() => this.setupGoldenLayout(), 1); @@ -238,7 +242,7 @@ export class CollectionDockingView extends React.Component { var json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.data = json; + this.props.Document.dockingConfig = json; if (this.undohack && !this.hack) { this.undohack.end(); this.undohack = undefined; @@ -285,6 +289,14 @@ export class CollectionDockingView extends React.Component runInAction(() => { + if (f instanceof Doc) { + let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); + docs && docs.map((d, i) => d[Id] === f[Id] && docs!.splice(i, 1)); + // bcz: this seems like it should work, but it only does occasionally -- usually I get -1 + // docs && docs.indexOf(f) !== -1 && docs.splice(docs.indexOf(f), 1); + } + })); tab.contentItem.remove(); }); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 7ec9a8549..1a6f1121d 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -12,6 +12,7 @@ import { Document, listSpec } from '../../../new_fields/Schema'; import { Cast, StrCast, BoolCast } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; +import { Utils } from '../../../Utils'; export interface TreeViewProps { @@ -101,7 +102,9 @@ class TreeView extends React.Component { if (!this._collapsed) { bulletType = BulletType.Collapsible; childElements =
    - {children.map(value => )} + {/* // bcz: should this work? + {children.map(value => )} */} + {children.map(value => )}
; } else bulletType = BulletType.Collapsed; @@ -132,7 +135,9 @@ export class CollectionTreeView extends CollectionSubView(Document) { let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); let childrenElement = !children ? (null) : (children.map(value => - ) + //bcz: shouldn't this work? - I think value[Id] is undefined sometimes + // ) + ) ); return ( diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f58dc4a02..b35d68c4b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -198,7 +198,9 @@ export class DocumentView extends DocComponent(Docu CollectionDockingView.Instance.AddRightSplit(kvp); } fullScreenClicked = (e: React.MouseEvent): void => { - const doc = Doc.MakeDelegate(FieldValue(this.Document.proto)); + const doc = Doc.MakeDelegate(FieldValue(this.props.Document.proto)); + // bcz .. should this work? + // const doc = Doc.MakeDelegate(FieldValue(this.Document.proto)); if (doc) { CollectionDockingView.Instance.OpenFullScreen(doc); } -- cgit v1.2.3-70-g09d2 From 6d52cfb2a7b2b575f448e7d7c3f02a97afa0027b Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 1 May 2019 22:38:49 -0400 Subject: fixed drag drop making source documents disappear. improved TreeView collapsing. Chnaged schemaView options placement --- src/client/util/DragManager.ts | 17 +++++++++++------ src/client/views/collections/CollectionSchemaView.tsx | 2 +- src/client/views/collections/CollectionTreeView.tsx | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index a149da52f..4acf609f4 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -301,15 +301,20 @@ export namespace DragManager { function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { let removed = dragEles.map(dragEle => { - let 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); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 1f26941c2..67784fa81 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -295,7 +295,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get tableOptionsPanel() { return !this.props.active() ? (null) : (
Options
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 1a6f1121d..2a620a5e6 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -104,7 +104,7 @@ class TreeView extends React.Component { childElements =
    {/* // bcz: should this work? {children.map(value => )} */} - {children.map(value => )} + {children.map(value => )}
; } else bulletType = BulletType.Collapsed; @@ -137,7 +137,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { (children.map(value => //bcz: shouldn't this work? - I think value[Id] is undefined sometimes // ) - ) + ) ); return ( -- cgit v1.2.3-70-g09d2 From be3719bbbede85b4dd099f436b4f7d3bade157cb Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 1 May 2019 23:49:23 -0400 Subject: Fixed treeview stuff --- src/client/views/collections/CollectionSubView.tsx | 6 ++++-- src/client/views/collections/CollectionTreeView.tsx | 9 ++------- 2 files changed, 6 insertions(+), 9 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 2c2d74302..bcbde24f0 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -13,7 +13,7 @@ import { CollectionVideoView } from "./CollectionVideoView"; import { Doc, Opt } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, PromiseValue } from "../../../new_fields/Types"; +import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; import { ObjectField } from "../../../new_fields/ObjectField"; @@ -48,7 +48,9 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } get children() { - return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)); + //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)); } @action diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2a620a5e6..f148c2b2f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -102,9 +102,7 @@ class TreeView extends React.Component { if (!this._collapsed) { bulletType = BulletType.Collapsible; childElements =
    - {/* // bcz: should this work? - {children.map(value => )} */} - {children.map(value => )} + {children.map(value => )}
; } else bulletType = BulletType.Collapsed; @@ -135,10 +133,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); let childrenElement = !children ? (null) : (children.map(value => - //bcz: shouldn't this work? - I think value[Id] is undefined sometimes - // ) - ) - ); + )); return (
Date: Thu, 2 May 2019 12:19:31 -0400 Subject: library and workspace stuff. --- src/client/views/Main.tsx | 28 ++----- src/client/views/TemplateMenu.tsx | 7 +- .../views/collections/CollectionDockingView.tsx | 27 ++----- .../views/collections/CollectionTreeView.scss | 1 - .../views/collections/CollectionTreeView.tsx | 57 ++++++++++---- src/client/views/collections/CollectionView.tsx | 1 + src/client/views/nodes/DocumentView.tsx | 10 +-- .../authentication/controllers/WorkspacesMenu.css | 3 - .../authentication/controllers/WorkspacesMenu.tsx | 90 ---------------------- 9 files changed, 60 insertions(+), 164 deletions(-) delete mode 100644 src/server/authentication/controllers/WorkspacesMenu.css delete mode 100644 src/server/authentication/controllers/WorkspacesMenu.tsx (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 4e918221c..b2ca4a196 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -100,8 +100,7 @@ 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]; - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt) => { + DocServer.GetRefField(pathname[pathname.length - 1]).then(action((field: Opt) => { if (field instanceof Doc) { this.openWorkspace(field, true); } @@ -132,7 +131,6 @@ export class Main extends React.Component { if (!CurrentUserUtils.MainDocId) { const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc); if (doc) { - CurrentUserUtils.MainDocId = doc[Id]; this.openWorkspace(doc); } else { this.createNewWorkspace(); @@ -148,11 +146,12 @@ export class Main extends React.Component { createNewWorkspace = async (id?: string) => { const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); if (list) { + let libraryDoc = Docs.TreeDocument([CurrentUserUtils.UserDocument], { x: 0, y: 400, title: `Library: ${CurrentUserUtils.email} ${list.length + 1}` }); + libraryDoc.excludeFromLibrary = true; let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; - let mainDoc = Docs.DockDocument([freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(libraryDoc, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; + let mainDoc = Docs.DockDocument([libraryDoc, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }); list.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); @@ -164,6 +163,7 @@ export class Main extends React.Component { @action openWorkspace = async (doc: Doc, fromHistory = false) => { + CurrentUserUtils.MainDocId = doc[Id]; this.mainContainer = doc; fromHistory || window.history.pushState(null, StrCast(doc.title), "/doc/" + doc[Id]); const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc); @@ -259,9 +259,7 @@ 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(); let logoutRef = React.createRef(); - let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown); return [ , @@ -270,24 +268,11 @@ export class Main extends React.Component {
, -
-
,
]; } - @computed - get workspaceMenu() { - let areWorkspacesShown = () => this._workspacesShown; - let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown); - let workspaces = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); - return (!workspaces || !this.mainContainer) ? (null) : - ; - } - render() { return (
@@ -297,7 +282,6 @@ export class Main extends React.Component { {this.nodesMenu()} {this.miscButtons} - {this.workspaceMenu}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 7be846e05..f29d9c8a1 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -59,12 +59,9 @@ export class TemplateMenu extends React.Component { } render() { - trace(); let templateMenu: Array = []; - this.props.templates.forEach((checked, template) => { - console.log("checked + " + checked + " " + this.props.templates.get(template)); - templateMenu.push(); - }); + this.props.templates.forEach((checked, template) => + templateMenu.push()); return (
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 014773ab6..cfb1aef7d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -19,15 +19,17 @@ import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; import { listSpec } from "../../../new_fields/Schema"; import { Id, FieldId } from "../../../new_fields/RefField"; +import { faSignInAlt } from "@fortawesome/free-solid-svg-icons"; @observer export class CollectionDockingView extends React.Component { public static Instance: CollectionDockingView; - public static makeDocumentConfig(document: Doc) { + public static makeDocumentConfig(document: Doc, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', title: document.title, + width: width, props: { documentId: document[Id], //collectionDockingView: CollectionDockingView.Instance @@ -37,7 +39,6 @@ export class CollectionDockingView extends React.Component(); - private _fullScreen: any = null; private _flush: boolean = false; private _ignoreStateChange = ""; @@ -67,20 +68,9 @@ export class CollectionDockingView extends React.Component { - _mainCont = React.createRef(); @observable private _panelWidth = 0; @observable private _panelHeight = 0; @@ -347,10 +336,7 @@ export class DockedFrameRenderer extends React.Component { const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); let wscale = this._panelWidth / nativeW; - if (wscale * nativeH > this._panelHeight) { - return this._panelHeight / nativeH; - } - return wscale; + return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; } ScreenToLocalTransform = () => { @@ -364,6 +350,8 @@ export class DockedFrameRenderer extends React.Component { get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } get content() { + if (!this._document) + return (null); return (
@@ -385,9 +373,10 @@ export class DockedFrameRenderer extends React.Component { } render() { + let theContent = this.content; return !this._document ? (null) : { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> - {({ measureRef }) =>
{this.content}
} + {({ measureRef }) =>
{theContent}
}
; } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 95df5edb9..ecb5f78bb 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -41,7 +41,6 @@ .coll-title { font-size: 24px; - margin-bottom: 20px; } .docContainer { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index f148c2b2f..0520e0f88 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -9,10 +9,15 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); import { Document, listSpec } from '../../../new_fields/Schema'; -import { Cast, StrCast, BoolCast } from '../../../new_fields/Types'; +import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; import { Utils } from '../../../Utils'; +import { JSXElement } from 'babel-types'; +import { ContextMenu } from '../ContextMenu'; +import { undoBatch } from '../../util/UndoManager'; +import { Main } from '../Main'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; export interface TreeViewProps { @@ -42,11 +47,14 @@ class TreeView extends React.Component { delete = () => this.props.deleteDoc(this.props.document); + get children() { + return Cast(this.props.document.data, listSpec(Doc), []).filter(doc => FieldValue(doc)); + } + @action remove = (document: Document) => { - var children = Cast(this.props.document.data, listSpec(Doc)); - if (children) { - children.splice(children.indexOf(document), 1); + if (this.children) { + this.children.splice(this.children.indexOf(document), 1); } } @@ -94,27 +102,38 @@ class TreeView extends React.Component {
); } + 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 + if (!ContextMenu.Instance.getItems().some(item => item.description === "Open as Workspace")) { + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); + } + } + } render() { let bulletType = BulletType.List; - let childElements: JSX.Element | undefined = undefined; + let contentElement: JSX.Element | null = (null); var children = Cast(this.props.document.data, listSpec(Doc)); if (children) { // add children for a collection if (!this._collapsed) { bulletType = BulletType.Collapsible; - childElements =
    - {children.map(value => )} + contentElement =
      + {TreeView.GetChildElements(children, this.remove, this.move, this.props.copyOnDrag)}
    ; } else bulletType = BulletType.Collapsed; } - return
    + return
  • {this.renderBullet(bulletType)} {this.renderTitle()} - {childElements ? childElements : (null)} + {contentElement}
  • ; } + public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, copyOnDrag: boolean) { + return docs.filter(child => !child.excludeFromLibrary).map(child => + ); + } } @observer @@ -128,21 +147,30 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } + 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()) }); + } + } render() { const children = this.children; let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); - let childrenElement = !children ? (null) : - (children.map(value => - )); + if (!children) { + return (null); + } + let testForLibrary = children && children.length === 1 && children[0] === CurrentUserUtils.UserDocument; + var subchildren = testForLibrary ? Cast(children[0].data, listSpec(Doc), children) : children; + let childElements = TreeView.GetChildElements(subchildren, this.remove, this.props.moveDocument, copyOnDrag); return (
    e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
    StrCast(this.props.Document.title)} @@ -151,9 +179,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { return true; }} />
    -
      - {childrenElement} + {childElements}
    ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c2049a09a..8c1442d38 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -11,6 +11,7 @@ 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 { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5588fa1ac..86d34725d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -205,16 +205,8 @@ export class DocumentView extends DocComponent(Docu CollectionDockingView.Instance.OpenFullScreen(doc); } 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(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + SelectionManager.DeselectAll(); } - @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { 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 91756315d..000000000 --- a/src/server/authentication/controllers/WorkspacesMenu.tsx +++ /dev/null @@ -1,90 +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 { EditableView } from '../../../client/views/EditableView'; -import { Doc } from '../../../new_fields/Doc'; -import { StrCast } from '../../../new_fields/Types'; -import { Id } from '../../../new_fields/RefField'; - -export interface WorkspaceMenuProps { - active: Doc | undefined; - open: (workspace: Doc) => void; - new: () => void; - allWorkspaces: Doc[]; - isShown: () => boolean; - toggle: () => void; -} - -@observer -export class WorkspacesMenu extends React.Component { - constructor(props: WorkspaceMenuProps) { - super(props); - this.addNewWorkspace = this.addNewWorkspace.bind(this); - } - - @action - addNewWorkspace() { - this.props.new(); - this.props.toggle(); - } - - render() { - return ( -
    - - {this.props.allWorkspaces.map((s, i) => -
    { - e.preventDefault(); - this.props.open(s); - }} - style={{ - marginTop: 10, - color: s === this.props.active ? "red" : "black" - }} - > - {i + 1} - - StrCast(s.title)} - SetValue={(title: string): boolean => { - s.title = title; - return true; - }} - contents={s.Title} - height={20} - /> -
    - )} -
    - ); - } -} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 1ece3904b6b3fbf4ddde29e93e31a30f3898e720 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 2 May 2019 13:20:27 -0400 Subject: cleaned up drag code - userDropAction takes precedence over dropAction. default is alias --- src/client/documents/Documents.ts | 3 ++- src/client/util/DragManager.ts | 19 ++++++++++------ src/client/views/DocumentDecorations.tsx | 1 - src/client/views/EditableView.tsx | 2 +- src/client/views/Main.tsx | 14 +++++------- src/client/views/collections/CollectionSubView.tsx | 6 ++--- .../views/collections/CollectionTreeView.scss | 3 ++- .../views/collections/CollectionTreeView.tsx | 26 +++++++++++----------- src/client/views/nodes/DocumentView.tsx | 12 +++++----- src/client/views/nodes/FormattedTextBox.tsx | 6 +++-- 10 files changed, 49 insertions(+), 43 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2de389a3c..8706359e4 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -32,6 +32,7 @@ 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"; export interface DocumentOptions { x?: number; @@ -51,7 +52,7 @@ export interface DocumentOptions { templates?: List; viewType?: number; backgroundColor?: string; - copyDraggedItems?: boolean; + dropAction?: dropActionType; backgroundLayout?: string; curPage?: number; documentText?: string; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 4acf609f4..a3dbe6e43 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,14 +1,14 @@ import { action } from "mobx"; 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"; import { Doc } from "../../new_fields/Doc"; import { Cast } from "../../new_fields/Types"; import { listSpec } from "../../new_fields/Schema"; -export function SetupDrag(_reference: React.RefObject, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) { +export type dropActionType = "alias" | "copy" | undefined; +export function SetupDrag(_reference: React.RefObject, docFunc: () => Doc, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) { let onRowMove = action((e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -16,7 +16,7 @@ export function SetupDrag(_reference: React.RefObject, 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); }); @@ -146,15 +146,20 @@ export namespace DragManager { droppedDocuments: Doc[]; xOffset: number; yOffset: number; - aliasOnDrop?: boolean; - copyOnDrop?: boolean; + dropAction: dropActionType; + userDropAction: dropActionType; moveDocument?: MoveFunction; [id: string]: any; } export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) { StartDrag(eles, dragData, downX, downY, options, - (dropData: { [id: string]: any }) => (dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) : dragData.copyOnDrop ? dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) : 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 { @@ -252,7 +257,7 @@ 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(); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7dcd71495..d07adcb71 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -144,7 +144,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); dragData.xOffset = xoff; dragData.yOffset = yoff; - dragData.aliasOnDrop = false; dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; this.Interacting = true; this._hidden = true; 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 { return (
    this.editing = true)} > - {this.props.contents} + {this.props.contents}
    ); } diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b2ca4a196..a07a2d5b1 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,9 +8,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import Measure from 'react-measure'; import * as request from 'request'; -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 { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; import { Docs } from '../documents/Documents'; @@ -86,11 +84,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; } @@ -219,8 +217,8 @@ export class Main extends React.Component { 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, copyDraggedItems: true })); - // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); + 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" })); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index bcbde24f0..a59bc196e 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -83,13 +83,13 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.data instanceof DragManager.DocumentDragData) { - if (de.data.aliasOnDrop || de.data.copyOnDrop) { + 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.aliasOnDrop || de.data.copyOnDrop) { + if (de.data.dropAction || de.data.userDropAction) { added = de.data.droppedDocuments.reduce((added: boolean, d) => { let moved = this.props.addDocument(d); return moved || added; @@ -129,7 +129,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } if (type.indexOf("excel") !== -1) { ctor = Docs.DBDocument; - options.copyDraggedItems = true; + options.dropAction = "copy"; } if (type.indexOf("html") !== -1) { if (path.includes('localhost')) { diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index ecb5f78bb..d00785879 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -45,7 +45,8 @@ .docContainer { margin-left: 10px; - display: inline-table; + display: block; + width: max-content; } .docContainer:hover { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 0520e0f88..037703c46 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -3,7 +3,7 @@ import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-s import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { DragManager, SetupDrag } from "../../util/DragManager"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { EditableView } from "../EditableView"; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; @@ -12,8 +12,6 @@ import { Document, listSpec } from '../../../new_fields/Schema'; import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; -import { Utils } from '../../../Utils'; -import { JSXElement } from 'babel-types'; import { ContextMenu } from '../ContextMenu'; import { undoBatch } from '../../util/UndoManager'; import { Main } from '../Main'; @@ -24,7 +22,7 @@ export interface TreeViewProps { document: Doc; deleteDoc: (doc: Doc) => void; moveDocument: DragManager.MoveFunction; - copyOnDrag: boolean; + dropAction: "alias" | "copy" | undefined; } export enum BulletType { @@ -83,7 +81,7 @@ class TreeView extends React.Component { */ renderTitle() { let reference = React.createRef(); - 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) => ( { height={36} GetValue={() => StrCast(this.props.document.title)} SetValue={(value: string) => { - this.props.document.title = value; + let target = this.props.document.proto ? this.props.document.proto : this.props.document; + target.title = value; return true; }} />); return (
    {editableView(StrCast(this.props.document.title))} -
    + {/*
    */}
    ); } @@ -117,7 +116,7 @@ class TreeView extends React.Component { if (!this._collapsed) { bulletType = BulletType.Collapsible; contentElement =
      - {TreeView.GetChildElements(children, this.remove, this.move, this.props.copyOnDrag)} + {TreeView.GetChildElements(children, this.remove, this.move, this.props.dropAction)}
    ; } else bulletType = BulletType.Collapsed; @@ -130,9 +129,9 @@ class TreeView extends React.Component {
    ; } - public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, copyOnDrag: boolean) { + public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { return docs.filter(child => !child.excludeFromLibrary).map(child => - ); + ); } } @@ -154,13 +153,13 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { const children = this.children; - let copyOnDrag = BoolCast(this.props.Document.copyDraggedItems, false); + let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; if (!children) { return (null); } let testForLibrary = children && children.length === 1 && children[0] === CurrentUserUtils.UserDocument; var subchildren = testForLibrary ? Cast(children[0].data, listSpec(Doc), children) : children; - let childElements = TreeView.GetChildElements(subchildren, this.remove, this.props.moveDocument, copyOnDrag); + let childElements = TreeView.GetChildElements(subchildren, this.remove, this.props.moveDocument, dropAction); return (
    StrCast(this.props.Document.title)} SetValue={(value: string) => { - this.props.Document.title = value; + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = value; return true; }} />
    diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 86d34725d..b151657e1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { emptyFunction, Utils } from "../../../Utils"; 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"; @@ -129,12 +129,12 @@ export class DocumentView extends DocComponent(Docu e.stopPropagation(); } - startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType) { if (this._mainCont.current) { const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData([this.props.Document]); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); - dragData.aliasOnDrop = dropAliasOfDraggedDoc; + dragData.dropAction = dropAction; dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; @@ -142,7 +142,7 @@ export class DocumentView extends DocComponent(Docu handlers: { dragComplete: action(emptyFunction) }, - hideSource: !dropAliasOfDraggedDoc + hideSource: !dropAction }); } } @@ -162,7 +162,7 @@ export class DocumentView extends DocComponent(Docu } if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined); } else { CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); } @@ -180,7 +180,7 @@ export class DocumentView extends DocComponent(Docu 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); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined); } } 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 diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 7f269a505..1d362bdaa 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -89,7 +89,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (title && title.startsWith("-") && this._editorView) { let str = this._editorView.state.doc.textContent; let titlestr = str.substr(0, Math.min(40, str.length)); - this.props.Document.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); }; } } @@ -275,7 +276,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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)); - this.props.Document.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } render() { -- cgit v1.2.3-70-g09d2 From 7dcd9dac57f848271ef6fa96f66064382331ee13 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 2 May 2019 14:17:15 -0400 Subject: added some hover highlighting. --- .../views/collections/CollectionTreeView.tsx | 28 ++++++++++++++++------ .../views/nodes/CollectionFreeFormDocumentView.tsx | 9 ++++++- src/client/views/nodes/DocumentView.tsx | 5 ++-- 3 files changed, 32 insertions(+), 10 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 037703c46..3c8cfcffd 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -46,13 +46,14 @@ class TreeView extends React.Component { delete = () => this.props.deleteDoc(this.props.document); get children() { - return Cast(this.props.document.data, listSpec(Doc), []).filter(doc => FieldValue(doc)); + return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); } @action remove = (document: Document) => { - if (this.children) { - this.children.splice(this.children.indexOf(document), 1); + let children = Cast(this.props.document.data, listSpec(Doc), []); + if (children) { + children.splice(children.indexOf(document), 1); } } @@ -106,8 +107,15 @@ class TreeView extends React.Component { if (!ContextMenu.Instance.getItems().some(item => item.description === "Open as Workspace")) { ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); } + if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { + ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + } } } + + 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 contentElement: JSX.Element | null = (null); @@ -121,7 +129,12 @@ class TreeView extends React.Component { } else bulletType = BulletType.Collapsed; } - return
    + return
  • {this.renderBullet(bulletType)} {this.renderTitle()} @@ -137,19 +150,20 @@ class TreeView extends React.Component { @observer export class CollectionTreeView extends CollectionSubView(Document) { - @action remove = (document: Document) => { - const children = this.children; + 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() { const children = this.children; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 27928d00b..09f9c8516 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -6,7 +6,7 @@ import "./DocumentView.scss"; import React = require("react"); import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, NumCast } from "../../../new_fields/Types"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { matchedData } from "express-validator/filter"; @@ -155,6 +155,9 @@ export class CollectionFreeFormDocumentView extends DocComponent { 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 : @@ -180,8 +183,12 @@ export class CollectionFreeFormDocumentView extends DocComponent(Docu 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: "Delete", event: this.deleteClicked }); + ContextMenu.Instance.addItem({ description: "Delete" + this.props.Document.title, event: this.deleteClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); if (!SelectionManager.IsSelected(this)) { SelectionManager.SelectDoc(this, false); } } + isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); -- cgit v1.2.3-70-g09d2 From a79bda7e5c7e77e398a33b8262f861155c5f1462 Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 2 May 2019 14:34:33 -0400 Subject: fixed some issues with contextmenus for tree view --- src/client/views/collections/CollectionTreeView.tsx | 18 ++++++++++-------- src/client/views/nodes/DocumentView.tsx | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 3c8cfcffd..48b226615 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -16,6 +16,8 @@ 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'; export interface TreeViewProps { @@ -104,12 +106,14 @@ class TreeView extends React.Component { 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 - if (!ContextMenu.Instance.getItems().some(item => item.description === "Open as Workspace")) { - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); - } - if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { - ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + 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(); } } @@ -130,9 +134,7 @@ class TreeView extends React.Component { else bulletType = BulletType.Collapsed; } return
  • diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3c8804415..8c245ab3a 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -268,7 +268,7 @@ export class DocumentView extends DocComponent(Docu 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: "Delete" + this.props.Document.title, event: this.deleteClicked }); + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); if (!SelectionManager.IsSelected(this)) { SelectionManager.SelectDoc(this, false); -- cgit v1.2.3-70-g09d2 From a78282cdf7fbb99386484640e1fb89d4b9b0cbee Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 2 May 2019 16:19:07 -0400 Subject: fixed some things with trees and summaries. --- src/client/views/Main.tsx | 3 +-- src/client/views/Templates.tsx | 12 +++++++++++- src/client/views/collections/CollectionTreeView.tsx | 10 +++++++--- .../views/collections/collectionFreeForm/MarqueeView.tsx | 4 +++- src/client/views/nodes/DocumentContentsView.tsx | 3 ++- src/client/views/nodes/FieldView.tsx | 11 ++++------- src/client/views/nodes/ImageBox.scss | 2 ++ src/new_fields/List.ts | 6 +++--- src/server/authentication/models/current_user_utils.ts | 2 ++ 9 files changed, 35 insertions(+), 18 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index a07a2d5b1..97eb73d7f 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -144,8 +144,7 @@ export class Main extends React.Component { createNewWorkspace = async (id?: string) => { const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); if (list) { - let libraryDoc = Docs.TreeDocument([CurrentUserUtils.UserDocument], { x: 0, y: 400, title: `Library: ${CurrentUserUtils.email} ${list.length + 1}` }); - libraryDoc.excludeFromLibrary = true; + let libraryDoc = await (CurrentUserUtils.UserDocument.library as Doc); let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` }); var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(libraryDoc, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; let mainDoc = Docs.DockDocument([libraryDoc, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }); diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index e17dd2354..668ae5312 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -54,7 +54,17 @@ export namespace Templates { `
    {layout}
    {props.Document.title}
    ` ); export const Summary = new Template("Title", TemplatePosition.InnerTop, - `
    {layout}
    {props.Document.doc1.title}
    ` + `
    +
    + {layout} +
    +
    + +
    +
    + +
    +
    ` ); export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption]; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 48b226615..2cef1462b 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,7 +1,7 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable } from "mobx"; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { EditableView } from "../EditableView"; @@ -145,8 +145,11 @@ class TreeView extends React.Component {
  • ; } public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { - return docs.filter(child => !child.excludeFromLibrary).map(child => - ); + return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child => { + console.log("child = " + child[Id]); + return + } + ); } } @@ -168,6 +171,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } render() { + trace(); const children = this.children; let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; if (!children) { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c58e7780c..82027a6f2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -180,7 +180,9 @@ export class MarqueeView extends React.Component // SelectionManager.DeselectAll(); if (e.key === "r") { let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - summary.doc1 = newCollection.proto!; + summary.doc1 = selected[0]; + if (selected.length > 1) + summary.doc2 = selected[1]; summary.templates = new List([Templates.Summary.Layout]); this.props.addLiveTextDocument(summary); e.preventDefault(); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 24e8a36ae..ddfe79a5c 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -15,6 +15,7 @@ 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"); @@ -63,7 +64,7 @@ export class DocumentContentsView extends React.Component { } render() { const field = this.field; - if (!field) { + if (field === undefined) { return

    {''}

    ; } - if (typeof field === "string") { - return

    {field}

    ; - } + // if (typeof field === "string") { + // return

    {field}

    ; + // } else if (field instanceof RichTextField) { return ; } @@ -108,9 +108,6 @@ export class FieldView extends React.Component { // else if (field instanceof HtmlField) { // return // } - else if (typeof field === "number") { - return

    {field}

    ; - } else if (!(field instanceof Promise)) { return

    {JSON.stringify(field)}

    ; } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 9fe211df0..2316a050e 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -10,6 +10,8 @@ } .imageBox-cont-interactive { pointer-events: all; + width:100%; + height:auto; } .imageBox-dot { diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index 1c4b96c81..db7932cec 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -2,7 +2,7 @@ import { Deserializable, autoObject } from "../client/util/SerializationHelper"; import { Field, Update, Self, FieldResult } from "./Doc"; import { setter, getter, deleteProperty } from "./util"; import { serializable, alias, list } from "serializr"; -import { observable } from "mobx"; +import { observable, action } from "mobx"; import { ObjectField, OnUpdate, Copy } from "./ObjectField"; import { RefField } from "./RefField"; import { ProxyField } from "./Proxy"; @@ -25,12 +25,12 @@ const listHandlers: any = { this[Update](); return field; }, - push(...items: any[]) { + push: action(function (this: any, ...items: any[]) { items = items.map(toObjectField); const res = this[Self].__fields.push(...items); this[Update](); return res; - }, + }), reverse() { const res = this[Self].__fields.reverse(); this[Update](); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 9db470ca0..93c2afb1d 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -26,6 +26,8 @@ export class CurrentUserUtils { doc.title = this.email; doc.data = new List(); doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" }); + doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); + (doc.library as Doc).excludeFromLibrary = true; return doc; } -- cgit v1.2.3-70-g09d2 From ac9b42dba420139c0a7fad5685425b3cf9c1570e Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 2 May 2019 16:58:14 -0400 Subject: clean up --- src/client/views/Main.tsx | 5 ++--- src/client/views/collections/CollectionTreeView.scss | 2 ++ src/client/views/collections/CollectionTreeView.tsx | 11 +++-------- src/new_fields/Doc.ts | 5 +++-- src/server/authentication/models/current_user_utils.ts | 10 ++++++++-- 5 files changed, 18 insertions(+), 15 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 97eb73d7f..90339aea2 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -144,10 +144,9 @@ export class Main extends React.Component { createNewWorkspace = async (id?: string) => { const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); if (list) { - let libraryDoc = await (CurrentUserUtils.UserDocument.library as Doc); let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(libraryDoc, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; - let mainDoc = Docs.DockDocument([libraryDoc, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${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(() => { diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index ce7da5767..19d4abc05 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -37,6 +37,8 @@ width: 1.5em; display: inline-block; color: $intermediate-color; + margin-top: 3px; + transform: scale(1.3,1.3); } .coll-title { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2cef1462b..b67d6f965 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -145,11 +145,8 @@ class TreeView extends React.Component {
    ; } public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { - return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child => { - console.log("child = " + child[Id]); - return - } - ); + return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child => + ); } } @@ -177,9 +174,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { if (!children) { return (null); } - let testForLibrary = children && children.length === 1 && children[0] === CurrentUserUtils.UserDocument; - var subchildren = testForLibrary ? Cast(children[0].data, listSpec(Doc), children) : children; - let childElements = TreeView.GetChildElements(subchildren, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(children, this.remove, this.props.moveDocument, dropAction); return (
    { - // let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); - let linkDoc = new Doc; + let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); + //let linkDoc = new Doc; linkDoc.title = "-link name-"; linkDoc.linkDescription = ""; linkDoc.linkTags = "Default"; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 93c2afb1d..5f45d7bcc 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -7,6 +7,9 @@ import { RouteStore } from "../../RouteStore"; 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; @@ -23,11 +26,14 @@ export class CurrentUserUtils { 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.excludeFromLibrary = true; doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" }); - doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); - (doc.library as Doc).excludeFromLibrary = true; + // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); + // (doc.library as Doc).excludeFromLibrary = true; return doc; } -- cgit v1.2.3-70-g09d2 From af2e5dbf49e0e82d76f267c681761968d4bafc62 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 4 May 2019 23:03:49 -0400 Subject: fixed tree view. added non-data keys. --- src/client/views/Main.scss | 1 + .../views/collections/CollectionTreeView.scss | 17 +++++++--- .../views/collections/CollectionTreeView.tsx | 37 ++++++++++++++-------- src/new_fields/Doc.ts | 2 +- 4 files changed, 38 insertions(+), 19 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index cbf920793..5c5c252e9 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -182,6 +182,7 @@ button:hover { top: 0; left: 0; overflow: scroll; + z-index: 1; } #mainContent-div { width: 100%; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 19d4abc05..6ce13cf56 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -33,9 +33,10 @@ } .bullet { - position: absolute; - width: 1.5em; - display: inline-block; + float:left; + position: relative; + width: 15px; + display: block; color: $intermediate-color; margin-top: 3px; transform: scale(1.3,1.3); @@ -50,7 +51,7 @@ .docContainer { margin-left: 10px; display: block; - width: max-content; + width:100%;//width: max-content; } .docContainer:hover { @@ -59,6 +60,9 @@ // width: auto; } } + .editableView-container { + font-weight: bold; + } .delete-button { color: $intermediate-color; @@ -67,4 +71,9 @@ // margin-top: 3px; display: inline; } + + .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 b67d6f965..17109508d 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -18,6 +18,8 @@ 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'; export interface TreeViewProps { @@ -122,17 +124,25 @@ class TreeView extends React.Component { render() { let bulletType = BulletType.List; - let contentElement: JSX.Element | null = (null); - var children = Cast(this.props.document.data, listSpec(Doc)); - if (children) { // add children for a collection - if (!this._collapsed) { - bulletType = BulletType.Collapsible; - contentElement =
      - {TreeView.GetChildElements(children, this.remove, this.move, this.props.dropAction)} -
    ; - } - 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))); } + 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(
      + {(key === "data") ? (null) : + {key}} + {TreeView.GetChildElements(docList, key !== "data", this.remove, this.move, this.props.dropAction)} +
    ); + } else + bulletType = BulletType.Collapsed; + } + }); return
    {
    ; } - public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { - return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child => + public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child => ); } } @@ -168,13 +178,12 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } render() { - trace(); const children = this.children; let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; if (!children) { return (null); } - let childElements = TreeView.GetChildElements(children, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction); return (
    { let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); //let linkDoc = new Doc; - linkDoc.title = "-link name-"; + linkDoc.proto!.title = "-link name-"; linkDoc.linkDescription = ""; linkDoc.linkTags = "Default"; -- cgit v1.2.3-70-g09d2 From 9830eb15041fa8e99ef8f5bb6fdcf6b3c28c058d Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Mon, 6 May 2019 01:10:14 -0400 Subject: added start of "minimap" support via better buttons and summaries. --- .../views/collections/CollectionDockingView.tsx | 36 +++++++++++++++++--- .../views/collections/CollectionTreeView.scss | 39 +++++++++++++--------- .../views/collections/CollectionTreeView.tsx | 13 +++++--- .../collections/collectionFreeForm/MarqueeView.tsx | 37 ++++++++++++-------- .../views/nodes/CollectionFreeFormDocumentView.tsx | 28 +++++++++++++--- src/client/views/nodes/DocumentView.tsx | 9 ++--- src/client/views/nodes/FormattedTextBox.tsx | 4 +-- 7 files changed, 118 insertions(+), 48 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 278065479..e4f02a4bc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -72,6 +72,37 @@ export class CollectionDockingView extends React.Component { + if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + child.contentItems[0].config.props.documentId == document[Id]) { + child.contentItems[0].remove(); + //this._goldenLayout.root.contentItems[0].contentItems.splice(i, 1); + this.layoutChanged(document); + } else + child.contentItems.map((tab: any, j: number) => { + if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) { + child.contentItems[j].remove(); + let docs = Cast(this.props.Document.data, listSpec(Doc)); + docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); + } + }); + }) + } + } + + @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 // @@ -107,10 +138,7 @@ export class CollectionDockingView extends React.Component { @observable _collapsed: boolean = true; delete = () => this.props.deleteDoc(this.props.document); + openRight = () => CollectionDockingView.Instance.AddRightSplit(this.props.document); get children() { return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); @@ -100,9 +102,11 @@ class TreeView extends React.Component { }} />); return ( -
    +
    {editableView(StrCast(this.props.document.title))} - {/*
    */} +
    + {/* {
    } */}
    ); } @@ -145,8 +149,7 @@ class TreeView extends React.Component { }); return
    + onContextMenu={this.onWorkspaceContextMenu}>
  • {this.renderBullet(bulletType)} {this.renderTitle()} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 8c81f6990..805921ad4 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -15,6 +15,8 @@ import { NumCast, Cast } from "../../../../new_fields/Types"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { Templates } from "../../Templates"; import { List } from "../../../../new_fields/List"; +import { emitKeypressEvents } from "readline"; +import { listSpec } from "../../../../new_fields/Schema"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -149,16 +151,17 @@ export class MarqueeView extends React.Component this.cleanupInteractions(false); e.stopPropagation(); } - if (e.key === "c" || e.key === "r" || e.key === "e") { + if (e.key === "c" || e.key === "r" || e.key === "R" || e.key === "e") { this._commandExecuted = true; e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().map(d => { - if (e.key !== "r") + 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; + 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 = Cast(this.props.container.props.Document.ink, InkField); @@ -179,16 +182,23 @@ export class MarqueeView extends React.Component this.marqueeInkDelete(inkData); // SelectionManager.DeselectAll(); - if (e.key === "r") { - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - summary.maximizedDocs = new List(selected); - // summary.doc1 = selected[0]; - // if (selected.length > 1) - // summary.doc2 = selected[1]; - // summary.templates = new List([Templates.Summary.Layout]); - this.props.addLiveTextDocument(summary); + if (e.key === "r" || e.key === "R") { 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 === "r") { + summary.proto!.maximizeOnRight = true; + let list = Cast(newCollection.data, listSpec(Doc)); + if (list && list.length === 1) { + selected = list; + } else { + selected = [newCollection]; + this.props.addDocument(newCollection, false); + } + } + summary.proto!.maximizedDocs = new List(selected); + summary.proto!.isButton = true; selected.map(maximizedDoc => { let maxx = NumCast(maximizedDoc.x, undefined); let maxy = NumCast(maximizedDoc.y, undefined); @@ -196,6 +206,7 @@ export class MarqueeView extends React.Component let maxh = NumCast(maximizedDoc.height, undefined); maximizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) }); + this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 2ba0458f5..cf08c1bc4 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -6,11 +6,12 @@ import "./DocumentView.scss"; import React = require("react"); import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { FieldValue, Cast, NumCast, BoolCast, PromiseValue } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { Doc } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -86,7 +87,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { e.stopPropagation(); let ctrlKey = e.ctrlKey; + let metaKey = e.metaKey; if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { if (await BoolCast(this.props.Document.isButton, false)) { let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc)); if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents - if (ctrlKey) - this.props.addDocument && maximizedDocs.filter(d => d instanceof Doc).map(maxDoc => this.props.addDocument!(maxDoc, false)); - this.toggleIcon(); + if ((metaKey && !this.props.Document.maximizeOnRight) || (!metaKey && this.props.Document.maximizeOnRight)) { + SelectionManager.DeselectAll(); + maximizedDocs.map(async mdoc => { + let maxDoc = await mdoc; + let dataDocs = await Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); + if (dataDocs) { + Promise.all(dataDocs.map(async doc => await doc)).then(docs => { + if (!docs || docs.indexOf(maxDoc) == -1) { + CollectionDockingView.Instance.AddRightSplit(maxDoc); + } else { + CollectionDockingView.Instance.CloseRightSplit(maxDoc); + } + }) + } + }); + } else { + this.props.addDocument && maximizedDocs.map(async maxDoc => this.props.addDocument!(await maxDoc, false)); + this.toggleIcon(); + } } } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 096a02d9b..9d356cc30 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -202,10 +202,11 @@ export class DocumentView extends DocComponent(Docu CollectionDockingView.Instance.AddRightSplit(kvp); } makeButton = (e: React.MouseEvent): void => { - this.props.Document.isButton = !BoolCast(this.props.Document.isButton, false); - if (this.props.Document.isButton && !this.props.Document.nativeWidth) { - this.props.Document.nativeWidth = this.props.Document[WidthSym](); - this.props.Document.nativeHeight = this.props.Document[HeightSym](); + 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 => { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index eeb60cb3d..65b8b805f 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -128,7 +128,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe ); } else { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), - () => this.props.isSelected() && !BoolCast(this.props.Document.isButton, false) && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); + () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); } @@ -310,7 +310,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // tfs: do we need this event handler onWheel={this.onPointerWheel} > -
    +
    ); } -- cgit v1.2.3-70-g09d2 From c631db5ff9cc8f376588c856a71c9ad09e309f1d Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 6 May 2019 12:50:32 -0400 Subject: added goldenlayout as a file, changed list / type stuff a bit. --- src/client/goldenLayout.js | 5359 ++++++++++++++++++++ .../views/collections/CollectionDockingView.tsx | 48 +- .../views/collections/CollectionTreeView.tsx | 12 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 39 +- src/new_fields/List.ts | 2 +- 5 files changed, 5413 insertions(+), 47 deletions(-) create mode 100644 src/client/goldenLayout.js (limited to 'src/client/views/collections/CollectionTreeView.tsx') 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, 'javascript') + .replace(/expression/gi, 'expression') + .replace(/onload/gi, 'onload') + .replace(/script/gi, 'script') + .replace(/onerror/gi, 'onerror'); + + if (keepTags === true) { + return output; + } else { + return output + .replace(/>/g, '>') + .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 = $('
    '); + 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 = $('
    '); + + 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 = $('
    ' + + '
    ' + + '
    ' + + '
    '); + + 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 = $([ + '
    ', + '
    ', + '
    ' + ].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 = '
    ' + + '
    ' + + '
      ' + + '
    • ' + + '' + + '
    • ' + + '
    ' + + '
    ' + + '
    ' + + '
    '; + + 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 = '
    '; + + 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 = [ + '
    ', + '
      ', + '
        ', + '
          ', + '
          ' + ].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 = $('
        • '); + 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 = $('
          '); + var element = $('
          '); + 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 = '
        • ' + + '
          ' + + '
        • '; + + 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 = $('
          '); + $(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 = $('
          '); + 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 = $('
          '); + 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 = $('
          '); + 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 = $('
          '); + 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} 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/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 21ca08471..6f721a0c8 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,25 +1,23 @@ -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, runInAction } from "mobx"; +import { action, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; -import { Utils, returnTrue, emptyFunction, returnOne, returnZero } from "../../../Utils"; +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 { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; -import { Transform } from '../../util/Transform'; -import { Doc, Opt, Field } from "../../../new_fields/Doc"; -import { Cast, NumCast, StrCast, PromiseValue } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { DocServer } from "../../DocServer"; -import { listSpec } from "../../../new_fields/Schema"; -import { Id, FieldId } from "../../../new_fields/RefField"; -import { faSignInAlt } from "@fortawesome/free-solid-svg-icons"; +import React = require("react"); @observer export class CollectionDockingView extends React.Component { @@ -286,6 +284,9 @@ export class CollectionDockingView extends React.Component { if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + 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(`
          0
          `); @@ -325,16 +326,6 @@ export class CollectionDockingView extends React.Component { let doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { @@ -342,7 +333,15 @@ export class CollectionDockingView extends React.Component ); } + } interface DockedFrameProps { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 947a066d9..429d0f047 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -20,6 +20,7 @@ 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 { @@ -48,8 +49,15 @@ class TreeView extends React.Component { @observable _collapsed: boolean = true; - delete = () => this.props.deleteDoc(this.props.document); - openRight = () => CollectionDockingView.Instance.AddRightSplit(this.props.document); + @undoBatch delete = () => this.props.deleteDoc(this.props.document); + + @undoBatch openRight = () => { + 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)); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index cf08c1bc4..2c8e6aef3 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -125,15 +125,15 @@ export class CollectionFreeFormDocumentView extends DocComponent => { SelectionManager.DeselectAll(); let isMinimized: boolean | undefined; - let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc)); + let maximizedDocs = Cast(this.props.Document.maximizedDocs, listSpec(Doc)); let minimizedDoc: Doc | undefined = this.props.Document; if (!maximizedDocs) { minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (minimizedDoc) maximizedDocs = await Cast(minimizedDoc.maximizedDocs, listSpec(Doc)); + if (minimizedDoc) maximizedDocs = Cast(minimizedDoc.maximizedDocs, listSpec(Doc)); } - if (minimizedDoc && maximizedDocs && maximizedDocs instanceof List) { + if (minimizedDoc && maximizedDocs) { let minimizedTarget = minimizedDoc; - maximizedDocs.map(maximizedDoc => { + (await Promise.all(maximizedDocs)).forEach(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { if (isMinimized === undefined) { @@ -166,26 +166,25 @@ export class CollectionFreeFormDocumentView extends DocComponent { - let maxDoc = await mdoc; - let dataDocs = await Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); - if (dataDocs) { - Promise.all(dataDocs.map(async doc => await doc)).then(docs => { - if (!docs || docs.indexOf(maxDoc) == -1) { - CollectionDockingView.Instance.AddRightSplit(maxDoc); - } else { - CollectionDockingView.Instance.CloseRightSplit(maxDoc); - } - }) - } - }); + let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc)); + if (dataDocs) { + const docs = await Promise.all(dataDocs); + const maxDocs = await Promise.all(maximizedDocs); + SelectionManager.DeselectAll(); + maxDocs.forEach(maxDoc => { + if (!docs || docs.indexOf(maxDoc) == -1) { + CollectionDockingView.Instance.AddRightSplit(maxDoc); + } else { + CollectionDockingView.Instance.CloseRightSplit(maxDoc); + } + }); + } } else { - this.props.addDocument && maximizedDocs.map(async maxDoc => this.props.addDocument!(await maxDoc, false)); + this.props.addDocument && maximizedDocs.forEach(async maxDoc => this.props.addDocument!(await maxDoc, false)); this.toggleIcon(); } } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index 96018dafa..213ed21aa 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -206,7 +206,7 @@ class ListImpl extends ObjectField { return list; } - [key: number]: FieldResult; + [key: number]: T | (T extends RefField ? Promise : never); @serializable(alias("fields", list(autoObject()))) private get __fields() { -- cgit v1.2.3-70-g09d2 From 684c8e190098dee8c285665ebf1e2c598bd5cf4c Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 6 May 2019 15:28:59 -0400 Subject: fixed a few golden layout and tree view issues. --- src/client/views/collections/CollectionDockingView.tsx | 4 +++- src/client/views/collections/CollectionTreeView.tsx | 18 ++++++++++++++---- src/client/views/nodes/IconBox.tsx | 14 +++++++++++++- src/new_fields/Doc.ts | 1 - 4 files changed, 30 insertions(+), 7 deletions(-) (limited to 'src/client/views/collections/CollectionTreeView.tsx') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6f721a0c8..05c467763 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -78,14 +78,16 @@ export class CollectionDockingView extends React.Component { 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(); } }); }) diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 429d0f047..6fa374464 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -10,7 +10,7 @@ import "./CollectionTreeView.scss"; import React = require("react"); import { Document, listSpec } from '../../../new_fields/Schema'; import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; import { ContextMenu } from '../ContextMenu'; import { undoBatch } from '../../util/UndoManager'; @@ -51,7 +51,7 @@ class TreeView extends React.Component { @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => { + @undoBatch openRight = async () => { if (this.props.document.dockingConfig) { Main.Instance.openWorkspace(this.props.document); } else { @@ -63,6 +63,10 @@ class TreeView extends React.Component { return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); } + onPointerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + } + @action remove = (document: Document) => { let children = Cast(this.props.document.data, listSpec(Doc), []); @@ -109,11 +113,18 @@ class TreeView extends React.Component { return true; }} />); + let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []); + let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( +
          + + +
          ); return (
          {editableView(StrCast(this.props.document.title))} -
          + {openRight} {/* {
          } */}
          ); } @@ -156,7 +167,6 @@ class TreeView extends React.Component { } }); return
        • {this.renderBullet(bulletType)} diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 3fab10df4..b521d5ce6 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, runInAction, reaction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { SelectionManager } from "../../util/SelectionManager"; import { FieldView, FieldViewProps } from './FieldView'; @@ -25,6 +25,17 @@ library.add(faFilm); @observer export class IconBox extends React.Component { public static LayoutString() { return FieldView.LayoutString(IconBox); } + _reactionDisposer?: IReactionDisposer; + componentDidMount() { + this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs], + () => { + let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []); + this.props.Document.title = maxDoc && (maxDoc.length === 1 ? maxDoc[0].title + ".icon" : ""); + }, { fireImmediately: true }); + } + componentWillUnmount() { + if (this._reactionDisposer) this._reactionDisposer(); + } @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "

          Error loading icon data

          "; } @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } @@ -52,6 +63,7 @@ export class IconBox extends React.Component { @observable _panelWidth: number = 0; @observable _panelHeight: number = 0; render() { + let title = this._title; let labelField = StrCast(this.props.Document.labelField); let hideLabel = BoolCast(this.props.Document.hideLabel); let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 70dd8361b..38c220bc8 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -220,7 +220,6 @@ export namespace Doc { return undefined; } const delegate = new Doc(); - //TODO Does this need to be doc[Self]? delegate.proto = doc; return delegate; } -- cgit v1.2.3-70-g09d2