From 9830eb15041fa8e99ef8f5bb6fdcf6b3c28c058d Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 6 May 2019 01:10:14 -0400
Subject: added start of "minimap" support via better buttons and summaries.

---
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 28 ++++++++++++++++++----
 1 file changed, 23 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

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<CollectionFreeF
                 let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
                 this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false);
             }
-        });
+        }, { fireImmediately: true });
     }
 
     componentWillUnmount() {
@@ -162,14 +163,31 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     onClick = async (e: React.MouseEvent) => {
         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();
+                    }
                 }
             }
         }
-- 
cgit v1.2.3-70-g09d2


From c631db5ff9cc8f376588c856a71c9ad09e309f1d Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
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/nodes/CollectionFreeFormDocumentView.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, 'j&#97;vascript')
+            .replace(/expression/gi, 'expr&#101;ssion')
+            .replace(/onload/gi, 'onlo&#97;d')
+            .replace(/script/gi, '&#115;cript')
+            .replace(/onerror/gi, 'on&#101;rror');
+
+        if (keepTags === true) {
+            return output;
+        } else {
+            return output
+                .replace(/>/g, '&gt;')
+                .replace(/</g, '&lt;');
+        }
+    };
+
+	/**
+	 * Removes html tags from a string
+	 *
+	 * @param   {String} input
+	 *
+	 * @returns {String} input without tags
+	 */
+    lm.utils.stripTags = function (input) {
+        return $.trim(input.replace(/(<([^>]+)>)/ig, ''));
+    };
+	/**
+	 * A generic and very fast EventEmitter
+	 * implementation. On top of emitting the
+	 * actual event it emits an
+	 *
+	 * lm.utils.EventEmitter.ALL_EVENT
+	 *
+	 * event for every event triggered. This allows
+	 * to hook into it and proxy events forwards
+	 *
+	 * @constructor
+	 */
+    lm.utils.EventEmitter = function () {
+        this._mSubscriptions = {};
+        this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT] = [];
+
+		/**
+		 * Listen for events
+		 *
+		 * @param   {String} sEvent    The name of the event to listen to
+		 * @param   {Function} fCallback The callback to execute when the event occurs
+		 * @param   {[Object]} oContext The value of the this pointer within the callback function
+		 *
+		 * @returns {void}
+		 */
+        this.on = function (sEvent, fCallback, oContext) {
+            if (!lm.utils.isFunction(fCallback)) {
+                throw new Error('Tried to listen to event ' + sEvent + ' with non-function callback ' + fCallback);
+            }
+
+            if (!this._mSubscriptions[sEvent]) {
+                this._mSubscriptions[sEvent] = [];
+            }
+
+            this._mSubscriptions[sEvent].push({ fn: fCallback, ctx: oContext });
+        };
+
+		/**
+		 * Emit an event and notify listeners
+		 *
+		 * @param   {String} sEvent The name of the event
+		 * @param    {Mixed}  various additional arguments that will be passed to the listener
+		 *
+		 * @returns {void}
+		 */
+        this.emit = function (sEvent) {
+            var i, ctx, args;
+
+            args = Array.prototype.slice.call(arguments, 1);
+
+            var subs = this._mSubscriptions[sEvent];
+
+            if (subs) {
+                subs = subs.slice();
+                for (i = 0; i < subs.length; i++) {
+                    ctx = subs[i].ctx || {};
+                    subs[i].fn.apply(ctx, args);
+                }
+            }
+
+            args.unshift(sEvent);
+
+            var allEventSubs = this._mSubscriptions[lm.utils.EventEmitter.ALL_EVENT].slice()
+
+            for (i = 0; i < allEventSubs.length; i++) {
+                ctx = allEventSubs[i].ctx || {};
+                allEventSubs[i].fn.apply(ctx, args);
+            }
+        };
+
+		/**
+		 * Removes a listener for an event, or all listeners if no callback and context is provided.
+		 *
+		 * @param   {String} sEvent    The name of the event
+		 * @param   {Function} fCallback The previously registered callback method (optional)
+		 * @param   {Object} oContext  The previously registered context (optional)
+		 *
+		 * @returns {void}
+		 */
+        this.unbind = function (sEvent, fCallback, oContext) {
+            if (!this._mSubscriptions[sEvent]) {
+                throw new Error('No subscribtions to unsubscribe for event ' + sEvent);
+            }
+
+            var i, bUnbound = false;
+
+            for (i = 0; i < this._mSubscriptions[sEvent].length; i++) {
+                if
+                    (
+                    (!fCallback || this._mSubscriptions[sEvent][i].fn === fCallback) &&
+                    (!oContext || oContext === this._mSubscriptions[sEvent][i].ctx)
+                ) {
+                    this._mSubscriptions[sEvent].splice(i, 1);
+                    bUnbound = true;
+                }
+            }
+
+            if (bUnbound === false) {
+                throw new Error('Nothing to unbind for ' + sEvent);
+            }
+        };
+
+		/**
+		 * Alias for unbind
+		 */
+        this.off = this.unbind;
+
+		/**
+		 * Alias for emit
+		 */
+        this.trigger = this.emit;
+    };
+
+	/**
+	 * The name of the event that's triggered for every other event
+	 *
+	 * usage
+	 *
+	 * myEmitter.on( lm.utils.EventEmitter.ALL_EVENT, function( eventName, argsArray ){
+	 * 	//do stuff
+	 * });
+	 *
+	 * @type {String}
+	 */
+    lm.utils.EventEmitter.ALL_EVENT = '__all';
+    lm.utils.DragListener = function (eElement, nButtonCode) {
+        lm.utils.EventEmitter.call(this);
+
+        this._eElement = $(eElement);
+        this._oDocument = $(document);
+        this._eBody = $(document.body);
+        this._nButtonCode = nButtonCode || 0;
+
+		/**
+		 * The delay after which to start the drag in milliseconds
+		 */
+        this._nDelay = 200;
+
+		/**
+		 * The distance the mouse needs to be moved to qualify as a drag
+		 */
+        this._nDistance = 10;//TODO - works better with delay only
+
+        this._nX = 0;
+        this._nY = 0;
+
+        this._nOriginalX = 0;
+        this._nOriginalY = 0;
+
+        this._bDragging = false;
+
+        this._fMove = lm.utils.fnBind(this.onMouseMove, this);
+        this._fUp = lm.utils.fnBind(this.onMouseUp, this);
+        this._fDown = lm.utils.fnBind(this.onMouseDown, this);
+
+
+        this._eElement.on('mousedown touchstart', this._fDown);
+    };
+
+    lm.utils.DragListener.timeout = null;
+
+    lm.utils.copy(lm.utils.DragListener.prototype, {
+        destroy: function () {
+            this._eElement.unbind('mousedown touchstart', this._fDown);
+            this._oDocument.unbind('mouseup touchend', this._fUp);
+            this._eElement = null;
+            this._oDocument = null;
+            this._eBody = null;
+        },
+
+        onMouseDown: function (oEvent) {
+            oEvent.preventDefault();
+
+            if (oEvent.button == 0 || oEvent.type === "touchstart") {
+                var coordinates = this._getCoordinates(oEvent);
+
+                this._nOriginalX = coordinates.x;
+                this._nOriginalY = coordinates.y;
+
+                this._oDocument.on('mousemove touchmove', this._fMove);
+                this._oDocument.one('mouseup touchend', this._fUp);
+
+                this._timeout = setTimeout(lm.utils.fnBind(this._startDrag, this), this._nDelay);
+            }
+        },
+
+        onMouseMove: function (oEvent) {
+            if (this._timeout != null) {
+                oEvent.preventDefault();
+
+                var coordinates = this._getCoordinates(oEvent);
+
+                this._nX = coordinates.x - this._nOriginalX;
+                this._nY = coordinates.y - this._nOriginalY;
+
+                if (this._bDragging === false) {
+                    if (
+                        Math.abs(this._nX) > this._nDistance ||
+                        Math.abs(this._nY) > this._nDistance
+                    ) {
+                        clearTimeout(this._timeout);
+                        this._startDrag();
+                    }
+                }
+
+                if (this._bDragging) {
+                    this.emit('drag', this._nX, this._nY, oEvent);
+                }
+            }
+        },
+
+        onMouseUp: function (oEvent) {
+            if (this._timeout != null) {
+                clearTimeout(this._timeout);
+                this._eBody.removeClass('lm_dragging');
+                this._eElement.removeClass('lm_dragging');
+                this._oDocument.find('iframe').css('pointer-events', '');
+                this._oDocument.unbind('mousemove touchmove', this._fMove);
+                this._oDocument.unbind('mouseup touchend', this._fUp);
+
+                if (this._bDragging === true) {
+                    this._bDragging = false;
+                    this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
+                }
+            }
+        },
+
+        _startDrag: function () {
+            this._bDragging = true;
+            this._eBody.addClass('lm_dragging');
+            this._eElement.addClass('lm_dragging');
+            this._oDocument.find('iframe').css('pointer-events', 'none');
+            this.emit('dragStart', this._nOriginalX, this._nOriginalY);
+        },
+
+        _getCoordinates: function (event) {
+            event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+            return {
+                x: event.pageX,
+                y: event.pageY
+            };
+        }
+    });
+	/**
+	 * The main class that will be exposed as GoldenLayout.
+	 *
+	 * @public
+	 * @constructor
+	 * @param {GoldenLayout config} config
+	 * @param {[DOM element container]} container Can be a jQuery selector string or a Dom element. Defaults to body
+	 *
+	 * @returns {VOID}
+	 */
+    lm.LayoutManager = function (config, container) {
+
+        if (!$ || typeof $.noConflict !== 'function') {
+            var errorMsg = 'jQuery is missing as dependency for GoldenLayout. ';
+            errorMsg += 'Please either expose $ on GoldenLayout\'s scope (e.g. window) or add "jquery" to ';
+            errorMsg += 'your paths when using RequireJS/AMD';
+            throw new Error(errorMsg);
+        }
+        lm.utils.EventEmitter.call(this);
+
+        this.isInitialised = false;
+        this._isFullPage = false;
+        this._resizeTimeoutId = null;
+        this._components = { 'lm-react-component': lm.utils.ReactComponentHandler };
+        this._itemAreas = [];
+        this._resizeFunction = lm.utils.fnBind(this._onResize, this);
+        this._unloadFunction = lm.utils.fnBind(this._onUnload, this);
+        this._maximisedItem = null;
+        this._maximisePlaceholder = $('<div class="lm_maximise_place"></div>');
+        this._creationTimeoutPassed = false;
+        this._subWindowsCreated = false;
+        this._dragSources = [];
+        this._updatingColumnsResponsive = false;
+        this._firstLoad = true;
+
+        this.width = null;
+        this.height = null;
+        this.root = null;
+        this.openPopouts = [];
+        this.selectedItem = null;
+        this.isSubWindow = false;
+        this.eventHub = new lm.utils.EventHub(this);
+        this.config = this._createConfig(config);
+        this.container = container;
+        this.dropTargetIndicator = null;
+        this.transitionIndicator = null;
+        this.tabDropPlaceholder = $('<div class="lm_drop_tab_placeholder"></div>');
+
+        if (this.isSubWindow === true) {
+            $('body').css('visibility', 'hidden');
+        }
+
+        this._typeToItem = {
+            'column': lm.utils.fnBind(lm.items.RowOrColumn, this, [true]),
+            'row': lm.utils.fnBind(lm.items.RowOrColumn, this, [false]),
+            'stack': lm.items.Stack,
+            'component': lm.items.Component
+        };
+    };
+
+	/**
+	 * Hook that allows to access private classes
+	 */
+    lm.LayoutManager.__lm = lm;
+
+	/**
+	 * Takes a GoldenLayout configuration object and
+	 * replaces its keys and values recursively with
+	 * one letter codes
+	 *
+	 * @static
+	 * @public
+	 * @param   {Object} config A GoldenLayout config object
+	 *
+	 * @returns {Object} minified config
+	 */
+    lm.LayoutManager.minifyConfig = function (config) {
+        return (new lm.utils.ConfigMinifier()).minifyConfig(config);
+    };
+
+	/**
+	 * Takes a configuration Object that was previously minified
+	 * using minifyConfig and returns its original version
+	 *
+	 * @static
+	 * @public
+	 * @param   {Object} minifiedConfig
+	 *
+	 * @returns {Object} the original configuration
+	 */
+    lm.LayoutManager.unminifyConfig = function (config) {
+        return (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+    };
+
+    lm.utils.copy(lm.LayoutManager.prototype, {
+
+		/**
+		 * Register a component with the layout manager. If a configuration node
+		 * of type component is reached it will look up componentName and create the
+		 * associated component
+		 *
+		 *  {
+		 *		type: "component",
+		 *		componentName: "EquityNewsFeed",
+		 *		componentState: { "feedTopic": "us-bluechips" }
+		 *  }
+		 *
+		 * @public
+		 * @param   {String} name
+		 * @param   {Function} constructor
+		 *
+		 * @returns {void}
+		 */
+        registerComponent: function (name, constructor) {
+            if (typeof constructor !== 'function') {
+                throw new Error('Please register a constructor function');
+            }
+
+            if (this._components[name] !== undefined) {
+                throw new Error('Component ' + name + ' is already registered');
+            }
+
+            this._components[name] = constructor;
+        },
+
+		/**
+		 * Creates a layout configuration object based on the the current state
+		 *
+		 * @public
+		 * @returns {Object} GoldenLayout configuration
+		 */
+        toConfig: function (root) {
+            var config, next, i;
+
+            if (this.isInitialised === false) {
+                throw new Error('Can\'t create config, layout not yet initialised');
+            }
+
+            if (root && !(root instanceof lm.items.AbstractContentItem)) {
+                throw new Error('Root must be a ContentItem');
+            }
+
+			/*
+			 * settings & labels
+			 */
+            config = {
+                settings: lm.utils.copy({}, this.config.settings),
+                dimensions: lm.utils.copy({}, this.config.dimensions),
+                labels: lm.utils.copy({}, this.config.labels)
+            };
+
+			/*
+			 * Content
+			 */
+            config.content = [];
+            next = function (configNode, item) {
+                var key, i;
+
+                for (key in item.config) {
+                    if (key !== 'content') {
+                        configNode[key] = item.config[key];
+                    }
+                }
+
+                if (item.contentItems.length) {
+                    configNode.content = [];
+
+                    for (i = 0; i < item.contentItems.length; i++) {
+                        configNode.content[i] = {};
+                        next(configNode.content[i], item.contentItems[i]);
+                    }
+                }
+            };
+
+            if (root) {
+                next(config, { contentItems: [root] });
+            } else {
+                next(config, this.root);
+            }
+
+			/*
+			 * Retrieve config for subwindows
+			 */
+            this._$reconcilePopoutWindows();
+            config.openPopouts = [];
+            for (i = 0; i < this.openPopouts.length; i++) {
+                config.openPopouts.push(this.openPopouts[i].toConfig());
+            }
+
+			/*
+			 * Add maximised item
+			 */
+            config.maximisedItemId = this._maximisedItem ? '__glMaximised' : null;
+            return config;
+        },
+
+		/**
+		 * Returns a previously registered component
+		 *
+		 * @public
+		 * @param   {String} name The name used
+		 *
+		 * @returns {Function}
+		 */
+        getComponent: function (name) {
+            if (this._components[name] === undefined) {
+                throw new lm.errors.ConfigurationError('Unknown component "' + name + '"');
+            }
+
+            return this._components[name];
+        },
+
+		/**
+		 * Creates the actual layout. Must be called after all initial components
+		 * are registered. Recurses through the configuration and sets up
+		 * the item tree.
+		 *
+		 * If called before the document is ready it adds itself as a listener
+		 * to the document.ready event
+		 *
+		 * @public
+		 *
+		 * @returns {void}
+		 */
+        init: function () {
+
+			/**
+			 * Create the popout windows straight away. If popouts are blocked
+			 * an error is thrown on the same 'thread' rather than a timeout and can
+			 * be caught. This also prevents any further initilisation from taking place.
+			 */
+            if (this._subWindowsCreated === false) {
+                this._createSubWindows();
+                this._subWindowsCreated = true;
+            }
+
+
+			/**
+			 * If the document isn't ready yet, wait for it.
+			 */
+            if (document.readyState === 'loading' || document.body === null) {
+                $(document).ready(lm.utils.fnBind(this.init, this));
+                return;
+            }
+
+			/**
+			 * If this is a subwindow, wait a few milliseconds for the original
+			 * page's js calls to be executed, then replace the bodies content
+			 * with GoldenLayout
+			 */
+            if (this.isSubWindow === true && this._creationTimeoutPassed === false) {
+                setTimeout(lm.utils.fnBind(this.init, this), 7);
+                this._creationTimeoutPassed = true;
+                return;
+            }
+
+            if (this.isSubWindow === true) {
+                this._adjustToWindowMode();
+            }
+
+            this._setContainer();
+            this.dropTargetIndicator = new lm.controls.DropTargetIndicator(this.container);
+            this.transitionIndicator = new lm.controls.TransitionIndicator();
+            this.updateSize();
+            this._create(this.config);
+            this._bindEvents();
+            this.isInitialised = true;
+            this._adjustColumnsResponsive();
+            this.emit('initialised');
+        },
+
+		/**
+		 * Updates the layout managers size
+		 *
+		 * @public
+		 * @param   {[int]} width  height in pixels
+		 * @param   {[int]} height width in pixels
+		 *
+		 * @returns {void}
+		 */
+        updateSize: function (width, height) {
+            if (arguments.length === 2) {
+                this.width = width;
+                this.height = height;
+            } else {
+                this.width = this.container.width();
+                this.height = this.container.height();
+            }
+
+            if (this.isInitialised === true) {
+                this.root.callDownwards('setSize', [this.width, this.height]);
+
+                if (this._maximisedItem) {
+                    this._maximisedItem.element.width(this.container.width());
+                    this._maximisedItem.element.height(this.container.height());
+                    this._maximisedItem.callDownwards('setSize');
+                }
+
+                this._adjustColumnsResponsive();
+            }
+        },
+
+		/**
+		 * Destroys the LayoutManager instance itself as well as every ContentItem
+		 * within it. After this is called nothing should be left of the LayoutManager.
+		 *
+		 * @public
+		 * @returns {void}
+		 */
+        destroy: function () {
+            if (this.isInitialised === false) {
+                return;
+            }
+            this._onUnload();
+            $(window).off('resize', this._resizeFunction);
+            $(window).off('unload beforeunload', this._unloadFunction);
+            this.root.callDownwards('_$destroy', [], true);
+            this.root.contentItems = [];
+            this.tabDropPlaceholder.remove();
+            this.dropTargetIndicator.destroy();
+            this.transitionIndicator.destroy();
+            this.eventHub.destroy();
+
+            this._dragSources.forEach(function (dragSource) {
+                dragSource._dragListener.destroy();
+                dragSource._element = null;
+                dragSource._itemConfig = null;
+                dragSource._dragListener = null;
+            });
+            this._dragSources = [];
+        },
+
+		/**
+		 * Recursively creates new item tree structures based on a provided
+		 * ItemConfiguration object
+		 *
+		 * @public
+		 * @param   {Object} config ItemConfig
+		 * @param   {[ContentItem]} parent The item the newly created item should be a child of
+		 *
+		 * @returns {lm.items.ContentItem}
+		 */
+        createContentItem: function (config, parent) {
+            var typeErrorMsg, contentItem;
+
+            if (typeof config.type !== 'string') {
+                throw new lm.errors.ConfigurationError('Missing parameter \'type\'', config);
+            }
+
+            if (config.type === 'react-component') {
+                config.type = 'component';
+                config.componentName = 'lm-react-component';
+            }
+
+            if (!this._typeToItem[config.type]) {
+                typeErrorMsg = 'Unknown type \'' + config.type + '\'. ' +
+                    'Valid types are ' + lm.utils.objectKeys(this._typeToItem).join(',');
+
+                throw new lm.errors.ConfigurationError(typeErrorMsg);
+            }
+
+
+			/**
+			 * We add an additional stack around every component that's not within a stack anyways.
+			 */
+            if (
+                // If this is a component
+                config.type === 'component' &&
+
+                // and it's not already within a stack
+                !(parent instanceof lm.items.Stack) &&
+
+                // and we have a parent
+                !!parent &&
+
+                // and it's not the topmost item in a new window
+                !(this.isSubWindow === true && parent instanceof lm.items.Root)
+            ) {
+                config = {
+                    type: 'stack',
+                    width: config.width,
+                    height: config.height,
+                    content: [config]
+                };
+            }
+
+            contentItem = new this._typeToItem[config.type](this, config, parent);
+            return contentItem;
+        },
+
+		/**
+		 * Creates a popout window with the specified content and dimensions
+		 *
+		 * @param   {Object|lm.itemsAbstractContentItem} configOrContentItem
+		 * @param   {[Object]} dimensions A map with width, height, left and top
+		 * @param    {[String]} parentId the id of the element this item will be appended to
+		 *                             when popIn is called
+		 * @param    {[Number]} indexInParent The position of this item within its parent element
+	
+		 * @returns {lm.controls.BrowserPopout}
+		 */
+        createPopout: function (configOrContentItem, dimensions, parentId, indexInParent) {
+            var config = configOrContentItem,
+                isItem = configOrContentItem instanceof lm.items.AbstractContentItem,
+                self = this,
+                windowLeft,
+                windowTop,
+                offset,
+                parent,
+                child,
+                browserPopout;
+
+            parentId = parentId || null;
+
+            if (isItem) {
+                config = this.toConfig(configOrContentItem).content;
+                parentId = lm.utils.getUniqueId();
+
+				/**
+				 * If the item is the only component within a stack or for some
+				 * other reason the only child of its parent the parent will be destroyed
+				 * when the child is removed.
+				 *
+				 * In order to support this we move up the tree until we find something
+				 * that will remain after the item is being popped out
+				 */
+                parent = configOrContentItem.parent;
+                child = configOrContentItem;
+                while (parent.contentItems.length === 1 && !parent.isRoot) {
+                    parent = parent.parent;
+                    child = child.parent;
+                }
+
+                parent.addId(parentId);
+                if (isNaN(indexInParent)) {
+                    indexInParent = lm.utils.indexOf(child, parent.contentItems);
+                }
+            } else {
+                if (!(config instanceof Array)) {
+                    config = [config];
+                }
+            }
+
+
+            if (!dimensions && isItem) {
+                windowLeft = window.screenX || window.screenLeft;
+                windowTop = window.screenY || window.screenTop;
+                offset = configOrContentItem.element.offset();
+
+                dimensions = {
+                    left: windowLeft + offset.left,
+                    top: windowTop + offset.top,
+                    width: configOrContentItem.element.width(),
+                    height: configOrContentItem.element.height()
+                };
+            }
+
+            if (!dimensions && !isItem) {
+                dimensions = {
+                    left: window.screenX || window.screenLeft + 20,
+                    top: window.screenY || window.screenTop + 20,
+                    width: 500,
+                    height: 309
+                };
+            }
+
+            if (isItem) {
+                configOrContentItem.remove();
+            }
+
+            browserPopout = new lm.controls.BrowserPopout(config, dimensions, parentId, indexInParent, this);
+
+            browserPopout.on('initialised', function () {
+                self.emit('windowOpened', browserPopout);
+            });
+
+            browserPopout.on('closed', function () {
+                self._$reconcilePopoutWindows();
+            });
+
+            this.openPopouts.push(browserPopout);
+
+            return browserPopout;
+        },
+
+		/**
+		 * Attaches DragListener to any given DOM element
+		 * and turns it into a way of creating new ContentItems
+		 * by 'dragging' the DOM element into the layout
+		 *
+		 * @param   {jQuery DOM element} element
+		 * @param   {Object|Function} itemConfig for the new item to be created, or a function which will provide it
+		 *
+		 * @returns {void}
+		 */
+        createDragSource: function (element, itemConfig) {
+            this.config.settings.constrainDragToContainer = false;
+            var dragSource = new lm.controls.DragSource($(element), itemConfig, this);
+            this._dragSources.push(dragSource);
+
+            return dragSource;
+        },
+
+		/**
+		 * Programmatically selects an item. This deselects
+		 * the currently selected item, selects the specified item
+		 * and emits a selectionChanged event
+		 *
+		 * @param   {lm.item.AbstractContentItem} item#
+		 * @param   {[Boolean]} _$silent Wheather to notify the item of its selection
+		 * @event    selectionChanged
+		 *
+		 * @returns {VOID}
+		 */
+        selectItem: function (item, _$silent) {
+
+            if (this.config.settings.selectionEnabled !== true) {
+                throw new Error('Please set selectionEnabled to true to use this feature');
+            }
+
+            if (item === this.selectedItem) {
+                return;
+            }
+
+            if (this.selectedItem !== null) {
+                this.selectedItem.deselect();
+            }
+
+            if (item && _$silent !== true) {
+                item.select();
+            }
+
+            this.selectedItem = item;
+
+            this.emit('selectionChanged', item);
+        },
+
+		/*************************
+		 * PACKAGE PRIVATE
+		 *************************/
+        _$maximiseItem: function (contentItem) {
+            if (this._maximisedItem !== null) {
+                this._$minimiseItem(this._maximisedItem);
+            }
+            this._maximisedItem = contentItem;
+            this._maximisedItem.addId('__glMaximised');
+            contentItem.element.addClass('lm_maximised');
+            contentItem.element.after(this._maximisePlaceholder);
+            this.root.element.prepend(contentItem.element);
+            contentItem.element.width(this.container.width());
+            contentItem.element.height(this.container.height());
+            contentItem.callDownwards('setSize');
+            this._maximisedItem.emit('maximised');
+            this.emit('stateChanged');
+        },
+
+        _$minimiseItem: function (contentItem) {
+            contentItem.element.removeClass('lm_maximised');
+            contentItem.removeId('__glMaximised');
+            this._maximisePlaceholder.after(contentItem.element);
+            this._maximisePlaceholder.remove();
+            contentItem.parent.callDownwards('setSize');
+            this._maximisedItem = null;
+            contentItem.emit('minimised');
+            this.emit('stateChanged');
+        },
+
+		/**
+		 * This method is used to get around sandboxed iframe restrictions.
+		 * If 'allow-top-navigation' is not specified in the iframe's 'sandbox' attribute
+		 * (as is the case with codepens) the parent window is forbidden from calling certain
+		 * methods on the child, such as window.close() or setting document.location.href.
+		 *
+		 * This prevented GoldenLayout popouts from popping in in codepens. The fix is to call
+		 * _$closeWindow on the child window's gl instance which (after a timeout to disconnect
+		 * the invoking method from the close call) closes itself.
+		 *
+		 * @packagePrivate
+		 *
+		 * @returns {void}
+		 */
+        _$closeWindow: function () {
+            window.setTimeout(function () {
+                window.close();
+            }, 1);
+        },
+
+        _$getArea: function (x, y) {
+            var i, area, smallestSurface = Infinity, mathingArea = null;
+
+            for (i = 0; i < this._itemAreas.length; i++) {
+                area = this._itemAreas[i];
+
+                if (
+                    x > area.x1 &&
+                    x < area.x2 &&
+                    y > area.y1 &&
+                    y < area.y2 &&
+                    smallestSurface > area.surface
+                ) {
+                    smallestSurface = area.surface;
+                    mathingArea = area;
+                }
+            }
+
+            return mathingArea;
+        },
+
+        _$createRootItemAreas: function () {
+            var areaSize = 50;
+            var sides = { y2: 0, x2: 0, y1: 'y2', x1: 'x2' };
+            for (var side in sides) {
+                var area = this.root._$getArea();
+                area.side = side;
+                if (sides[side])
+                    area[side] = area[sides[side]] - areaSize;
+                else
+                    area[side] = areaSize;
+                area.surface = (area.x2 - area.x1) * (area.y2 - area.y1);
+                this._itemAreas.push(area);
+            }
+        },
+
+        _$calculateItemAreas: function () {
+            var i, area, allContentItems = this._getAllContentItems();
+            this._itemAreas = [];
+
+			/**
+			 * If the last item is dragged out, highlight the entire container size to
+			 * allow to re-drop it. allContentItems[ 0 ] === this.root at this point
+			 *
+			 * Don't include root into the possible drop areas though otherwise since it
+			 * will used for every gap in the layout, e.g. splitters
+			 */
+            if (allContentItems.length === 1) {
+                this._itemAreas.push(this.root._$getArea());
+                return;
+            }
+            this._$createRootItemAreas();
+
+            for (i = 0; i < allContentItems.length; i++) {
+
+                if (!(allContentItems[i].isStack)) {
+                    continue;
+                }
+
+                area = allContentItems[i]._$getArea();
+
+                if (area === null) {
+                    continue;
+                } else if (area instanceof Array) {
+                    this._itemAreas = this._itemAreas.concat(area);
+                } else {
+                    this._itemAreas.push(area);
+                    var header = {};
+                    lm.utils.copy(header, area);
+                    lm.utils.copy(header, area.contentItem._contentAreaDimensions.header.highlightArea);
+                    header.surface = (header.x2 - header.x1) * (header.y2 - header.y1);
+                    this._itemAreas.push(header);
+                }
+            }
+        },
+
+		/**
+		 * Takes a contentItem or a configuration and optionally a parent
+		 * item and returns an initialised instance of the contentItem.
+		 * If the contentItem is a function, it is first called
+		 *
+		 * @packagePrivate
+		 *
+		 * @param   {lm.items.AbtractContentItem|Object|Function} contentItemOrConfig
+		 * @param   {lm.items.AbtractContentItem} parent Only necessary when passing in config
+		 *
+		 * @returns {lm.items.AbtractContentItem}
+		 */
+        _$normalizeContentItem: function (contentItemOrConfig, parent) {
+            if (!contentItemOrConfig) {
+                throw new Error('No content item defined');
+            }
+
+            if (lm.utils.isFunction(contentItemOrConfig)) {
+                contentItemOrConfig = contentItemOrConfig();
+            }
+
+            if (contentItemOrConfig instanceof lm.items.AbstractContentItem) {
+                return contentItemOrConfig;
+            }
+
+            if ($.isPlainObject(contentItemOrConfig) && contentItemOrConfig.type) {
+                var newContentItem = this.createContentItem(contentItemOrConfig, parent);
+                newContentItem.callDownwards('_$init');
+                return newContentItem;
+            } else {
+                throw new Error('Invalid contentItem');
+            }
+        },
+
+		/**
+		 * Iterates through the array of open popout windows and removes the ones
+		 * that are effectively closed. This is necessary due to the lack of reliably
+		 * listening for window.close / unload events in a cross browser compatible fashion.
+		 *
+		 * @packagePrivate
+		 *
+		 * @returns {void}
+		 */
+        _$reconcilePopoutWindows: function () {
+            var openPopouts = [], i;
+
+            for (i = 0; i < this.openPopouts.length; i++) {
+                if (this.openPopouts[i].getWindow().closed === false) {
+                    openPopouts.push(this.openPopouts[i]);
+                } else {
+                    this.emit('windowClosed', this.openPopouts[i]);
+                }
+            }
+
+            if (this.openPopouts.length !== openPopouts.length) {
+                this.emit('stateChanged');
+                this.openPopouts = openPopouts;
+            }
+
+        },
+
+		/***************************
+		 * PRIVATE
+		 ***************************/
+		/**
+		 * Returns a flattened array of all content items,
+		 * regardles of level or type
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _getAllContentItems: function () {
+            var allContentItems = [];
+
+            var addChildren = function (contentItem) {
+                allContentItems.push(contentItem);
+
+                if (contentItem.contentItems instanceof Array) {
+                    for (var i = 0; i < contentItem.contentItems.length; i++) {
+                        addChildren(contentItem.contentItems[i]);
+                    }
+                }
+            };
+
+            addChildren(this.root);
+
+            return allContentItems;
+        },
+
+		/**
+		 * Binds to DOM/BOM events on init
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _bindEvents: function () {
+            if (this._isFullPage) {
+                $(window).resize(this._resizeFunction);
+            }
+            $(window).on('unload beforeunload', this._unloadFunction);
+        },
+
+		/**
+		 * Debounces resize events
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _onResize: function () {
+            clearTimeout(this._resizeTimeoutId);
+            this._resizeTimeoutId = setTimeout(lm.utils.fnBind(this.updateSize, this), 100);
+        },
+
+		/**
+		 * Extends the default config with the user specific settings and applies
+		 * derivations. Please note that there's a seperate method (AbstractContentItem._extendItemNode)
+		 * that deals with the extension of item configs
+		 *
+		 * @param   {Object} config
+		 * @static
+		 * @returns {Object} config
+		 */
+        _createConfig: function (config) {
+            var windowConfigKey = lm.utils.getQueryStringParam('gl-window');
+
+            if (windowConfigKey) {
+                this.isSubWindow = true;
+                config = localStorage.getItem(windowConfigKey);
+                config = JSON.parse(config);
+                config = (new lm.utils.ConfigMinifier()).unminifyConfig(config);
+                localStorage.removeItem(windowConfigKey);
+            }
+
+            config = $.extend(true, {}, lm.config.defaultConfig, config);
+
+            var nextNode = function (node) {
+                for (var key in node) {
+                    if (key !== 'props' && typeof node[key] === 'object') {
+                        nextNode(node[key]);
+                    }
+                    else if (key === 'type' && node[key] === 'react-component') {
+                        node.type = 'component';
+                        node.componentName = 'lm-react-component';
+                    }
+                }
+            }
+
+            nextNode(config);
+
+            if (config.settings.hasHeaders === false) {
+                config.dimensions.headerHeight = 0;
+            }
+
+            return config;
+        },
+
+		/**
+		 * This is executed when GoldenLayout detects that it is run
+		 * within a previously opened popout window.
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _adjustToWindowMode: function () {
+            var popInButton = $('<div class="lm_popin" title="' + this.config.labels.popin + '">' +
+                '<div class="lm_icon"></div>' +
+                '<div class="lm_bg"></div>' +
+                '</div>');
+
+            popInButton.click(lm.utils.fnBind(function () {
+                this.emit('popIn');
+            }, this));
+
+            document.title = lm.utils.stripTags(this.config.content[0].title);
+
+            $('head').append($('body link, body style, template, .gl_keep'));
+
+            this.container = $('body')
+                .html('')
+                .css('visibility', 'visible')
+                .append(popInButton);
+
+			/*
+			 * This seems a bit pointless, but actually causes a reflow/re-evaluation getting around
+			 * slickgrid's "Cannot find stylesheet." bug in chrome
+			 */
+            var x = document.body.offsetHeight; // jshint ignore:line
+
+			/*
+			 * Expose this instance on the window object
+			 * to allow the opening window to interact with
+			 * it
+			 */
+            window.__glInstance = this;
+        },
+
+		/**
+		 * Creates Subwindows (if there are any). Throws an error
+		 * if popouts are blocked.
+		 *
+		 * @returns {void}
+		 */
+        _createSubWindows: function () {
+            var i, popout;
+
+            for (i = 0; i < this.config.openPopouts.length; i++) {
+                popout = this.config.openPopouts[i];
+
+                this.createPopout(
+                    popout.content,
+                    popout.dimensions,
+                    popout.parentId,
+                    popout.indexInParent
+                );
+            }
+        },
+
+		/**
+		 * Determines what element the layout will be created in
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _setContainer: function () {
+            var container = $(this.container || 'body');
+
+            if (container.length === 0) {
+                throw new Error('GoldenLayout container not found');
+            }
+
+            if (container.length > 1) {
+                throw new Error('GoldenLayout more than one container element specified');
+            }
+
+            if (container[0] === document.body) {
+                this._isFullPage = true;
+
+                $('html, body').css({
+                    height: '100%',
+                    margin: 0,
+                    padding: 0,
+                    overflow: 'hidden'
+                });
+            }
+
+            this.container = container;
+        },
+
+		/**
+		 * Kicks of the initial, recursive creation chain
+		 *
+		 * @param   {Object} config GoldenLayout Config
+		 *
+		 * @returns {void}
+		 */
+        _create: function (config) {
+            var errorMsg;
+
+            if (!(config.content instanceof Array)) {
+                if (config.content === undefined) {
+                    errorMsg = 'Missing setting \'content\' on top level of configuration';
+                } else {
+                    errorMsg = 'Configuration parameter \'content\' must be an array';
+                }
+
+                throw new lm.errors.ConfigurationError(errorMsg, config);
+            }
+
+            if (config.content.length > 1) {
+                errorMsg = 'Top level content can\'t contain more then one element.';
+                throw new lm.errors.ConfigurationError(errorMsg, config);
+            }
+
+            this.root = new lm.items.Root(this, { content: config.content }, this.container);
+            this.root.callDownwards('_$init');
+
+            if (config.maximisedItemId === '__glMaximised') {
+                this.root.getItemsById(config.maximisedItemId)[0].toggleMaximise();
+            }
+        },
+
+		/**
+		 * Called when the window is closed or the user navigates away
+		 * from the page
+		 *
+		 * @returns {void}
+		 */
+        _onUnload: function () {
+            if (this.config.settings.closePopoutsOnUnload === true) {
+                for (var i = 0; i < this.openPopouts.length; i++) {
+                    this.openPopouts[i].close();
+                }
+            }
+        },
+
+		/**
+		 * Adjusts the number of columns to be lower to fit the screen and still maintain minItemWidth.
+		 *
+		 * @returns {void}
+		 */
+        _adjustColumnsResponsive: function () {
+
+            // If there is no min width set, or not content items, do nothing.
+            if (!this._useResponsiveLayout() || this._updatingColumnsResponsive || !this.config.dimensions || !this.config.dimensions.minItemWidth || this.root.contentItems.length === 0 || !this.root.contentItems[0].isRow) {
+                this._firstLoad = false;
+                return;
+            }
+
+            this._firstLoad = false;
+
+            // If there is only one column, do nothing.
+            var columnCount = this.root.contentItems[0].contentItems.length;
+            if (columnCount <= 1) {
+                return;
+            }
+
+            // If they all still fit, do nothing.
+            var minItemWidth = this.config.dimensions.minItemWidth;
+            var totalMinWidth = columnCount * minItemWidth;
+            if (totalMinWidth <= this.width) {
+                return;
+            }
+
+            // Prevent updates while it is already happening.
+            this._updatingColumnsResponsive = true;
+
+            // Figure out how many columns to stack, and put them all in the first stack container.
+            var finalColumnCount = Math.max(Math.floor(this.width / minItemWidth), 1);
+            var stackColumnCount = columnCount - finalColumnCount;
+
+            var rootContentItem = this.root.contentItems[0];
+            var firstStackContainer = this._findAllStackContainers()[0];
+            for (var i = 0; i < stackColumnCount; i++) {
+                // Stack from right.
+                var column = rootContentItem.contentItems[rootContentItem.contentItems.length - 1];
+                this._addChildContentItemsToContainer(firstStackContainer, column);
+            }
+
+            this._updatingColumnsResponsive = false;
+        },
+
+		/**
+		 * Determines if responsive layout should be used.
+		 *
+		 * @returns {bool} - True if responsive layout should be used; otherwise false.
+		 */
+        _useResponsiveLayout: function () {
+            return this.config.settings && (this.config.settings.responsiveMode == 'always' || (this.config.settings.responsiveMode == 'onload' && this._firstLoad));
+        },
+
+		/**
+		 * Adds all children of a node to another container recursively.
+		 * @param {object} container - Container to add child content items to.
+		 * @param {object} node - Node to search for content items.
+		 * @returns {void}
+		 */
+        _addChildContentItemsToContainer: function (container, node) {
+            if (node.type === 'stack') {
+                node.contentItems.forEach(function (item) {
+                    container.addChild(item);
+                    node.removeChild(item, true);
+                });
+            }
+            else {
+                node.contentItems.forEach(lm.utils.fnBind(function (item) {
+                    this._addChildContentItemsToContainer(container, item);
+                }, this));
+            }
+        },
+
+		/**
+		 * Finds all the stack containers.
+		 * @returns {array} - The found stack containers.
+		 */
+        _findAllStackContainers: function () {
+            var stackContainers = [];
+            this._findAllStackContainersRecursive(stackContainers, this.root);
+
+            return stackContainers;
+        },
+
+		/**
+		 * Finds all the stack containers.
+		 *
+		 * @param {array} - Set of containers to populate.
+		 * @param {object} - Current node to process.
+		 *
+		 * @returns {void}
+		 */
+        _findAllStackContainersRecursive: function (stackContainers, node) {
+            node.contentItems.forEach(lm.utils.fnBind(function (item) {
+                if (item.type == 'stack') {
+                    stackContainers.push(item);
+                }
+                else if (!item.isComponent) {
+                    this._findAllStackContainersRecursive(stackContainers, item);
+                }
+            }, this));
+        }
+    });
+
+	/**
+	 * Expose the Layoutmanager as the single entrypoint using UMD
+	 */
+    (function () {
+        /* global define */
+        if (typeof define === 'function' && define.amd) {
+            define(['jquery'], function (jquery) {
+                $ = jquery;
+                return lm.LayoutManager;
+            }); // jshint ignore:line
+        } else if (typeof exports === 'object') {
+            module.exports = lm.LayoutManager;
+        } else {
+            window.GoldenLayout = lm.LayoutManager;
+        }
+    })();
+
+    lm.config.itemDefaultConfig = {
+        isClosable: true,
+        reorderEnabled: true,
+        title: ''
+    };
+    lm.config.defaultConfig = {
+        openPopouts: [],
+        settings: {
+            hasHeaders: true,
+            constrainDragToContainer: true,
+            reorderEnabled: true,
+            selectionEnabled: false,
+            popoutWholeStack: false,
+            blockedPopoutsThrowError: true,
+            closePopoutsOnUnload: true,
+            showPopoutIcon: true,
+            showMaximiseIcon: true,
+            showCloseIcon: true,
+            responsiveMode: 'onload', // Can be onload, always, or none.
+            tabOverlapAllowance: 0, // maximum pixel overlap per tab
+            reorderOnTabMenuClick: true,
+            tabControlOffset: 10
+        },
+        dimensions: {
+            borderWidth: 5,
+            borderGrabWidth: 15,
+            minItemHeight: 10,
+            minItemWidth: 10,
+            headerHeight: 20,
+            dragProxyWidth: 300,
+            dragProxyHeight: 200
+        },
+        labels: {
+            close: 'close',
+            maximise: 'maximise',
+            minimise: 'minimise',
+            popout: 'open in new window',
+            popin: 'pop in',
+            tabDropdown: 'additional tabs'
+        }
+    };
+
+    lm.container.ItemContainer = function (config, parent, layoutManager) {
+        lm.utils.EventEmitter.call(this);
+
+        this.width = null;
+        this.height = null;
+        this.title = config.componentName;
+        this.parent = parent;
+        this.layoutManager = layoutManager;
+        this.isHidden = false;
+
+        this._config = config;
+        this._element = $([
+            '<div class="lm_item_container">',
+            '<div class="lm_content"></div>',
+            '</div>'
+        ].join(''));
+
+        this._contentElement = this._element.find('.lm_content');
+    };
+
+    lm.utils.copy(lm.container.ItemContainer.prototype, {
+
+		/**
+		 * Get the inner DOM element the container's content
+		 * is intended to live in
+		 *
+		 * @returns {DOM element}
+		 */
+        getElement: function () {
+            return this._contentElement;
+        },
+
+		/**
+		 * Hide the container. Notifies the containers content first
+		 * and then hides the DOM node. If the container is already hidden
+		 * this should have no effect
+		 *
+		 * @returns {void}
+		 */
+        hide: function () {
+            this.emit('hide');
+            this.isHidden = true;
+            this._element.hide();
+        },
+
+		/**
+		 * Shows a previously hidden container. Notifies the
+		 * containers content first and then shows the DOM element.
+		 * If the container is already visible this has no effect.
+		 *
+		 * @returns {void}
+		 */
+        show: function () {
+            this.emit('show');
+            this.isHidden = false;
+            this._element.show();
+            // call shown only if the container has a valid size
+            if (this.height != 0 || this.width != 0) {
+                this.emit('shown');
+            }
+        },
+
+		/**
+		 * Set the size from within the container. Traverses up
+		 * the item tree until it finds a row or column element
+		 * and resizes its items accordingly.
+		 *
+		 * If this container isn't a descendant of a row or column
+		 * it returns false
+		 * @todo  Rework!!!
+		 * @param {Number} width  The new width in pixel
+		 * @param {Number} height The new height in pixel
+		 *
+		 * @returns {Boolean} resizeSuccesful
+		 */
+        setSize: function (width, height) {
+            var rowOrColumn = this.parent,
+                rowOrColumnChild = this,
+                totalPixel,
+                percentage,
+                direction,
+                newSize,
+                delta,
+                i;
+
+            while (!rowOrColumn.isColumn && !rowOrColumn.isRow) {
+                rowOrColumnChild = rowOrColumn;
+                rowOrColumn = rowOrColumn.parent;
+
+
+				/**
+				 * No row or column has been found
+				 */
+                if (rowOrColumn.isRoot) {
+                    return false;
+                }
+            }
+
+            direction = rowOrColumn.isColumn ? "height" : "width";
+            newSize = direction === "height" ? height : width;
+
+            totalPixel = this[direction] * (1 / (rowOrColumnChild.config[direction] / 100));
+            percentage = (newSize / totalPixel) * 100;
+            delta = (rowOrColumnChild.config[direction] - percentage) / (rowOrColumn.contentItems.length - 1);
+
+            for (i = 0; i < rowOrColumn.contentItems.length; i++) {
+                if (rowOrColumn.contentItems[i] === rowOrColumnChild) {
+                    rowOrColumn.contentItems[i].config[direction] = percentage;
+                } else {
+                    rowOrColumn.contentItems[i].config[direction] += delta;
+                }
+            }
+
+            rowOrColumn.callDownwards('setSize');
+
+            return true;
+        },
+
+		/**
+		 * Closes the container if it is closable. Can be called by
+		 * both the component within at as well as the contentItem containing
+		 * it. Emits a close event before the container itself is closed.
+		 *
+		 * @returns {void}
+		 */
+        close: function () {
+            if (this._config.isClosable) {
+                this.emit('close');
+                this.parent.close();
+            }
+        },
+
+		/**
+		 * Returns the current state object
+		 *
+		 * @returns {Object} state
+		 */
+        getState: function () {
+            return this._config.componentState;
+        },
+
+		/**
+		 * Merges the provided state into the current one
+		 *
+		 * @param   {Object} state
+		 *
+		 * @returns {void}
+		 */
+        extendState: function (state) {
+            this.setState($.extend(true, this.getState(), state));
+        },
+
+		/**
+		 * Notifies the layout manager of a stateupdate
+		 *
+		 * @param {serialisable} state
+		 */
+        setState: function (state) {
+            this._config.componentState = state;
+            this.parent.emitBubblingEvent('stateChanged');
+        },
+
+		/**
+		 * Set's the components title
+		 *
+		 * @param {String} title
+		 */
+        setTitle: function (title) {
+            this.parent.setTitle(title);
+        },
+
+		/**
+		 * Set's the containers size. Called by the container's component.
+		 * To set the size programmatically from within the container please
+		 * use the public setSize method
+		 *
+		 * @param {[Int]} width  in px
+		 * @param {[Int]} height in px
+		 *
+		 * @returns {void}
+		 */
+        _$setSize: function (width, height) {
+            if (width !== this.width || height !== this.height) {
+                this.width = width;
+                this.height = height;
+                var cl = this._contentElement[0];
+                var hdelta = cl.offsetWidth - cl.clientWidth;
+                var vdelta = cl.offsetHeight - cl.clientHeight;
+                this._contentElement.width(this.width - hdelta)
+                    .height(this.height - vdelta);
+                this.emit('resize');
+            }
+        }
+    });
+
+	/**
+	 * Pops a content item out into a new browser window.
+	 * This is achieved by
+	 *
+	 *    - Creating a new configuration with the content item as root element
+	 *    - Serializing and minifying the configuration
+	 *    - Opening the current window's URL with the configuration as a GET parameter
+	 *    - GoldenLayout when opened in the new window will look for the GET parameter
+	 *      and use it instead of the provided configuration
+	 *
+	 * @param {Object} config GoldenLayout item config
+	 * @param {Object} dimensions A map with width, height, top and left
+	 * @param {String} parentId The id of the element the item will be appended to on popIn
+	 * @param {Number} indexInParent The position of this element within its parent
+	 * @param {lm.LayoutManager} layoutManager
+	 */
+    lm.controls.BrowserPopout = function (config, dimensions, parentId, indexInParent, layoutManager) {
+        lm.utils.EventEmitter.call(this);
+        this.isInitialised = false;
+
+        this._config = config;
+        this._dimensions = dimensions;
+        this._parentId = parentId;
+        this._indexInParent = indexInParent;
+        this._layoutManager = layoutManager;
+        this._popoutWindow = null;
+        this._id = null;
+        this._createWindow();
+    };
+
+    lm.utils.copy(lm.controls.BrowserPopout.prototype, {
+
+        toConfig: function () {
+            if (this.isInitialised === false) {
+                throw new Error('Can\'t create config, layout not yet initialised');
+                return;
+            }
+            return {
+                dimensions: {
+                    width: this.getGlInstance().width,
+                    height: this.getGlInstance().height,
+                    left: this._popoutWindow.screenX || this._popoutWindow.screenLeft,
+                    top: this._popoutWindow.screenY || this._popoutWindow.screenTop
+                },
+                content: this.getGlInstance().toConfig().content,
+                parentId: this._parentId,
+                indexInParent: this._indexInParent
+            };
+        },
+
+        getGlInstance: function () {
+            return this._popoutWindow.__glInstance;
+        },
+
+        getWindow: function () {
+            return this._popoutWindow;
+        },
+
+        close: function () {
+            if (this.getGlInstance()) {
+                this.getGlInstance()._$closeWindow();
+            } else {
+                try {
+                    this.getWindow().close();
+                } catch (e) {
+                }
+            }
+        },
+
+		/**
+		 * Returns the popped out item to its original position. If the original
+		 * parent isn't available anymore it falls back to the layout's topmost element
+		 */
+        popIn: function () {
+            var childConfig,
+                parentItem,
+                index = this._indexInParent;
+
+            if (this._parentId) {
+
+				/*
+				 * The $.extend call seems a bit pointless, but it's crucial to
+				 * copy the config returned by this.getGlInstance().toConfig()
+				 * onto a new object. Internet Explorer keeps the references
+				 * to objects on the child window, resulting in the following error
+				 * once the child window is closed:
+				 *
+				 * The callee (server [not server application]) is not available and disappeared
+				 */
+                childConfig = $.extend(true, {}, this.getGlInstance().toConfig()).content[0];
+                parentItem = this._layoutManager.root.getItemsById(this._parentId)[0];
+
+				/*
+				 * Fallback if parentItem is not available. Either add it to the topmost
+				 * item or make it the topmost item if the layout is empty
+				 */
+                if (!parentItem) {
+                    if (this._layoutManager.root.contentItems.length > 0) {
+                        parentItem = this._layoutManager.root.contentItems[0];
+                    } else {
+                        parentItem = this._layoutManager.root;
+                    }
+                    index = 0;
+                }
+            }
+
+            parentItem.addChild(childConfig, this._indexInParent);
+            this.close();
+        },
+
+		/**
+		 * Creates the URL and window parameter
+		 * and opens a new window
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _createWindow: function () {
+            var checkReadyInterval,
+                url = this._createUrl(),
+
+				/**
+				 * Bogus title to prevent re-usage of existing window with the
+				 * same title. The actual title will be set by the new window's
+				 * GoldenLayout instance if it detects that it is in subWindowMode
+				 */
+                title = Math.floor(Math.random() * 1000000).toString(36),
+
+				/**
+				 * The options as used in the window.open string
+				 */
+                options = this._serializeWindowOptions({
+                    width: this._dimensions.width,
+                    height: this._dimensions.height,
+                    innerWidth: this._dimensions.width,
+                    innerHeight: this._dimensions.height,
+                    menubar: 'no',
+                    toolbar: 'no',
+                    location: 'no',
+                    personalbar: 'no',
+                    resizable: 'yes',
+                    scrollbars: 'no',
+                    status: 'no'
+                });
+
+            this._popoutWindow = window.open(url, title, options);
+
+            if (!this._popoutWindow) {
+                if (this._layoutManager.config.settings.blockedPopoutsThrowError === true) {
+                    var error = new Error('Popout blocked');
+                    error.type = 'popoutBlocked';
+                    throw error;
+                } else {
+                    return;
+                }
+            }
+
+            $(this._popoutWindow)
+                .on('load', lm.utils.fnBind(this._positionWindow, this))
+                .on('unload beforeunload', lm.utils.fnBind(this._onClose, this));
+
+			/**
+			 * Polling the childwindow to find out if GoldenLayout has been initialised
+			 * doesn't seem optimal, but the alternatives - adding a callback to the parent
+			 * window or raising an event on the window object - both would introduce knowledge
+			 * about the parent to the child window which we'd rather avoid
+			 */
+            checkReadyInterval = setInterval(lm.utils.fnBind(function () {
+                if (this._popoutWindow.__glInstance && this._popoutWindow.__glInstance.isInitialised) {
+                    this._onInitialised();
+                    clearInterval(checkReadyInterval);
+                }
+            }, this), 10);
+        },
+
+		/**
+		 * Serialises a map of key:values to a window options string
+		 *
+		 * @param   {Object} windowOptions
+		 *
+		 * @returns {String} serialised window options
+		 */
+        _serializeWindowOptions: function (windowOptions) {
+            var windowOptionsString = [], key;
+
+            for (key in windowOptions) {
+                windowOptionsString.push(key + '=' + windowOptions[key]);
+            }
+
+            return windowOptionsString.join(',');
+        },
+
+		/**
+		 * Creates the URL for the new window, including the
+		 * config GET parameter
+		 *
+		 * @returns {String} URL
+		 */
+        _createUrl: function () {
+            var config = { content: this._config },
+                storageKey = 'gl-window-config-' + lm.utils.getUniqueId(),
+                urlParts;
+
+            config = (new lm.utils.ConfigMinifier()).minifyConfig(config);
+
+            try {
+                localStorage.setItem(storageKey, JSON.stringify(config));
+            } catch (e) {
+                throw new Error('Error while writing to localStorage ' + e.toString());
+            }
+
+            urlParts = document.location.href.split('?');
+
+            // URL doesn't contain GET-parameters
+            if (urlParts.length === 1) {
+                return urlParts[0] + '?gl-window=' + storageKey;
+
+                // URL contains GET-parameters
+            } else {
+                return document.location.href + '&gl-window=' + storageKey;
+            }
+        },
+
+		/**
+		 * Move the newly created window roughly to
+		 * where the component used to be.
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _positionWindow: function () {
+            this._popoutWindow.moveTo(this._dimensions.left, this._dimensions.top);
+            this._popoutWindow.focus();
+        },
+
+		/**
+		 * Callback when the new window is opened and the GoldenLayout instance
+		 * within it is initialised
+		 *
+		 * @returns {void}
+		 */
+        _onInitialised: function () {
+            this.isInitialised = true;
+            this.getGlInstance().on('popIn', this.popIn, this);
+            this.emit('initialised');
+        },
+
+		/**
+		 * Invoked 50ms after the window unload event
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _onClose: function () {
+            setTimeout(lm.utils.fnBind(this.emit, this, ['closed']), 50);
+        }
+    });
+	/**
+	 * This class creates a temporary container
+	 * for the component whilst it is being dragged
+	 * and handles drag events
+	 *
+	 * @constructor
+	 * @private
+	 *
+	 * @param {Number} x              The initial x position
+	 * @param {Number} y              The initial y position
+	 * @param {lm.utils.DragListener} dragListener
+	 * @param {lm.LayoutManager} layoutManager
+	 * @param {lm.item.AbstractContentItem} contentItem
+	 * @param {lm.item.AbstractContentItem} originalParent
+	 */
+    lm.controls.DragProxy = function (x, y, dragListener, layoutManager, contentItem, originalParent) {
+
+        lm.utils.EventEmitter.call(this);
+
+        this._dragListener = dragListener;
+        this._layoutManager = layoutManager;
+        this._contentItem = contentItem;
+        this._originalParent = originalParent;
+
+        this._area = null;
+        this._lastValidArea = null;
+
+        this._dragListener.on('drag', this._onDrag, this);
+        this._dragListener.on('dragStop', this._onDrop, this);
+
+        this.element = $(lm.controls.DragProxy._template);
+        if (originalParent && originalParent._side) {
+            this._sided = originalParent._sided;
+            this.element.addClass('lm_' + originalParent._side);
+            if (['right', 'bottom'].indexOf(originalParent._side) >= 0)
+                this.element.find('.lm_content').after(this.element.find('.lm_header'));
+        }
+        this.element.css({ left: x, top: y });
+        this.element.find('.lm_tab').attr('title', lm.utils.stripTags(this._contentItem.config.title));
+        this.element.find('.lm_title').html(this._contentItem.config.title);
+        this.childElementContainer = this.element.find('.lm_content');
+        this.childElementContainer.append(contentItem.element);
+
+        this._updateTree();
+        this._layoutManager._$calculateItemAreas();
+        this._setDimensions();
+
+        $(document.body).append(this.element);
+
+        var offset = this._layoutManager.container.offset();
+
+        this._minX = offset.left;
+        this._minY = offset.top;
+        this._maxX = this._layoutManager.container.width() + this._minX;
+        this._maxY = this._layoutManager.container.height() + this._minY;
+        this._width = this.element.width();
+        this._height = this.element.height();
+
+        this._setDropPosition(x, y);
+    };
+
+    lm.controls.DragProxy._template = '<div class="lm_dragProxy">' +
+        '<div class="lm_header">' +
+        '<ul class="lm_tabs">' +
+        '<li class="lm_tab lm_active"><i class="lm_left"></i>' +
+        '<span class="lm_title"></span>' +
+        '<i class="lm_right"></i></li>' +
+        '</ul>' +
+        '</div>' +
+        '<div class="lm_content"></div>' +
+        '</div>';
+
+    lm.utils.copy(lm.controls.DragProxy.prototype, {
+
+		/**
+		 * Callback on every mouseMove event during a drag. Determines if the drag is
+		 * still within the valid drag area and calls the layoutManager to highlight the
+		 * current drop area
+		 *
+		 * @param   {Number} offsetX The difference from the original x position in px
+		 * @param   {Number} offsetY The difference from the original y position in px
+		 * @param   {jQuery DOM event} event
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _onDrag: function (offsetX, offsetY, event) {
+
+            event = event.originalEvent && event.originalEvent.touches ? event.originalEvent.touches[0] : event;
+
+            var x = event.pageX,
+                y = event.pageY,
+                isWithinContainer = x > this._minX && x < this._maxX && y > this._minY && y < this._maxY;
+
+            if (!isWithinContainer && this._layoutManager.config.settings.constrainDragToContainer === true) {
+                return;
+            }
+
+            this._setDropPosition(x, y);
+        },
+
+		/**
+		 * Sets the target position, highlighting the appropriate area
+		 *
+		 * @param   {Number} x The x position in px
+		 * @param   {Number} y The y position in px
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _setDropPosition: function (x, y) {
+            this.element.css({ left: x, top: y });
+            this._area = this._layoutManager._$getArea(x, y);
+
+            if (this._area !== null) {
+                this._lastValidArea = this._area;
+                this._area.contentItem._$highlightDropZone(x, y, this._area);
+            }
+        },
+
+		/**
+		 * Callback when the drag has finished. Determines the drop area
+		 * and adds the child to it
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _onDrop: function () {
+            this._layoutManager.dropTargetIndicator.hide();
+
+			/*
+			 * Valid drop area found
+			 */
+            if (this._area !== null) {
+                this._area.contentItem._$onDrop(this._contentItem, this._area);
+
+				/**
+				 * No valid drop area available at present, but one has been found before.
+				 * Use it
+				 */
+            } else if (this._lastValidArea !== null) {
+                this._lastValidArea.contentItem._$onDrop(this._contentItem, this._lastValidArea);
+
+				/**
+				 * No valid drop area found during the duration of the drag. Return
+				 * content item to its original position if a original parent is provided.
+				 * (Which is not the case if the drag had been initiated by createDragSource)
+				 */
+            } else if (this._originalParent) {
+                this._originalParent.addChild(this._contentItem);
+
+				/**
+				 * The drag didn't ultimately end up with adding the content item to
+				 * any container. In order to ensure clean up happens, destroy the
+				 * content item.
+				 */
+            } else {
+                this._contentItem._$destroy();
+            }
+
+            this.element.remove();
+
+            this._layoutManager.emit('itemDropped', this._contentItem);
+        },
+
+		/**
+		 * Removes the item from its original position within the tree
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _updateTree: function () {
+
+			/**
+			 * parent is null if the drag had been initiated by a external drag source
+			 */
+            if (this._contentItem.parent) {
+                this._contentItem.parent.removeChild(this._contentItem, true);
+            }
+
+            this._contentItem._$setParent(this);
+        },
+
+		/**
+		 * Updates the Drag Proxie's dimensions
+		 *
+		 * @private
+		 *
+		 * @returns {void}
+		 */
+        _setDimensions: function () {
+            var dimensions = this._layoutManager.config.dimensions,
+                width = dimensions.dragProxyWidth,
+                height = dimensions.dragProxyHeight;
+
+            this.element.width(width);
+            this.element.height(height);
+            width -= (this._sided ? dimensions.headerHeight : 0);
+            height -= (!this._sided ? dimensions.headerHeight : 0);
+            this.childElementContainer.width(width);
+            this.childElementContainer.height(height);
+            this._contentItem.element.width(width);
+            this._contentItem.element.height(height);
+            this._contentItem.callDownwards('_$show');
+            this._contentItem.callDownwards('setSize');
+        }
+    });
+
+	/**
+	 * Allows for any DOM item to create a component on drag
+	 * start tobe dragged into the Layout
+	 *
+	 * @param {jQuery element} element
+	 * @param {Object} itemConfig the configuration for the contentItem that will be created
+	 * @param {LayoutManager} layoutManager
+	 *
+	 * @constructor
+	 */
+    lm.controls.DragSource = function (element, itemConfig, layoutManager) {
+        this._element = element;
+        this._itemConfig = itemConfig;
+        this._layoutManager = layoutManager;
+        this._dragListener = null;
+
+        this._createDragListener();
+    };
+
+    lm.utils.copy(lm.controls.DragSource.prototype, {
+
+		/**
+		 * Called initially and after every drag
+		 *
+		 * @returns {void}
+		 */
+        _createDragListener: function () {
+            if (this._dragListener !== null) {
+                this._dragListener.destroy();
+            }
+
+            this._dragListener = new lm.utils.DragListener(this._element);
+            this._dragListener.on('dragStart', this._onDragStart, this);
+            this._dragListener.on('dragStop', this._createDragListener, this);
+        },
+
+		/**
+		 * Callback for the DragListener's dragStart event
+		 *
+		 * @param   {int} x the x position of the mouse on dragStart
+		 * @param   {int} y the x position of the mouse on dragStart
+		 *
+		 * @returns {void}
+		 */
+        _onDragStart: function (x, y) {
+            var itemConfig = this._itemConfig;
+            if (lm.utils.isFunction(itemConfig)) {
+                itemConfig = itemConfig();
+            }
+            var contentItem = this._layoutManager._$normalizeContentItem($.extend(true, {}, itemConfig)),
+                dragProxy = new lm.controls.DragProxy(x, y, this._dragListener, this._layoutManager, contentItem, null);
+
+            this._layoutManager.transitionIndicator.transitionElements(this._element, dragProxy.element);
+        }
+    });
+
+    lm.controls.DropTargetIndicator = function () {
+        this.element = $(lm.controls.DropTargetIndicator._template);
+        $(document.body).append(this.element);
+    };
+
+    lm.controls.DropTargetIndicator._template = '<div class="lm_dropTargetIndicator"><div class="lm_inner"></div></div>';
+
+    lm.utils.copy(lm.controls.DropTargetIndicator.prototype, {
+        destroy: function () {
+            this.element.remove();
+        },
+
+        highlight: function (x1, y1, x2, y2) {
+            this.highlightArea({ x1: x1, y1: y1, x2: x2, y2: y2 });
+        },
+
+        highlightArea: function (area) {
+            this.element.css({
+                left: area.x1,
+                top: area.y1,
+                width: area.x2 - area.x1,
+                height: area.y2 - area.y1
+            }).show();
+        },
+
+        hide: function () {
+            this.element.hide();
+        }
+    });
+	/**
+	 * This class represents a header above a Stack ContentItem.
+	 *
+	 * @param {lm.LayoutManager} layoutManager
+	 * @param {lm.item.AbstractContentItem} parent
+	 */
+    lm.controls.Header = function (layoutManager, parent) {
+        lm.utils.EventEmitter.call(this);
+
+        this.layoutManager = layoutManager;
+        this.element = $(lm.controls.Header._template);
+
+        if (this.layoutManager.config.settings.selectionEnabled === true) {
+            this.element.addClass('lm_selectable');
+            this.element.on('click touchstart', lm.utils.fnBind(this._onHeaderClick, this));
+        }
+
+        this.tabsContainer = this.element.find('.lm_tabs');
+        this.tabDropdownContainer = this.element.find('.lm_tabdropdown_list');
+        this.tabDropdownContainer.hide();
+        this.controlsContainer = this.element.find('.lm_controls');
+        this.parent = parent;
+        this.parent.on('resize', this._updateTabSizes, this);
+        this.tabs = [];
+        this.activeContentItem = null;
+        this.closeButton = null;
+        this.tabDropdownButton = null;
+        this.hideAdditionalTabsDropdown = lm.utils.fnBind(this._hideAdditionalTabsDropdown, this);
+        $(document).mouseup(this.hideAdditionalTabsDropdown);
+
+        this._lastVisibleTabIndex = -1;
+        this._tabControlOffset = this.layoutManager.config.settings.tabControlOffset;
+        this._createControls();
+    };
+
+    lm.controls.Header._template = [
+        '<div class="lm_header">',
+        '<ul class="lm_tabs"></ul>',
+        '<ul class="lm_controls"></ul>',
+        '<ul class="lm_tabdropdown_list"></ul>',
+        '</div>'
+    ].join('');
+
+    lm.utils.copy(lm.controls.Header.prototype, {
+
+		/**
+		 * Creates a new tab and associates it with a contentItem
+		 *
+		 * @param    {lm.item.AbstractContentItem} contentItem
+		 * @param    {Integer} index The position of the tab
+		 *
+		 * @returns {void}
+		 */
+        createTab: function (contentItem, index) {
+            var tab, i;
+
+            //If there's already a tab relating to the
+            //content item, don't do anything
+            for (i = 0; i < this.tabs.length; i++) {
+                if (this.tabs[i].contentItem === contentItem) {
+                    return;
+                }
+            }
+
+            tab = new lm.controls.Tab(this, contentItem);
+
+            if (this.tabs.length === 0) {
+                this.tabs.push(tab);
+                this.tabsContainer.append(tab.element);
+                return;
+            }
+
+            if (index === undefined) {
+                index = this.tabs.length;
+            }
+
+            if (index > 0) {
+                this.tabs[index - 1].element.after(tab.element);
+            } else {
+                this.tabs[0].element.before(tab.element);
+            }
+
+            this.tabs.splice(index, 0, tab);
+            this._updateTabSizes();
+        },
+
+		/**
+		 * Finds a tab based on the contentItem its associated with and removes it.
+		 *
+		 * @param    {lm.item.AbstractContentItem} contentItem
+		 *
+		 * @returns {void}
+		 */
+        removeTab: function (contentItem) {
+            for (var i = 0; i < this.tabs.length; i++) {
+                if (this.tabs[i].contentItem === contentItem) {
+                    this.tabs[i]._$destroy();
+                    this.tabs.splice(i, 1);
+                    return;
+                }
+            }
+
+            throw new Error('contentItem is not controlled by this header');
+        },
+
+		/**
+		 * The programmatical equivalent of clicking a Tab.
+		 *
+		 * @param {lm.item.AbstractContentItem} contentItem
+		 */
+        setActiveContentItem: function (contentItem) {
+            var i, j, isActive, activeTab;
+
+            for (i = 0; i < this.tabs.length; i++) {
+                isActive = this.tabs[i].contentItem === contentItem;
+                this.tabs[i].setActive(isActive);
+                if (isActive === true) {
+                    this.activeContentItem = contentItem;
+                    this.parent.config.activeItemIndex = i;
+                }
+            }
+
+            if (this.layoutManager.config.settings.reorderOnTabMenuClick) {
+				/**
+				 * If the tab selected was in the dropdown, move everything down one to make way for this one to be the first.
+				 * This will make sure the most used tabs stay visible.
+				 */
+                if (this._lastVisibleTabIndex !== -1 && this.parent.config.activeItemIndex > this._lastVisibleTabIndex) {
+                    activeTab = this.tabs[this.parent.config.activeItemIndex];
+                    for (j = this.parent.config.activeItemIndex; j > 0; j--) {
+                        this.tabs[j] = this.tabs[j - 1];
+                    }
+                    this.tabs[0] = activeTab;
+                    this.parent.config.activeItemIndex = 0;
+                }
+            }
+
+            this._updateTabSizes();
+            this.parent.emitBubblingEvent('stateChanged');
+        },
+
+		/**
+		 * Programmatically operate with header position.
+		 *
+		 * @param {string} position one of ('top','left','right','bottom') to set or empty to get it.
+		 *
+		 * @returns {string} previous header position
+		 */
+        position: function (position) {
+            var previous = this.parent._header.show;
+            if (previous && !this.parent._side)
+                previous = 'top';
+            if (position !== undefined && this.parent._header.show != position) {
+                this.parent._header.show = position;
+                this.parent._setupHeaderPosition();
+            }
+            return previous;
+        },
+
+		/**
+		 * Programmatically set closability.
+		 *
+		 * @package private
+		 * @param {Boolean} isClosable Whether to enable/disable closability.
+		 *
+		 * @returns {Boolean} Whether the action was successful
+		 */
+        _$setClosable: function (isClosable) {
+            if (this.closeButton && this._isClosable()) {
+                this.closeButton.element[isClosable ? "show" : "hide"]();
+                return true;
+            }
+
+            return false;
+        },
+
+		/**
+		 * Destroys the entire header
+		 *
+		 * @package private
+		 *
+		 * @returns {void}
+		 */
+        _$destroy: function () {
+            this.emit('destroy', this);
+
+            for (var i = 0; i < this.tabs.length; i++) {
+                this.tabs[i]._$destroy();
+            }
+            $(document).off('mouseup', this.hideAdditionalTabsDropdown);
+            this.element.remove();
+        },
+
+		/**
+		 * get settings from header
+		 *
+		 * @returns {string} when exists
+		 */
+        _getHeaderSetting: function (name) {
+            if (name in this.parent._header)
+                return this.parent._header[name];
+        },
+		/**
+		 * Creates the popout, maximise and close buttons in the header's top right corner
+		 *
+		 * @returns {void}
+		 */
+        _createControls: function () {
+            var closeStack,
+                popout,
+                label,
+                maximiseLabel,
+                minimiseLabel,
+                maximise,
+                maximiseButton,
+                tabDropdownLabel,
+                showTabDropdown;
+
+			/**
+			 * Dropdown to show additional tabs.
+			 */
+            showTabDropdown = lm.utils.fnBind(this._showAdditionalTabsDropdown, this);
+            tabDropdownLabel = this.layoutManager.config.labels.tabDropdown;
+            this.tabDropdownButton = new lm.controls.HeaderButton(this, tabDropdownLabel, 'lm_tabdropdown', showTabDropdown);
+            this.tabDropdownButton.element.hide();
+
+			/**
+			 * Popout control to launch component in new window.
+			 */
+            if (this._getHeaderSetting('popout')) {
+                popout = lm.utils.fnBind(this._onPopoutClick, this);
+                label = this._getHeaderSetting('popout');
+                new lm.controls.HeaderButton(this, label, 'lm_popout', popout);
+            }
+
+			/**
+			 * Maximise control - set the component to the full size of the layout
+			 */
+            if (this._getHeaderSetting('maximise')) {
+                maximise = lm.utils.fnBind(this.parent.toggleMaximise, this.parent);
+                maximiseLabel = this._getHeaderSetting('maximise');
+                minimiseLabel = this._getHeaderSetting('minimise');
+                maximiseButton = new lm.controls.HeaderButton(this, maximiseLabel, 'lm_maximise', maximise);
+
+                this.parent.on('maximised', function () {
+                    maximiseButton.element.attr('title', minimiseLabel);
+                });
+
+                this.parent.on('minimised', function () {
+                    maximiseButton.element.attr('title', maximiseLabel);
+                });
+            }
+
+			/**
+			 * Close button
+			 */
+            if (this._isClosable()) {
+                closeStack = lm.utils.fnBind(this.parent.remove, this.parent);
+                label = this._getHeaderSetting('close');
+                this.closeButton = new lm.controls.HeaderButton(this, label, 'lm_close', closeStack);
+            }
+        },
+
+		/**
+		 * Shows drop down for additional tabs when there are too many to display.
+		 *
+		 * @returns {void}
+		 */
+        _showAdditionalTabsDropdown: function () {
+            this.tabDropdownContainer.show();
+        },
+
+		/**
+		 * Hides drop down for additional tabs when there are too many to display.
+		 *
+		 * @returns {void}
+		 */
+        _hideAdditionalTabsDropdown: function (e) {
+            this.tabDropdownContainer.hide();
+        },
+
+		/**
+		 * Checks whether the header is closable based on the parent config and
+		 * the global config.
+		 *
+		 * @returns {Boolean} Whether the header is closable.
+		 */
+        _isClosable: function () {
+            return this.parent.config.isClosable && this.layoutManager.config.settings.showCloseIcon;
+        },
+
+        _onPopoutClick: function () {
+            if (this.layoutManager.config.settings.popoutWholeStack === true) {
+                this.parent.popout();
+            } else {
+                this.activeContentItem.popout();
+            }
+        },
+
+
+		/**
+		 * Invoked when the header's background is clicked (not it's tabs or controls)
+		 *
+		 * @param    {jQuery DOM event} event
+		 *
+		 * @returns {void}
+		 */
+        _onHeaderClick: function (event) {
+            if (event.target === this.element[0]) {
+                this.parent.select();
+            }
+        },
+
+		/**
+		 * Pushes the tabs to the tab dropdown if the available space is not sufficient
+		 *
+		 * @returns {void}
+		 */
+        _updateTabSizes: function (showTabMenu) {
+            if (this.tabs.length === 0) {
+                return;
+            }
+
+            //Show the menu based on function argument
+            this.tabDropdownButton.element.toggle(showTabMenu === true);
+
+            var size = function (val) {
+                return val ? 'width' : 'height';
+            };
+            this.element.css(size(!this.parent._sided), '');
+            this.element[size(this.parent._sided)](this.layoutManager.config.dimensions.headerHeight);
+            var availableWidth = this.element.outerWidth() - this.controlsContainer.outerWidth() - this._tabControlOffset,
+                cumulativeTabWidth = 0,
+                visibleTabWidth = 0,
+                tabElement,
+                i,
+                j,
+                marginLeft,
+                overlap = 0,
+                tabWidth,
+                tabOverlapAllowance = this.layoutManager.config.settings.tabOverlapAllowance,
+                tabOverlapAllowanceExceeded = false,
+                activeIndex = (this.activeContentItem ? this.tabs.indexOf(this.activeContentItem.tab) : 0),
+                activeTab = this.tabs[activeIndex];
+            if (this.parent._sided)
+                availableWidth = this.element.outerHeight() - this.controlsContainer.outerHeight() - this._tabControlOffset;
+            this._lastVisibleTabIndex = -1;
+
+            for (i = 0; i < this.tabs.length; i++) {
+                tabElement = this.tabs[i].element;
+
+                //Put the tab in the tabContainer so its true width can be checked
+                this.tabsContainer.append(tabElement);
+                tabWidth = tabElement.outerWidth() + parseInt(tabElement.css('margin-right'), 10);
+
+                cumulativeTabWidth += tabWidth;
+
+                //Include the active tab's width if it isn't already
+                //This is to ensure there is room to show the active tab
+                if (activeIndex <= i) {
+                    visibleTabWidth = cumulativeTabWidth;
+                } else {
+                    visibleTabWidth = cumulativeTabWidth + activeTab.element.outerWidth() + parseInt(activeTab.element.css('margin-right'), 10);
+                }
+
+                // If the tabs won't fit, check the overlap allowance.
+                if (visibleTabWidth > availableWidth) {
+
+                    //Once allowance is exceeded, all remaining tabs go to menu.
+                    if (!tabOverlapAllowanceExceeded) {
+
+                        //No overlap for first tab or active tab
+                        //Overlap spreads among non-active, non-first tabs
+                        if (activeIndex > 0 && activeIndex <= i) {
+                            overlap = (visibleTabWidth - availableWidth) / (i - 1);
+                        } else {
+                            overlap = (visibleTabWidth - availableWidth) / i;
+                        }
+
+                        //Check overlap against allowance.
+                        if (overlap < tabOverlapAllowance) {
+                            for (j = 0; j <= i; j++) {
+                                marginLeft = (j !== activeIndex && j !== 0) ? '-' + overlap + 'px' : '';
+                                this.tabs[j].element.css({ 'z-index': i - j, 'margin-left': marginLeft });
+                            }
+                            this._lastVisibleTabIndex = i;
+                            this.tabsContainer.append(tabElement);
+                        } else {
+                            tabOverlapAllowanceExceeded = true;
+                        }
+
+                    } else if (i === activeIndex) {
+                        //Active tab should show even if allowance exceeded. (We left room.)
+                        tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+                        this.tabsContainer.append(tabElement);
+                    }
+
+                    if (tabOverlapAllowanceExceeded && i !== activeIndex) {
+                        if (showTabMenu) {
+                            //Tab menu already shown, so we just add to it.
+                            tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+                            this.tabDropdownContainer.append(tabElement);
+                        } else {
+                            //We now know the tab menu must be shown, so we have to recalculate everything.
+                            this._updateTabSizes(true);
+                            return;
+                        }
+                    }
+
+                }
+                else {
+                    this._lastVisibleTabIndex = i;
+                    tabElement.css({ 'z-index': 'auto', 'margin-left': '' });
+                    this.tabsContainer.append(tabElement);
+                }
+            }
+
+        }
+    });
+
+
+    lm.controls.HeaderButton = function (header, label, cssClass, action) {
+        this._header = header;
+        this.element = $('<li class="' + cssClass + '" title="' + label + '"></li>');
+        this._header.on('destroy', this._$destroy, this);
+        this._action = action;
+        this.element.on('click touchstart', this._action);
+        this._header.controlsContainer.append(this.element);
+    };
+
+    lm.utils.copy(lm.controls.HeaderButton.prototype, {
+        _$destroy: function () {
+            this.element.off();
+            this.element.remove();
+        }
+    });
+    lm.controls.Splitter = function (isVertical, size, grabSize) {
+        this._isVertical = isVertical;
+        this._size = size;
+        this._grabSize = grabSize < size ? size : grabSize;
+
+        this.element = this._createElement();
+        this._dragListener = new lm.utils.DragListener(this.element);
+    };
+
+    lm.utils.copy(lm.controls.Splitter.prototype, {
+        on: function (event, callback, context) {
+            this._dragListener.on(event, callback, context);
+        },
+
+        _$destroy: function () {
+            this.element.remove();
+        },
+
+        _createElement: function () {
+            var dragHandle = $('<div class="lm_drag_handle"></div>');
+            var element = $('<div class="lm_splitter"></div>');
+            element.append(dragHandle);
+
+            var handleExcessSize = this._grabSize - this._size;
+            var handleExcessPos = handleExcessSize / 2;
+
+            if (this._isVertical) {
+                dragHandle.css('top', -handleExcessPos);
+                dragHandle.css('height', this._size + handleExcessSize);
+                element.addClass('lm_vertical');
+                element['height'](this._size);
+            } else {
+                dragHandle.css('left', -handleExcessPos);
+                dragHandle.css('width', this._size + handleExcessSize);
+                element.addClass('lm_horizontal');
+                element['width'](this._size);
+            }
+
+            return element;
+        }
+    });
+
+	/**
+	 * Represents an individual tab within a Stack's header
+	 *
+	 * @param {lm.controls.Header} header
+	 * @param {lm.items.AbstractContentItem} contentItem
+	 *
+	 * @constructor
+	 */
+    lm.controls.Tab = function (header, contentItem) {
+        this.header = header;
+        this.contentItem = contentItem;
+        this.element = $(lm.controls.Tab._template);
+        this.titleElement = this.element.find('.lm_title');
+        this.closeElement = this.element.find('.lm_close_tab');
+        this.closeElement[contentItem.config.isClosable ? 'show' : 'hide']();
+        this.isActive = false;
+
+        this.setTitle(contentItem.config.title);
+        this.contentItem.on('titleChanged', this.setTitle, this);
+
+        this._layoutManager = this.contentItem.layoutManager;
+
+        if (
+            this._layoutManager.config.settings.reorderEnabled === true &&
+            contentItem.config.reorderEnabled === true
+        ) {
+            this._dragListener = new lm.utils.DragListener(this.element);
+            this._dragListener.on('dragStart', this._onDragStart, this);
+            this.contentItem.on('destroy', this._dragListener.destroy, this._dragListener);
+        }
+
+        this._onTabClickFn = lm.utils.fnBind(this._onTabClick, this);
+        this._onCloseClickFn = lm.utils.fnBind(this._onCloseClick, this);
+
+        this.element.on('mousedown touchstart', this._onTabClickFn);
+
+        if (this.contentItem.config.isClosable) {
+            this.closeElement.on('click touchstart', this._onCloseClickFn);
+            this.closeElement.on('mousedown', this._onCloseMousedown);
+        } else {
+            this.closeElement.remove();
+        }
+
+        this.contentItem.tab = this;
+        this.contentItem.emit('tab', this);
+        this.contentItem.layoutManager.emit('tabCreated', this);
+
+        if (this.contentItem.isComponent) {
+            this.contentItem.container.tab = this;
+            this.contentItem.container.emit('tab', this);
+        }
+    };
+
+	/**
+	 * The tab's html template
+	 *
+	 * @type {String}
+	 */
+    lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
+        '<span class="lm_title"></span><div class="lm_close_tab"></div>' +
+        '<i class="lm_right"></i></li>';
+
+    lm.utils.copy(lm.controls.Tab.prototype, {
+
+		/**
+		 * Sets the tab's title to the provided string and sets
+		 * its title attribute to a pure text representation (without
+		 * html tags) of the same string.
+		 *
+		 * @public
+		 * @param {String} title can contain html
+		 */
+        setTitle: function (title) {
+            this.element.attr('title', lm.utils.stripTags(title));
+            this.titleElement.html(title);
+        },
+
+		/**
+		 * Sets this tab's active state. To programmatically
+		 * switch tabs, use header.setActiveContentItem( item ) instead.
+		 *
+		 * @public
+		 * @param {Boolean} isActive
+		 */
+        setActive: function (isActive) {
+            if (isActive === this.isActive) {
+                return;
+            }
+            this.isActive = isActive;
+
+            if (isActive) {
+                this.element.addClass('lm_active');
+            } else {
+                this.element.removeClass('lm_active');
+            }
+        },
+
+		/**
+		 * Destroys the tab
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _$destroy: function () {
+            this.element.off('mousedown touchstart', this._onTabClickFn);
+            this.closeElement.off('click touchstart', this._onCloseClickFn);
+            if (this._dragListener) {
+                this.contentItem.off('destroy', this._dragListener.destroy, this._dragListener);
+                this._dragListener.off('dragStart', this._onDragStart);
+                this._dragListener = null;
+            }
+            this.element.remove();
+        },
+
+		/**
+		 * Callback for the DragListener
+		 *
+		 * @param   {Number} x The tabs absolute x position
+		 * @param   {Number} y The tabs absolute y position
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _onDragStart: function (x, y) {
+            if (this.contentItem.parent.isMaximised === true) {
+                this.contentItem.parent.toggleMaximise();
+            }
+            new lm.controls.DragProxy(
+                x,
+                y,
+                this._dragListener,
+                this._layoutManager,
+                this.contentItem,
+                this.header.parent
+            );
+        },
+
+		/**
+		 * Callback when the tab is clicked
+		 *
+		 * @param {jQuery DOM event} event
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _onTabClick: function (event) {
+            // left mouse button or tap
+            if (event.button === 0 || event.type === 'touchstart') {
+                var activeContentItem = this.header.parent.getActiveContentItem();
+                if (this.contentItem !== activeContentItem) {
+                    this.header.parent.setActiveContentItem(this.contentItem);
+                }
+
+                // middle mouse button
+            } else if (event.button === 1 && this.contentItem.config.isClosable) {
+                this._onCloseClick(event);
+            }
+        },
+
+		/**
+		 * Callback when the tab's close button is
+		 * clicked
+		 *
+		 * @param   {jQuery DOM event} event
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _onCloseClick: function (event) {
+            event.stopPropagation();
+            this.header.parent.removeChild(this.contentItem);
+        },
+
+
+		/**
+		 * Callback to capture tab close button mousedown
+		 * to prevent tab from activating.
+		 *
+		 * @param (jQuery DOM event) event
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _onCloseMousedown: function (event) {
+            event.stopPropagation();
+        }
+    });
+
+    lm.controls.TransitionIndicator = function () {
+        this._element = $('<div class="lm_transition_indicator"></div>');
+        $(document.body).append(this._element);
+
+        this._toElement = null;
+        this._fromDimensions = null;
+        this._totalAnimationDuration = 200;
+        this._animationStartTime = null;
+    };
+
+    lm.utils.copy(lm.controls.TransitionIndicator.prototype, {
+        destroy: function () {
+            this._element.remove();
+        },
+
+        transitionElements: function (fromElement, toElement) {
+			/**
+			 * TODO - This is not quite as cool as expected. Review.
+			 */
+            return;
+            this._toElement = toElement;
+            this._animationStartTime = lm.utils.now();
+            this._fromDimensions = this._measure(fromElement);
+            this._fromDimensions.opacity = 0.8;
+            this._element.show().css(this._fromDimensions);
+            lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+        },
+
+        _nextAnimationFrame: function () {
+            var toDimensions = this._measure(this._toElement),
+                animationProgress = (lm.utils.now() - this._animationStartTime) / this._totalAnimationDuration,
+                currentFrameStyles = {},
+                cssProperty;
+
+            if (animationProgress >= 1) {
+                this._element.hide();
+                return;
+            }
+
+            toDimensions.opacity = 0;
+
+            for (cssProperty in this._fromDimensions) {
+                currentFrameStyles[cssProperty] = this._fromDimensions[cssProperty] +
+                    (toDimensions[cssProperty] - this._fromDimensions[cssProperty]) *
+                    animationProgress;
+            }
+
+            this._element.css(currentFrameStyles);
+            lm.utils.animFrame(lm.utils.fnBind(this._nextAnimationFrame, this));
+        },
+
+        _measure: function (element) {
+            var offset = element.offset();
+
+            return {
+                left: offset.left,
+                top: offset.top,
+                width: element.outerWidth(),
+                height: element.outerHeight()
+            };
+        }
+    });
+    lm.errors.ConfigurationError = function (message, node) {
+        Error.call(this);
+
+        this.name = 'Configuration Error';
+        this.message = message;
+        this.node = node;
+    };
+
+    lm.errors.ConfigurationError.prototype = new Error();
+
+	/**
+	 * This is the baseclass that all content items inherit from.
+	 * Most methods provide a subset of what the sub-classes do.
+	 *
+	 * It also provides a number of functions for tree traversal
+	 *
+	 * @param {lm.LayoutManager} layoutManager
+	 * @param {item node configuration} config
+	 * @param {lm.item} parent
+	 *
+	 * @event stateChanged
+	 * @event beforeItemDestroyed
+	 * @event itemDestroyed
+	 * @event itemCreated
+	 * @event componentCreated
+	 * @event rowCreated
+	 * @event columnCreated
+	 * @event stackCreated
+	 *
+	 * @constructor
+	 */
+    lm.items.AbstractContentItem = function (layoutManager, config, parent) {
+        lm.utils.EventEmitter.call(this);
+
+        this.config = this._extendItemNode(config);
+        this.type = config.type;
+        this.contentItems = [];
+        this.parent = parent;
+
+        this.isInitialised = false;
+        this.isMaximised = false;
+        this.isRoot = false;
+        this.isRow = false;
+        this.isColumn = false;
+        this.isStack = false;
+        this.isComponent = false;
+
+        this.layoutManager = layoutManager;
+        this._pendingEventPropagations = {};
+        this._throttledEvents = ['stateChanged'];
+
+        this.on(lm.utils.EventEmitter.ALL_EVENT, this._propagateEvent, this);
+
+        if (config.content) {
+            this._createContentItems(config);
+        }
+    };
+
+    lm.utils.copy(lm.items.AbstractContentItem.prototype, {
+
+		/**
+		 * Set the size of the component and its children, called recursively
+		 *
+		 * @abstract
+		 * @returns void
+		 */
+        setSize: function () {
+            throw new Error('Abstract Method');
+        },
+
+		/**
+		 * Calls a method recursively downwards on the tree
+		 *
+		 * @param   {String} functionName      the name of the function to be called
+		 * @param   {[Array]}functionArguments optional arguments that are passed to every function
+		 * @param   {[bool]} bottomUp          Call methods from bottom to top, defaults to false
+		 * @param   {[bool]} skipSelf          Don't invoke the method on the class that calls it, defaults to false
+		 *
+		 * @returns {void}
+		 */
+        callDownwards: function (functionName, functionArguments, bottomUp, skipSelf) {
+            var i;
+
+            if (bottomUp !== true && skipSelf !== true) {
+                this[functionName].apply(this, functionArguments || []);
+            }
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.contentItems[i].callDownwards(functionName, functionArguments, bottomUp);
+            }
+            if (bottomUp === true && skipSelf !== true) {
+                this[functionName].apply(this, functionArguments || []);
+            }
+        },
+
+		/**
+		 * Removes a child node (and its children) from the tree
+		 *
+		 * @param   {lm.items.ContentItem} contentItem
+		 *
+		 * @returns {void}
+		 */
+        removeChild: function (contentItem, keepChild) {
+
+			/*
+			 * Get the position of the item that's to be removed within all content items this node contains
+			 */
+            var index = lm.utils.indexOf(contentItem, this.contentItems);
+
+			/*
+			 * Make sure the content item to be removed is actually a child of this item
+			 */
+            if (index === -1) {
+                throw new Error('Can\'t remove child item. Unknown content item');
+            }
+
+			/**
+			 * Call ._$destroy on the content item. This also calls ._$destroy on all its children
+			 */
+            if (keepChild !== true) {
+                this.contentItems[index]._$destroy();
+            }
+
+			/**
+			 * Remove the content item from this nodes array of children
+			 */
+            this.contentItems.splice(index, 1);
+
+			/**
+			 * Remove the item from the configuration
+			 */
+            this.config.content.splice(index, 1);
+
+			/**
+			 * If this node still contains other content items, adjust their size
+			 */
+            if (this.contentItems.length > 0) {
+                this.callDownwards('setSize');
+
+				/**
+				 * If this was the last content item, remove this node as well
+				 */
+            } else if (!(this instanceof lm.items.Root) && this.config.isClosable === true) {
+                this.parent.removeChild(this);
+            }
+        },
+
+		/**
+		 * Sets up the tree structure for the newly added child
+		 * The responsibility for the actual DOM manipulations lies
+		 * with the concrete item
+		 *
+		 * @param {lm.items.AbstractContentItem} contentItem
+		 * @param {[Int]} index If omitted item will be appended
+		 */
+        addChild: function (contentItem, index) {
+            if (index === undefined) {
+                index = this.contentItems.length;
+            }
+
+            this.contentItems.splice(index, 0, contentItem);
+
+            if (this.config.content === undefined) {
+                this.config.content = [];
+            }
+
+            this.config.content.splice(index, 0, contentItem.config);
+            contentItem.parent = this;
+
+            if (contentItem.parent.isInitialised === true && contentItem.isInitialised === false) {
+                contentItem._$init();
+            }
+        },
+
+		/**
+		 * Replaces oldChild with newChild. This used to use jQuery.replaceWith... which for
+		 * some reason removes all event listeners, so isn't really an option.
+		 *
+		 * @param   {lm.item.AbstractContentItem} oldChild
+		 * @param   {lm.item.AbstractContentItem} newChild
+		 *
+		 * @returns {void}
+		 */
+        replaceChild: function (oldChild, newChild, _$destroyOldChild) {
+
+            newChild = this.layoutManager._$normalizeContentItem(newChild);
+
+            var index = lm.utils.indexOf(oldChild, this.contentItems),
+                parentNode = oldChild.element[0].parentNode;
+
+            if (index === -1) {
+                throw new Error('Can\'t replace child. oldChild is not child of this');
+            }
+
+            parentNode.replaceChild(newChild.element[0], oldChild.element[0]);
+
+			/*
+			 * Optionally destroy the old content item
+			 */
+            if (_$destroyOldChild === true) {
+                oldChild.parent = null;
+                oldChild._$destroy();
+            }
+
+			/*
+			 * Wire the new contentItem into the tree
+			 */
+            this.contentItems[index] = newChild;
+            newChild.parent = this;
+
+			/*
+			 * Update tab reference
+			 */
+            if (this.isStack) {
+                this.header.tabs[index].contentItem = newChild;
+            }
+
+            //TODO This doesn't update the config... refactor to leave item nodes untouched after creation
+            if (newChild.parent.isInitialised === true && newChild.isInitialised === false) {
+                newChild._$init();
+            }
+
+            this.callDownwards('setSize');
+        },
+
+		/**
+		 * Convenience method.
+		 * Shorthand for this.parent.removeChild( this )
+		 *
+		 * @returns {void}
+		 */
+        remove: function () {
+            this.parent.removeChild(this);
+        },
+
+		/**
+		 * Removes the component from the layout and creates a new
+		 * browser window with the component and its children inside
+		 *
+		 * @returns {lm.controls.BrowserPopout}
+		 */
+        popout: function () {
+            var browserPopout = this.layoutManager.createPopout(this);
+            this.emitBubblingEvent('stateChanged');
+            return browserPopout;
+        },
+
+		/**
+		 * Maximises the Item or minimises it if it is already maximised
+		 *
+		 * @returns {void}
+		 */
+        toggleMaximise: function (e) {
+            e && e.preventDefault();
+            if (this.isMaximised === true) {
+                this.layoutManager._$minimiseItem(this);
+            } else {
+                this.layoutManager._$maximiseItem(this);
+            }
+
+            this.isMaximised = !this.isMaximised;
+            this.emitBubblingEvent('stateChanged');
+        },
+
+		/**
+		 * Selects the item if it is not already selected
+		 *
+		 * @returns {void}
+		 */
+        select: function () {
+            if (this.layoutManager.selectedItem !== this) {
+                this.layoutManager.selectItem(this, true);
+                this.element.addClass('lm_selected');
+            }
+        },
+
+		/**
+		 * De-selects the item if it is selected
+		 *
+		 * @returns {void}
+		 */
+        deselect: function () {
+            if (this.layoutManager.selectedItem === this) {
+                this.layoutManager.selectedItem = null;
+                this.element.removeClass('lm_selected');
+            }
+        },
+
+		/**
+		 * Set this component's title
+		 *
+		 * @public
+		 * @param {String} title
+		 *
+		 * @returns {void}
+		 */
+        setTitle: function (title) {
+            this.config.title = title;
+            this.emit('titleChanged', title);
+            this.emit('stateChanged');
+        },
+
+		/**
+		 * Checks whether a provided id is present
+		 *
+		 * @public
+		 * @param   {String}  id
+		 *
+		 * @returns {Boolean} isPresent
+		 */
+        hasId: function (id) {
+            if (!this.config.id) {
+                return false;
+            } else if (typeof this.config.id === 'string') {
+                return this.config.id === id;
+            } else if (this.config.id instanceof Array) {
+                return lm.utils.indexOf(id, this.config.id) !== -1;
+            }
+        },
+
+		/**
+		 * Adds an id. Adds it as a string if the component doesn't
+		 * have an id yet or creates/uses an array
+		 *
+		 * @public
+		 * @param {String} id
+		 *
+		 * @returns {void}
+		 */
+        addId: function (id) {
+            if (this.hasId(id)) {
+                return;
+            }
+
+            if (!this.config.id) {
+                this.config.id = id;
+            } else if (typeof this.config.id === 'string') {
+                this.config.id = [this.config.id, id];
+            } else if (this.config.id instanceof Array) {
+                this.config.id.push(id);
+            }
+        },
+
+		/**
+		 * Removes an existing id. Throws an error
+		 * if the id is not present
+		 *
+		 * @public
+		 * @param   {String} id
+		 *
+		 * @returns {void}
+		 */
+        removeId: function (id) {
+            if (!this.hasId(id)) {
+                throw new Error('Id not found');
+            }
+
+            if (typeof this.config.id === 'string') {
+                delete this.config.id;
+            } else if (this.config.id instanceof Array) {
+                var index = lm.utils.indexOf(id, this.config.id);
+                this.config.id.splice(index, 1);
+            }
+        },
+
+		/****************************************
+		 * SELECTOR
+		 ****************************************/
+        getItemsByFilter: function (filter) {
+            var result = [],
+                next = function (contentItem) {
+                    for (var i = 0; i < contentItem.contentItems.length; i++) {
+
+                        if (filter(contentItem.contentItems[i]) === true) {
+                            result.push(contentItem.contentItems[i]);
+                        }
+
+                        next(contentItem.contentItems[i]);
+                    }
+                };
+
+            next(this);
+            return result;
+        },
+
+        getItemsById: function (id) {
+            return this.getItemsByFilter(function (item) {
+                if (item.config.id instanceof Array) {
+                    return lm.utils.indexOf(id, item.config.id) !== -1;
+                } else {
+                    return item.config.id === id;
+                }
+            });
+        },
+
+        getItemsByType: function (type) {
+            return this._$getItemsByProperty('type', type);
+        },
+
+        getComponentsByName: function (componentName) {
+            var components = this._$getItemsByProperty('componentName', componentName),
+                instances = [],
+                i;
+
+            for (i = 0; i < components.length; i++) {
+                instances.push(components[i].instance);
+            }
+
+            return instances;
+        },
+
+		/****************************************
+		 * PACKAGE PRIVATE
+		 ****************************************/
+        _$getItemsByProperty: function (key, value) {
+            return this.getItemsByFilter(function (item) {
+                return item[key] === value;
+            });
+        },
+
+        _$setParent: function (parent) {
+            this.parent = parent;
+        },
+
+        _$highlightDropZone: function (x, y, area) {
+            this.layoutManager.dropTargetIndicator.highlightArea(area);
+        },
+
+        _$onDrop: function (contentItem) {
+            this.addChild(contentItem);
+        },
+
+        _$hide: function () {
+            this._callOnActiveComponents('hide');
+            this.element.hide();
+            this.layoutManager.updateSize();
+        },
+
+        _$show: function () {
+            this._callOnActiveComponents('show');
+            this.element.show();
+            this.layoutManager.updateSize();
+        },
+
+        _callOnActiveComponents: function (methodName) {
+            var stacks = this.getItemsByType('stack'),
+                activeContentItem,
+                i;
+
+            for (i = 0; i < stacks.length; i++) {
+                activeContentItem = stacks[i].getActiveContentItem();
+
+                if (activeContentItem && activeContentItem.isComponent) {
+                    activeContentItem.container[methodName]();
+                }
+            }
+        },
+
+		/**
+		 * Destroys this item ands its children
+		 *
+		 * @returns {void}
+		 */
+        _$destroy: function () {
+            this.emitBubblingEvent('beforeItemDestroyed');
+            this.callDownwards('_$destroy', [], true, true);
+            this.element.remove();
+            this.emitBubblingEvent('itemDestroyed');
+        },
+
+		/**
+		 * Returns the area the component currently occupies in the format
+		 *
+		 * {
+		 *		x1: int
+		 *		xy: int
+		 *		y1: int
+		 *		y2: int
+		 *		contentItem: contentItem
+		 * }
+		 */
+        _$getArea: function (element) {
+            element = element || this.element;
+
+            var offset = element.offset(),
+                width = element.width(),
+                height = element.height();
+
+            return {
+                x1: offset.left,
+                y1: offset.top,
+                x2: offset.left + width,
+                y2: offset.top + height,
+                surface: width * height,
+                contentItem: this
+            };
+        },
+
+		/**
+		 * The tree of content items is created in two steps: First all content items are instantiated,
+		 * then init is called recursively from top to bottem. This is the basic init function,
+		 * it can be used, extended or overwritten by the content items
+		 *
+		 * Its behaviour depends on the content item
+		 *
+		 * @package private
+		 *
+		 * @returns {void}
+		 */
+        _$init: function () {
+            var i;
+            this.setSize();
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.childElementContainer.append(this.contentItems[i].element);
+            }
+
+            this.isInitialised = true;
+            this.emitBubblingEvent('itemCreated');
+            this.emitBubblingEvent(this.type + 'Created');
+        },
+
+		/**
+		 * Emit an event that bubbles up the item tree.
+		 *
+		 * @param   {String} name The name of the event
+		 *
+		 * @returns {void}
+		 */
+        emitBubblingEvent: function (name) {
+            var event = new lm.utils.BubblingEvent(name, this);
+            this.emit(name, event);
+        },
+
+		/**
+		 * Private method, creates all content items for this node at initialisation time
+		 * PLEASE NOTE, please see addChild for adding contentItems add runtime
+		 * @private
+		 * @param   {configuration item node} config
+		 *
+		 * @returns {void}
+		 */
+        _createContentItems: function (config) {
+            var oContentItem, i;
+
+            if (!(config.content instanceof Array)) {
+                throw new lm.errors.ConfigurationError('content must be an Array', config);
+            }
+
+            for (i = 0; i < config.content.length; i++) {
+                oContentItem = this.layoutManager.createContentItem(config.content[i], this);
+                this.contentItems.push(oContentItem);
+            }
+        },
+
+		/**
+		 * Extends an item configuration node with default settings
+		 * @private
+		 * @param   {configuration item node} config
+		 *
+		 * @returns {configuration item node} extended config
+		 */
+        _extendItemNode: function (config) {
+
+            for (var key in lm.config.itemDefaultConfig) {
+                if (config[key] === undefined) {
+                    config[key] = lm.config.itemDefaultConfig[key];
+                }
+            }
+
+            return config;
+        },
+
+		/**
+		 * Called for every event on the item tree. Decides whether the event is a bubbling
+		 * event and propagates it to its parent
+		 *
+		 * @param    {String} name the name of the event
+		 * @param   {lm.utils.BubblingEvent} event
+		 *
+		 * @returns {void}
+		 */
+        _propagateEvent: function (name, event) {
+            if (event instanceof lm.utils.BubblingEvent &&
+                event.isPropagationStopped === false &&
+                this.isInitialised === true) {
+
+				/**
+				 * In some cases (e.g. if an element is created from a DragSource) it
+				 * doesn't have a parent and is not below root. If that's the case
+				 * propagate the bubbling event from the top level of the substree directly
+				 * to the layoutManager
+				 */
+                if (this.isRoot === false && this.parent) {
+                    this.parent.emit.apply(this.parent, Array.prototype.slice.call(arguments, 0));
+                } else {
+                    this._scheduleEventPropagationToLayoutManager(name, event);
+                }
+            }
+        },
+
+		/**
+		 * All raw events bubble up to the root element. Some events that
+		 * are propagated to - and emitted by - the layoutManager however are
+		 * only string-based, batched and sanitized to make them more usable
+		 *
+		 * @param {String} name the name of the event
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _scheduleEventPropagationToLayoutManager: function (name, event) {
+            if (lm.utils.indexOf(name, this._throttledEvents) === -1) {
+                this.layoutManager.emit(name, event.origin);
+            } else {
+                if (this._pendingEventPropagations[name] !== true) {
+                    this._pendingEventPropagations[name] = true;
+                    lm.utils.animFrame(lm.utils.fnBind(this._propagateEventToLayoutManager, this, [name, event]));
+                }
+            }
+
+        },
+
+		/**
+		 * Callback for events scheduled by _scheduleEventPropagationToLayoutManager
+		 *
+		 * @param {String} name the name of the event
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _propagateEventToLayoutManager: function (name, event) {
+            this._pendingEventPropagations[name] = false;
+            this.layoutManager.emit(name, event);
+        }
+    });
+
+	/**
+	 * @param {[type]} layoutManager [description]
+	 * @param {[type]} config      [description]
+	 * @param {[type]} parent        [description]
+	 */
+    lm.items.Component = function (layoutManager, config, parent) {
+        lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+        var ComponentConstructor = layoutManager.getComponent(this.config.componentName),
+            componentConfig = $.extend(true, {}, this.config.componentState || {});
+
+        componentConfig.componentName = this.config.componentName;
+        this.componentName = this.config.componentName;
+
+        if (this.config.title === '') {
+            this.config.title = this.config.componentName;
+        }
+
+        this.isComponent = true;
+        this.container = new lm.container.ItemContainer(this.config, this, layoutManager);
+        this.instance = new ComponentConstructor(this.container, componentConfig);
+        this.element = this.container._element;
+    };
+
+    lm.utils.extend(lm.items.Component, lm.items.AbstractContentItem);
+
+    lm.utils.copy(lm.items.Component.prototype, {
+
+        close: function () {
+            this.parent.removeChild(this);
+        },
+
+        setSize: function () {
+            if (this.element.is(':visible')) {
+                // Do not update size of hidden components to prevent unwanted reflows
+                this.container._$setSize(this.element.width(), this.element.height());
+            }
+        },
+
+        _$init: function () {
+            lm.items.AbstractContentItem.prototype._$init.call(this);
+            this.container.emit('open');
+        },
+
+        _$hide: function () {
+            this.container.hide();
+            lm.items.AbstractContentItem.prototype._$hide.call(this);
+        },
+
+        _$show: function () {
+            this.container.show();
+            lm.items.AbstractContentItem.prototype._$show.call(this);
+        },
+
+        _$shown: function () {
+            this.container.shown();
+            lm.items.AbstractContentItem.prototype._$shown.call(this);
+        },
+
+        _$destroy: function () {
+            this.container.emit('destroy', this);
+            lm.items.AbstractContentItem.prototype._$destroy.call(this);
+        },
+
+		/**
+		 * Dragging onto a component directly is not an option
+		 *
+		 * @returns null
+		 */
+        _$getArea: function () {
+            return null;
+        }
+    });
+
+    lm.items.Root = function (layoutManager, config, containerElement) {
+        lm.items.AbstractContentItem.call(this, layoutManager, config, null);
+        this.isRoot = true;
+        this.type = 'root';
+        this.element = $('<div class="lm_goldenlayout lm_item lm_root"></div>');
+        this.childElementContainer = this.element;
+        this._containerElement = containerElement;
+        this._containerElement.append(this.element);
+    };
+
+    lm.utils.extend(lm.items.Root, lm.items.AbstractContentItem);
+
+    lm.utils.copy(lm.items.Root.prototype, {
+        addChild: function (contentItem) {
+            if (this.contentItems.length > 0) {
+                throw new Error('Root node can only have a single child');
+            }
+
+            contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+            this.childElementContainer.append(contentItem.element);
+            lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem);
+
+            this.callDownwards('setSize');
+            this.emitBubblingEvent('stateChanged');
+        },
+
+        setSize: function (width, height) {
+            width = (typeof width === 'undefined') ? this._containerElement.width() : width;
+            height = (typeof height === 'undefined') ? this._containerElement.height() : height;
+
+            this.element.width(width);
+            this.element.height(height);
+
+			/*
+			 * Root can be empty
+			 */
+            if (this.contentItems[0]) {
+                this.contentItems[0].element.width(width);
+                this.contentItems[0].element.height(height);
+            }
+        },
+        _$highlightDropZone: function (x, y, area) {
+            this.layoutManager.tabDropPlaceholder.remove();
+            lm.items.AbstractContentItem.prototype._$highlightDropZone.apply(this, arguments);
+        },
+
+        _$onDrop: function (contentItem, area) {
+            var stack;
+
+            if (contentItem.isComponent) {
+                stack = this.layoutManager.createContentItem({
+                    type: 'stack',
+                    header: contentItem.config.header || {}
+                }, this);
+                stack._$init();
+                stack.addChild(contentItem);
+                contentItem = stack;
+            }
+
+            if (!this.contentItems.length) {
+                this.addChild(contentItem);
+            } else {
+                var type = area.side[0] == 'x' ? 'row' : 'column';
+                var dimension = area.side[0] == 'x' ? 'width' : 'height';
+                var insertBefore = area.side[1] == '2';
+                var column = this.contentItems[0];
+                if (!column instanceof lm.items.RowOrColumn || column.type != type) {
+                    var rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+                    this.replaceChild(column, rowOrColumn);
+                    rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+                    rowOrColumn.addChild(column, insertBefore ? undefined : 0, true);
+                    column.config[dimension] = 50;
+                    contentItem.config[dimension] = 50;
+                    rowOrColumn.callDownwards('setSize');
+                } else {
+                    var sibbling = column.contentItems[insertBefore ? 0 : column.contentItems.length - 1]
+                    column.addChild(contentItem, insertBefore ? 0 : undefined, true);
+                    sibbling.config[dimension] *= 0.5;
+                    contentItem.config[dimension] = sibbling.config[dimension];
+                    column.callDownwards('setSize');
+                }
+            }
+        }
+    });
+
+
+
+    lm.items.RowOrColumn = function (isColumn, layoutManager, config, parent) {
+        lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+        this.isRow = !isColumn;
+        this.isColumn = isColumn;
+
+        this.element = $('<div class="lm_item lm_' + (isColumn ? 'column' : 'row') + '"></div>');
+        this.childElementContainer = this.element;
+        this._splitterSize = layoutManager.config.dimensions.borderWidth;
+        this._splitterGrabSize = layoutManager.config.dimensions.borderGrabWidth;
+        this._isColumn = isColumn;
+        this._dimension = isColumn ? 'height' : 'width';
+        this._splitter = [];
+        this._splitterPosition = null;
+        this._splitterMinPosition = null;
+        this._splitterMaxPosition = null;
+    };
+
+    lm.utils.extend(lm.items.RowOrColumn, lm.items.AbstractContentItem);
+
+    lm.utils.copy(lm.items.RowOrColumn.prototype, {
+
+		/**
+		 * Add a new contentItem to the Row or Column
+		 *
+		 * @param {lm.item.AbstractContentItem} contentItem
+		 * @param {[int]} index The position of the new item within the Row or Column.
+		 *                      If no index is provided the item will be added to the end
+		 * @param {[bool]} _$suspendResize If true the items won't be resized. This will leave the item in
+		 *                                 an inconsistent state and is only intended to be used if multiple
+		 *                                 children need to be added in one go and resize is called afterwards
+		 *
+		 * @returns {void}
+		 */
+        addChild: function (contentItem, index, _$suspendResize) {
+
+            var newItemSize, itemSize, i, splitterElement;
+
+            contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+
+            if (index === undefined) {
+                index = this.contentItems.length;
+            }
+
+            if (this.contentItems.length > 0) {
+                splitterElement = this._createSplitter(Math.max(0, index - 1)).element;
+
+                if (index > 0) {
+                    this.contentItems[index - 1].element.after(splitterElement);
+                    splitterElement.after(contentItem.element);
+                } else {
+                    this.contentItems[0].element.before(splitterElement);
+                    splitterElement.before(contentItem.element);
+                }
+            } else {
+                this.childElementContainer.append(contentItem.element);
+            }
+
+            lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+
+            let fixedItemSize = 0;
+            let variableItemCount = 0;
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this.contentItems[i].config.fixed)
+                    fixedItemSize += this.contentItems[i].config[this._dimension];
+                else variableItemCount++;
+            }
+
+            newItemSize = (1 / variableItemCount) * (100 - fixedItemSize);
+
+            if (_$suspendResize === true) {
+                this.emitBubblingEvent('stateChanged');
+                return;
+            }
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this.contentItems[i].config.fixed)
+                    ;
+                else if (this.contentItems[i] === contentItem) {
+                    contentItem.config[this._dimension] = newItemSize;
+                } else {
+                    itemSize = this.contentItems[i].config[this._dimension] *= (100 - newItemSize - fixedItemSize) / (100 - fixedItemSize);
+                    this.contentItems[i].config[this._dimension] = itemSize;
+                }
+            }
+
+            this.callDownwards('setSize');
+            this.emitBubblingEvent('stateChanged');
+
+        },
+
+		/**
+		 * Removes a child of this element
+		 *
+		 * @param   {lm.items.AbstractContentItem} contentItem
+		 * @param   {boolean} keepChild   If true the child will be removed, but not destroyed
+		 *
+		 * @returns {void}
+		 */
+        removeChild: function (contentItem, keepChild) {
+            var removedItemSize = contentItem.config[this._dimension],
+                index = lm.utils.indexOf(contentItem, this.contentItems),
+                splitterIndex = Math.max(index - 1, 0),
+                i,
+                childItem;
+
+            if (index === -1) {
+                throw new Error('Can\'t remove child. ContentItem is not child of this Row or Column');
+            }
+
+			/**
+			 * Remove the splitter before the item or after if the item happens
+			 * to be the first in the row/column
+			 */
+            if (this._splitter[splitterIndex]) {
+                this._splitter[splitterIndex]._$destroy();
+                this._splitter.splice(splitterIndex, 1);
+            }
+
+            let fixedItemSize = 0;
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this.contentItems[i].config.fixed)
+                    fixedItemSize += this.contentItems[i].config[this._dimension];
+            }
+			/**
+			 * Allocate the space that the removed item occupied to the remaining items
+			 */
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this.contentItems[i].config.fixed)
+                    ;
+                else if (this.contentItems[i] !== contentItem) {
+                    this.contentItems[i].config[this._dimension] *= (100 - fixedItemSize) / (100 - removedItemSize - fixedItemSize);
+                }
+            }
+
+            lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+
+            if (this.contentItems.length === 1 && this.config.isClosable === true) {
+                childItem = this.contentItems[0];
+                this.contentItems = [];
+                this.parent.replaceChild(this, childItem, true);
+            } else {
+                this.callDownwards('setSize');
+                this.emitBubblingEvent('stateChanged');
+            }
+        },
+
+		/**
+		 * Replaces a child of this Row or Column with another contentItem
+		 *
+		 * @param   {lm.items.AbstractContentItem} oldChild
+		 * @param   {lm.items.AbstractContentItem} newChild
+		 *
+		 * @returns {void}
+		 */
+        replaceChild: function (oldChild, newChild) {
+            var size = oldChild.config[this._dimension];
+            lm.items.AbstractContentItem.prototype.replaceChild.call(this, oldChild, newChild);
+            newChild.config[this._dimension] = size;
+            this.callDownwards('setSize');
+            this.emitBubblingEvent('stateChanged');
+        },
+
+		/**
+		 * Called whenever the dimensions of this item or one of its parents change
+		 *
+		 * @returns {void}
+		 */
+        setSize: function () {
+            if (this.contentItems.length > 0) {
+                this._calculateRelativeSizes();
+                this._setAbsoluteSizes();
+            }
+            this.emitBubblingEvent('stateChanged');
+            this.emit('resize');
+        },
+
+		/**
+		 * Invoked recursively by the layout manager. AbstractContentItem.init appends
+		 * the contentItem's DOM elements to the container, RowOrColumn init adds splitters
+		 * in between them
+		 *
+		 * @package private
+		 * @override AbstractContentItem._$init
+		 * @returns {void}
+		 */
+        _$init: function () {
+            if (this.isInitialised === true) return;
+
+            var i;
+
+            lm.items.AbstractContentItem.prototype._$init.call(this);
+
+            for (i = 0; i < this.contentItems.length - 1; i++) {
+                this.contentItems[i].element.after(this._createSplitter(i).element);
+            }
+        },
+
+		/**
+		 * Turns the relative sizes calculated by _calculateRelativeSizes into
+		 * absolute pixel values and applies them to the children's DOM elements
+		 *
+		 * Assigns additional pixels to counteract Math.floor
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _setAbsoluteSizes: function () {
+            var i,
+                sizeData = this._calculateAbsoluteSizes();
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (sizeData.additionalPixel - i > 0) {
+                    sizeData.itemSizes[i]++;
+                }
+
+                if (this._isColumn) {
+                    this.contentItems[i].element.width(sizeData.totalWidth);
+                    this.contentItems[i].element.height(sizeData.itemSizes[i]);
+                } else {
+                    this.contentItems[i].element.width(sizeData.itemSizes[i]);
+                    this.contentItems[i].element.height(sizeData.totalHeight);
+                }
+            }
+        },
+
+		/**
+		 * Calculates the absolute sizes of all of the children of this Item.
+		 * @returns {object} - Set with absolute sizes and additional pixels.
+		 */
+        _calculateAbsoluteSizes: function () {
+            var i,
+                totalSplitterSize = (this.contentItems.length - 1) * this._splitterSize,
+                totalWidth = this.element.width(),
+                totalHeight = this.element.height(),
+                totalAssigned = 0,
+                additionalPixel,
+                itemSize,
+                itemSizes = [];
+
+            if (this._isColumn) {
+                totalHeight -= totalSplitterSize;
+            } else {
+                totalWidth -= totalSplitterSize;
+            }
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this._isColumn) {
+                    itemSize = Math.floor(totalHeight * (this.contentItems[i].config.height / 100));
+                } else {
+                    itemSize = Math.floor(totalWidth * (this.contentItems[i].config.width / 100));
+                }
+
+                totalAssigned += itemSize;
+                itemSizes.push(itemSize);
+            }
+
+            additionalPixel = Math.floor((this._isColumn ? totalHeight : totalWidth) - totalAssigned);
+
+            return {
+                itemSizes: itemSizes,
+                additionalPixel: additionalPixel,
+                totalWidth: totalWidth,
+                totalHeight: totalHeight
+            };
+        },
+
+		/**
+		 * Calculates the relative sizes of all children of this Item. The logic
+		 * is as follows:
+		 *
+		 * - Add up the total size of all items that have a configured size
+		 *
+		 * - If the total == 100 (check for floating point errors)
+		 *        Excellent, job done
+		 *
+		 * - If the total is > 100,
+		 *        set the size of items without set dimensions to 1/3 and add this to the total
+		 *        set the size off all items so that the total is hundred relative to their original size
+		 *
+		 * - If the total is < 100
+		 *        If there are items without set dimensions, distribute the remainder to 100 evenly between them
+		 *        If there are no items without set dimensions, increase all items sizes relative to
+		 *        their original size so that they add up to 100
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _calculateRelativeSizes: function () {
+
+            var i,
+                total = 0,
+                itemsWithoutSetDimension = [],
+                dimension = this._isColumn ? 'height' : 'width';
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                if (this.contentItems[i].config[dimension] !== undefined) {
+                    total += this.contentItems[i].config[dimension];
+                } else {
+                    itemsWithoutSetDimension.push(this.contentItems[i]);
+                }
+            }
+
+			/**
+			 * Everything adds up to hundred, all good :-)
+			 */
+            if (Math.round(total) === 100) {
+                this._respectMinItemWidth();
+                return;
+            }
+
+			/**
+			 * Allocate the remaining size to the items without a set dimension
+			 */
+            if (Math.round(total) < 100 && itemsWithoutSetDimension.length > 0) {
+                for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+                    itemsWithoutSetDimension[i].config[dimension] = (100 - total) / itemsWithoutSetDimension.length;
+                }
+                this._respectMinItemWidth();
+                return;
+            }
+
+			/**
+			 * If the total is > 100, but there are also items without a set dimension left, assing 50
+			 * as their dimension and add it to the total
+			 *
+			 * This will be reset in the next step
+			 */
+            if (Math.round(total) > 100) {
+                for (i = 0; i < itemsWithoutSetDimension.length; i++) {
+                    itemsWithoutSetDimension[i].config[dimension] = 50;
+                    total += 50;
+                }
+            }
+
+			/**
+			 * Set every items size relative to 100 relative to its size to total
+			 */
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.contentItems[i].config[dimension] = (this.contentItems[i].config[dimension] / total) * 100;
+            }
+
+            this._respectMinItemWidth();
+        },
+
+		/**
+		 * Adjusts the column widths to respect the dimensions minItemWidth if set.
+		 * @returns {}
+		 */
+        _respectMinItemWidth: function () {
+            var minItemWidth = this.layoutManager.config.dimensions ? (this.layoutManager.config.dimensions.minItemWidth || 0) : 0,
+                sizeData = null,
+                entriesOverMin = [],
+                totalOverMin = 0,
+                totalUnderMin = 0,
+                remainingWidth = 0,
+                itemSize = 0,
+                contentItem = null,
+                reducePercent,
+                reducedWidth,
+                allEntries = [],
+                entry;
+
+            if (this._isColumn || !minItemWidth || this.contentItems.length <= 1) {
+                return;
+            }
+
+            sizeData = this._calculateAbsoluteSizes();
+
+			/**
+			 * Figure out how much we are under the min item size total and how much room we have to use.
+			 */
+            for (var i = 0; i < this.contentItems.length; i++) {
+
+                contentItem = this.contentItems[i];
+                itemSize = sizeData.itemSizes[i];
+
+                if (itemSize < minItemWidth) {
+                    totalUnderMin += minItemWidth - itemSize;
+                    entry = { width: minItemWidth };
+
+                }
+                else {
+                    totalOverMin += itemSize - minItemWidth;
+                    entry = { width: itemSize };
+                    entriesOverMin.push(entry);
+                }
+
+                allEntries.push(entry);
+            }
+
+			/**
+			 * If there is nothing under min, or there is not enough over to make up the difference, do nothing.
+			 */
+            if (totalUnderMin === 0 || totalUnderMin > totalOverMin) {
+                return;
+            }
+
+			/**
+			 * Evenly reduce all columns that are over the min item width to make up the difference.
+			 */
+            reducePercent = totalUnderMin / totalOverMin;
+            remainingWidth = totalUnderMin;
+            for (i = 0; i < entriesOverMin.length; i++) {
+                entry = entriesOverMin[i];
+                reducedWidth = Math.round((entry.width - minItemWidth) * reducePercent);
+                remainingWidth -= reducedWidth;
+                entry.width -= reducedWidth;
+            }
+
+			/**
+			 * Take anything remaining from the last item.
+			 */
+            if (remainingWidth !== 0) {
+                allEntries[allEntries.length - 1].width -= remainingWidth;
+            }
+
+			/**
+			 * Set every items size relative to 100 relative to its size to total
+			 */
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.contentItems[i].config.width = (allEntries[i].width / sizeData.totalWidth) * 100;
+            }
+        },
+
+		/**
+		 * Instantiates a new lm.controls.Splitter, binds events to it and adds
+		 * it to the array of splitters at the position specified as the index argument
+		 *
+		 * What it doesn't do though is append the splitter to the DOM
+		 *
+		 * @param   {Int} index The position of the splitter
+		 *
+		 * @returns {lm.controls.Splitter}
+		 */
+        _createSplitter: function (index) {
+            var splitter;
+            splitter = new lm.controls.Splitter(this._isColumn, this._splitterSize, this._splitterGrabSize);
+            splitter.on('drag', lm.utils.fnBind(this._onSplitterDrag, this, [splitter]), this);
+            splitter.on('dragStop', lm.utils.fnBind(this._onSplitterDragStop, this, [splitter]), this);
+            splitter.on('dragStart', lm.utils.fnBind(this._onSplitterDragStart, this, [splitter]), this);
+            this._splitter.splice(index, 0, splitter);
+            return splitter;
+        },
+
+		/**
+		 * Locates the instance of lm.controls.Splitter in the array of
+		 * registered splitters and returns a map containing the contentItem
+		 * before and after the splitters, both of which are affected if the
+		 * splitter is moved
+		 *
+		 * @param   {lm.controls.Splitter} splitter
+		 *
+		 * @returns {Object} A map of contentItems that the splitter affects
+		 */
+        _getItemsForSplitter: function (splitter) {
+            var index = lm.utils.indexOf(splitter, this._splitter);
+
+            return {
+                before: this.contentItems[index],
+                after: this.contentItems[index + 1]
+            };
+        },
+
+		/**
+		 * Gets the minimum dimensions for the given item configuration array
+		 * @param item
+		 * @private
+		 */
+        _getMinimumDimensions: function (arr) {
+            var minWidth = 0, minHeight = 0;
+
+            for (var i = 0; i < arr.length; ++i) {
+                minWidth = Math.max(arr[i].minWidth || 0, minWidth);
+                minHeight = Math.max(arr[i].minHeight || 0, minHeight);
+            }
+
+            return { horizontal: minWidth, vertical: minHeight };
+        },
+
+		/**
+		 * Invoked when a splitter's dragListener fires dragStart. Calculates the splitters
+		 * movement area once (so that it doesn't need calculating on every mousemove event)
+		 *
+		 * @param   {lm.controls.Splitter} splitter
+		 *
+		 * @returns {void}
+		 */
+        _onSplitterDragStart: function (splitter) {
+            var items = this._getItemsForSplitter(splitter),
+                minSize = this.layoutManager.config.dimensions[this._isColumn ? 'minItemHeight' : 'minItemWidth'];
+
+            var beforeMinDim = this._getMinimumDimensions(items.before.config.content);
+            var beforeMinSize = this._isColumn ? beforeMinDim.vertical : beforeMinDim.horizontal;
+
+            var afterMinDim = this._getMinimumDimensions(items.after.config.content);
+            var afterMinSize = this._isColumn ? afterMinDim.vertical : afterMinDim.horizontal;
+
+            this._splitterPosition = 0;
+            this._splitterMinPosition = -1 * (items.before.element[this._dimension]() - (beforeMinSize || minSize));
+            this._splitterMaxPosition = items.after.element[this._dimension]() - (afterMinSize || minSize);
+        },
+
+		/**
+		 * Invoked when a splitter's DragListener fires drag. Updates the splitters DOM position,
+		 * but not the sizes of the elements the splitter controls in order to minimize resize events
+		 *
+		 * @param   {lm.controls.Splitter} splitter
+		 * @param   {Int} offsetX  Relative pixel values to the splitters original position. Can be negative
+		 * @param   {Int} offsetY  Relative pixel values to the splitters original position. Can be negative
+		 *
+		 * @returns {void}
+		 */
+        _onSplitterDrag: function (splitter, offsetX, offsetY) {
+            var offset = this._isColumn ? offsetY : offsetX;
+
+            if (offset > this._splitterMinPosition && offset < this._splitterMaxPosition) {
+                this._splitterPosition = offset;
+                splitter.element.css(this._isColumn ? 'top' : 'left', offset);
+            }
+        },
+
+		/**
+		 * Invoked when a splitter's DragListener fires dragStop. Resets the splitters DOM position,
+		 * and applies the new sizes to the elements before and after the splitter and their children
+		 * on the next animation frame
+		 *
+		 * @param   {lm.controls.Splitter} splitter
+		 *
+		 * @returns {void}
+		 */
+        _onSplitterDragStop: function (splitter) {
+
+            var items = this._getItemsForSplitter(splitter),
+                sizeBefore = items.before.element[this._dimension](),
+                sizeAfter = items.after.element[this._dimension](),
+                splitterPositionInRange = (this._splitterPosition + sizeBefore) / (sizeBefore + sizeAfter),
+                totalRelativeSize = items.before.config[this._dimension] + items.after.config[this._dimension];
+
+            items.before.config[this._dimension] = splitterPositionInRange * totalRelativeSize;
+            items.after.config[this._dimension] = (1 - splitterPositionInRange) * totalRelativeSize;
+
+            splitter.element.css({
+                'top': 0,
+                'left': 0
+            });
+
+            lm.utils.animFrame(lm.utils.fnBind(this.callDownwards, this, ['setSize']));
+        }
+    });
+
+    lm.items.Stack = function (layoutManager, config, parent) {
+        lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
+
+        this.element = $('<div class="lm_item lm_stack"></div>');
+        this._activeContentItem = null;
+        var cfg = layoutManager.config;
+        this._header = { // defaults' reconstruction from old configuration style
+            show: cfg.settings.hasHeaders === true && config.hasHeaders !== false,
+            popout: cfg.settings.showPopoutIcon && cfg.labels.popout,
+            maximise: cfg.settings.showMaximiseIcon && cfg.labels.maximise,
+            close: cfg.settings.showCloseIcon && cfg.labels.close,
+            minimise: cfg.labels.minimise,
+        };
+        if (cfg.header) // load simplified version of header configuration (https://github.com/deepstreamIO/golden-layout/pull/245)
+            lm.utils.copy(this._header, cfg.header);
+        if (config.header) // load from stack
+            lm.utils.copy(this._header, config.header);
+        if (config.content && config.content[0] && config.content[0].header) // load from component if stack omitted
+            lm.utils.copy(this._header, config.content[0].header);
+
+        this._dropZones = {};
+        this._dropSegment = null;
+        this._contentAreaDimensions = null;
+        this._dropIndex = null;
+
+        this.isStack = true;
+
+        this.childElementContainer = $('<div class="lm_items"></div>');
+        this.header = new lm.controls.Header(layoutManager, this);
+
+        this.element.append(this.header.element);
+        this.element.append(this.childElementContainer);
+        this._setupHeaderPosition();
+        this._$validateClosability();
+    };
+
+    lm.utils.extend(lm.items.Stack, lm.items.AbstractContentItem);
+
+    lm.utils.copy(lm.items.Stack.prototype, {
+
+        setSize: function () {
+            var i,
+                headerSize = this._header.show ? this.layoutManager.config.dimensions.headerHeight : 0,
+                contentWidth = this.element.width() - (this._sided ? headerSize : 0),
+                contentHeight = this.element.height() - (!this._sided ? headerSize : 0);
+
+            this.childElementContainer.width(contentWidth);
+            this.childElementContainer.height(contentHeight);
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.contentItems[i].element.width(contentWidth).height(contentHeight);
+            }
+            this.emit('resize');
+            this.emitBubblingEvent('stateChanged');
+        },
+
+        _$init: function () {
+            var i, initialItem;
+
+            if (this.isInitialised === true) return;
+
+            lm.items.AbstractContentItem.prototype._$init.call(this);
+
+            for (i = 0; i < this.contentItems.length; i++) {
+                this.header.createTab(this.contentItems[i]);
+                this.contentItems[i]._$hide();
+            }
+
+            if (this.contentItems.length > 0) {
+                initialItem = this.contentItems[this.config.activeItemIndex || 0];
+
+                if (!initialItem) {
+                    throw new Error('Configured activeItemIndex out of bounds');
+                }
+
+                this.setActiveContentItem(initialItem);
+            }
+        },
+
+        setActiveContentItem: function (contentItem) {
+            if (lm.utils.indexOf(contentItem, this.contentItems) === -1) {
+                throw new Error('contentItem is not a child of this stack');
+            }
+
+            if (this._activeContentItem !== null) {
+                this._activeContentItem._$hide();
+            }
+
+            this._activeContentItem = contentItem;
+            this.header.setActiveContentItem(contentItem);
+            contentItem._$show();
+            this.emit('activeContentItemChanged', contentItem);
+            this.layoutManager.emit('activeContentItemChanged', contentItem);
+            this.emitBubblingEvent('stateChanged');
+        },
+
+        getActiveContentItem: function () {
+            return this.header.activeContentItem;
+        },
+
+        addChild: function (contentItem, index) {
+            contentItem = this.layoutManager._$normalizeContentItem(contentItem, this);
+            lm.items.AbstractContentItem.prototype.addChild.call(this, contentItem, index);
+            this.childElementContainer.append(contentItem.element);
+            this.header.createTab(contentItem, index);
+            this.setActiveContentItem(contentItem);
+            this.callDownwards('setSize');
+            this._$validateClosability();
+            this.emitBubblingEvent('stateChanged');
+        },
+
+        removeChild: function (contentItem, keepChild) {
+            var index = lm.utils.indexOf(contentItem, this.contentItems);
+            lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
+            this.header.removeTab(contentItem);
+            if (this.header.activeContentItem === contentItem) {
+                if (this.contentItems.length > 0) {
+                    this.setActiveContentItem(this.contentItems[Math.max(index - 1, 0)]);
+                } else {
+                    this._activeContentItem = null;
+                }
+            }
+
+            this._$validateClosability();
+            this.emitBubblingEvent('stateChanged');
+        },
+
+		/**
+		 * Validates that the stack is still closable or not. If a stack is able
+		 * to close, but has a non closable component added to it, the stack is no
+		 * longer closable until all components are closable.
+		 *
+		 * @returns {void}
+		 */
+        _$validateClosability: function () {
+            var contentItem,
+                isClosable,
+                len,
+                i;
+
+            isClosable = this.header._isClosable();
+
+            for (i = 0, len = this.contentItems.length; i < len; i++) {
+                if (!isClosable) {
+                    break;
+                }
+
+                isClosable = this.contentItems[i].config.isClosable;
+            }
+
+            this.header._$setClosable(isClosable);
+        },
+
+        _$destroy: function () {
+            lm.items.AbstractContentItem.prototype._$destroy.call(this);
+            this.header._$destroy();
+        },
+
+
+		/**
+		 * Ok, this one is going to be the tricky one: The user has dropped {contentItem} onto this stack.
+		 *
+		 * It was dropped on either the stacks header or the top, right, bottom or left bit of the content area
+		 * (which one of those is stored in this._dropSegment). Now, if the user has dropped on the header the case
+		 * is relatively clear: We add the item to the existing stack... job done (might be good to have
+		 * tab reordering at some point, but lets not sweat it right now)
+		 *
+		 * If the item was dropped on the content part things are a bit more complicated. If it was dropped on either the
+		 * top or bottom region we need to create a new column and place the items accordingly.
+		 * Unless, of course if the stack is already within a column... in which case we want
+		 * to add the newly created item to the existing column...
+		 * either prepend or append it, depending on wether its top or bottom.
+		 *
+		 * Same thing for rows and left / right drop segments... so in total there are 9 things that can potentially happen
+		 * (left, top, right, bottom) * is child of the right parent (row, column) + header drop
+		 *
+		 * @param    {lm.item} contentItem
+		 *
+		 * @returns {void}
+		 */
+        _$onDrop: function (contentItem) {
+
+			/*
+			 * The item was dropped on the header area. Just add it as a child of this stack and
+			 * get the hell out of this logic
+			 */
+            if (this._dropSegment === 'header') {
+                this._resetHeaderDropZone();
+                this.addChild(contentItem, this._dropIndex);
+                return;
+            }
+
+			/*
+			 * The stack is empty. Let's just add the element.
+			 */
+            if (this._dropSegment === 'body') {
+                this.addChild(contentItem);
+                return;
+            }
+
+			/*
+			 * The item was dropped on the top-, left-, bottom- or right- part of the content. Let's
+			 * aggregate some conditions to make the if statements later on more readable
+			 */
+            var isVertical = this._dropSegment === 'top' || this._dropSegment === 'bottom',
+                isHorizontal = this._dropSegment === 'left' || this._dropSegment === 'right',
+                insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left',
+                hasCorrectParent = (isVertical && this.parent.isColumn) || (isHorizontal && this.parent.isRow),
+                type = isVertical ? 'column' : 'row',
+                dimension = isVertical ? 'height' : 'width',
+                index,
+                stack,
+                rowOrColumn;
+
+			/*
+			 * The content item can be either a component or a stack. If it is a component, wrap it into a stack
+			 */
+            if (contentItem.isComponent) {
+                stack = this.layoutManager.createContentItem({
+                    type: 'stack',
+                    header: contentItem.config.header || {}
+                }, this);
+                stack._$init();
+                stack.addChild(contentItem);
+                contentItem = stack;
+            }
+
+			/*
+			 * If the item is dropped on top or bottom of a column or left and right of a row, it's already
+			 * layd out in the correct way. Just add it as a child
+			 */
+            if (hasCorrectParent) {
+                index = lm.utils.indexOf(this, this.parent.contentItems);
+                this.parent.addChild(contentItem, insertBefore ? index : index + 1, true);
+                this.config[dimension] *= 0.5;
+                contentItem.config[dimension] = this.config[dimension];
+                this.parent.callDownwards('setSize');
+				/*
+				 * This handles items that are dropped on top or bottom of a row or left / right of a column. We need
+				 * to create the appropriate contentItem for them to live in
+				 */
+            } else {
+                type = isVertical ? 'column' : 'row';
+                rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+                this.parent.replaceChild(this, rowOrColumn);
+
+                rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+                rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
+
+                this.config[dimension] = 50;
+                contentItem.config[dimension] = 50;
+                rowOrColumn.callDownwards('setSize');
+            }
+        },
+
+		/**
+		 * If the user hovers above the header part of the stack, indicate drop positions for tabs.
+		 * otherwise indicate which segment of the body the dragged item would be dropped on
+		 *
+		 * @param    {Int} x Absolute Screen X
+		 * @param    {Int} y Absolute Screen Y
+		 *
+		 * @returns {void}
+		 */
+        _$highlightDropZone: function (x, y) {
+            var segment, area;
+
+            for (segment in this._contentAreaDimensions) {
+                area = this._contentAreaDimensions[segment].hoverArea;
+
+                if (area.x1 < x && area.x2 > x && area.y1 < y && area.y2 > y) {
+
+                    if (segment === 'header') {
+                        this._dropSegment = 'header';
+                        this._highlightHeaderDropZone(this._sided ? y : x);
+                    } else {
+                        this._resetHeaderDropZone();
+                        this._highlightBodyDropZone(segment);
+                    }
+
+                    return;
+                }
+            }
+        },
+
+        _$getArea: function () {
+            if (this.element.is(':visible') === false) {
+                return null;
+            }
+
+            var getArea = lm.items.AbstractContentItem.prototype._$getArea,
+                headerArea = getArea.call(this, this.header.element),
+                contentArea = getArea.call(this, this.childElementContainer),
+                contentWidth = contentArea.x2 - contentArea.x1,
+                contentHeight = contentArea.y2 - contentArea.y1;
+
+            this._contentAreaDimensions = {
+                header: {
+                    hoverArea: {
+                        x1: headerArea.x1,
+                        y1: headerArea.y1,
+                        x2: headerArea.x2,
+                        y2: headerArea.y2
+                    },
+                    highlightArea: {
+                        x1: headerArea.x1,
+                        y1: headerArea.y1,
+                        x2: headerArea.x2,
+                        y2: headerArea.y2
+                    }
+                }
+            };
+
+			/**
+			 * If this Stack is a parent to rows, columns or other stacks only its
+			 * header is a valid dropzone.
+			 */
+            if (this._activeContentItem && this._activeContentItem.isComponent === false) {
+                return headerArea;
+            }
+
+			/**
+			 * Highlight the entire body if the stack is empty
+			 */
+            if (this.contentItems.length === 0) {
+
+                this._contentAreaDimensions.body = {
+                    hoverArea: {
+                        x1: contentArea.x1,
+                        y1: contentArea.y1,
+                        x2: contentArea.x2,
+                        y2: contentArea.y2
+                    },
+                    highlightArea: {
+                        x1: contentArea.x1,
+                        y1: contentArea.y1,
+                        x2: contentArea.x2,
+                        y2: contentArea.y2
+                    }
+                };
+
+                return getArea.call(this, this.element);
+            }
+
+            this._contentAreaDimensions.left = {
+                hoverArea: {
+                    x1: contentArea.x1,
+                    y1: contentArea.y1,
+                    x2: contentArea.x1 + contentWidth * 0.25,
+                    y2: contentArea.y2
+                },
+                highlightArea: {
+                    x1: contentArea.x1,
+                    y1: contentArea.y1,
+                    x2: contentArea.x1 + contentWidth * 0.5,
+                    y2: contentArea.y2
+                }
+            };
+
+            this._contentAreaDimensions.top = {
+                hoverArea: {
+                    x1: contentArea.x1 + contentWidth * 0.25,
+                    y1: contentArea.y1,
+                    x2: contentArea.x1 + contentWidth * 0.75,
+                    y2: contentArea.y1 + contentHeight * 0.5
+                },
+                highlightArea: {
+                    x1: contentArea.x1,
+                    y1: contentArea.y1,
+                    x2: contentArea.x2,
+                    y2: contentArea.y1 + contentHeight * 0.5
+                }
+            };
+
+            this._contentAreaDimensions.right = {
+                hoverArea: {
+                    x1: contentArea.x1 + contentWidth * 0.75,
+                    y1: contentArea.y1,
+                    x2: contentArea.x2,
+                    y2: contentArea.y2
+                },
+                highlightArea: {
+                    x1: contentArea.x1 + contentWidth * 0.5,
+                    y1: contentArea.y1,
+                    x2: contentArea.x2,
+                    y2: contentArea.y2
+                }
+            };
+
+            this._contentAreaDimensions.bottom = {
+                hoverArea: {
+                    x1: contentArea.x1 + contentWidth * 0.25,
+                    y1: contentArea.y1 + contentHeight * 0.5,
+                    x2: contentArea.x1 + contentWidth * 0.75,
+                    y2: contentArea.y2
+                },
+                highlightArea: {
+                    x1: contentArea.x1,
+                    y1: contentArea.y1 + contentHeight * 0.5,
+                    x2: contentArea.x2,
+                    y2: contentArea.y2
+                }
+            };
+
+            return getArea.call(this, this.element);
+        },
+
+        _highlightHeaderDropZone: function (x) {
+            var i,
+                tabElement,
+                tabsLength = this.header.tabs.length,
+                isAboveTab = false,
+                tabTop,
+                tabLeft,
+                offset,
+                placeHolderLeft,
+                headerOffset,
+                tabWidth,
+                halfX;
+
+            // Empty stack
+            if (tabsLength === 0) {
+                headerOffset = this.header.element.offset();
+
+                this.layoutManager.dropTargetIndicator.highlightArea({
+                    x1: headerOffset.left,
+                    x2: headerOffset.left + 100,
+                    y1: headerOffset.top + this.header.element.height() - 20,
+                    y2: headerOffset.top + this.header.element.height()
+                });
+
+                return;
+            }
+
+            for (i = 0; i < tabsLength; i++) {
+                tabElement = this.header.tabs[i].element;
+                offset = tabElement.offset();
+                if (this._sided) {
+                    tabLeft = offset.top;
+                    tabTop = offset.left;
+                    tabWidth = tabElement.height();
+                } else {
+                    tabLeft = offset.left;
+                    tabTop = offset.top;
+                    tabWidth = tabElement.width();
+                }
+
+                if (x > tabLeft && x < tabLeft + tabWidth) {
+                    isAboveTab = true;
+                    break;
+                }
+            }
+
+            if (isAboveTab === false && x < tabLeft) {
+                return;
+            }
+
+            halfX = tabLeft + tabWidth / 2;
+
+            if (x < halfX) {
+                this._dropIndex = i;
+                tabElement.before(this.layoutManager.tabDropPlaceholder);
+            } else {
+                this._dropIndex = Math.min(i + 1, tabsLength);
+                tabElement.after(this.layoutManager.tabDropPlaceholder);
+            }
+
+
+            if (this._sided) {
+                placeHolderTop = this.layoutManager.tabDropPlaceholder.offset().top;
+                this.layoutManager.dropTargetIndicator.highlightArea({
+                    x1: tabTop,
+                    x2: tabTop + tabElement.innerHeight(),
+                    y1: placeHolderTop,
+                    y2: placeHolderTop + this.layoutManager.tabDropPlaceholder.width()
+                });
+                return;
+            }
+            placeHolderLeft = this.layoutManager.tabDropPlaceholder.offset().left;
+
+            this.layoutManager.dropTargetIndicator.highlightArea({
+                x1: placeHolderLeft,
+                x2: placeHolderLeft + this.layoutManager.tabDropPlaceholder.width(),
+                y1: tabTop,
+                y2: tabTop + tabElement.innerHeight()
+            });
+        },
+
+        _resetHeaderDropZone: function () {
+            this.layoutManager.tabDropPlaceholder.remove();
+        },
+
+        _setupHeaderPosition: function () {
+            var side = ['right', 'left', 'bottom'].indexOf(this._header.show) >= 0 && this._header.show;
+            this.header.element.toggle(!!this._header.show);
+            this._side = side;
+            this._sided = ['right', 'left'].indexOf(this._side) >= 0;
+            this.element.removeClass('lm_left lm_right lm_bottom');
+            if (this._side)
+                this.element.addClass('lm_' + this._side);
+            if (this.element.find('.lm_header').length && this.childElementContainer) {
+                var headerPosition = ['right', 'bottom'].indexOf(this._side) >= 0 ? 'before' : 'after';
+                this.header.element[headerPosition](this.childElementContainer);
+                this.callDownwards('setSize');
+            }
+        },
+
+        _highlightBodyDropZone: function (segment) {
+            var highlightArea = this._contentAreaDimensions[segment].highlightArea;
+            this.layoutManager.dropTargetIndicator.highlightArea(highlightArea);
+            this._dropSegment = segment;
+        }
+    });
+
+    lm.utils.BubblingEvent = function (name, origin) {
+        this.name = name;
+        this.origin = origin;
+        this.isPropagationStopped = false;
+    };
+
+    lm.utils.BubblingEvent.prototype.stopPropagation = function () {
+        this.isPropagationStopped = true;
+    };
+	/**
+	 * Minifies and unminifies configs by replacing frequent keys
+	 * and values with one letter substitutes. Config options must
+	 * retain array position/index, add new options at the end.
+	 *
+	 * @constructor
+	 */
+    lm.utils.ConfigMinifier = function () {
+        this._keys = [
+            'settings',
+            'hasHeaders',
+            'constrainDragToContainer',
+            'selectionEnabled',
+            'dimensions',
+            'borderWidth',
+            'minItemHeight',
+            'minItemWidth',
+            'headerHeight',
+            'dragProxyWidth',
+            'dragProxyHeight',
+            'labels',
+            'close',
+            'maximise',
+            'minimise',
+            'popout',
+            'content',
+            'componentName',
+            'componentState',
+            'id',
+            'width',
+            'type',
+            'height',
+            'isClosable',
+            'title',
+            'popoutWholeStack',
+            'openPopouts',
+            'parentId',
+            'activeItemIndex',
+            'reorderEnabled',
+            'borderGrabWidth',
+
+
+
+
+            //Maximum 36 entries, do not cross this line!
+        ];
+        if (this._keys.length > 36) {
+            throw new Error('Too many keys in config minifier map');
+        }
+
+        this._values = [
+            true,
+            false,
+            'row',
+            'column',
+            'stack',
+            'component',
+            'close',
+            'maximise',
+            'minimise',
+            'open in new window'
+        ];
+    };
+
+    lm.utils.copy(lm.utils.ConfigMinifier.prototype, {
+
+		/**
+		 * Takes a GoldenLayout configuration object and
+		 * replaces its keys and values recursively with
+		 * one letter counterparts
+		 *
+		 * @param   {Object} config A GoldenLayout config object
+		 *
+		 * @returns {Object} minified config
+		 */
+        minifyConfig: function (config) {
+            var min = {};
+            this._nextLevel(config, min, '_min');
+            return min;
+        },
+
+		/**
+		 * Takes a configuration Object that was previously minified
+		 * using minifyConfig and returns its original version
+		 *
+		 * @param   {Object} minifiedConfig
+		 *
+		 * @returns {Object} the original configuration
+		 */
+        unminifyConfig: function (minifiedConfig) {
+            var orig = {};
+            this._nextLevel(minifiedConfig, orig, '_max');
+            return orig;
+        },
+
+		/**
+		 * Recursive function, called for every level of the config structure
+		 *
+		 * @param   {Array|Object} orig
+		 * @param   {Array|Object} min
+		 * @param    {String} translationFn
+		 *
+		 * @returns {void}
+		 */
+        _nextLevel: function (from, to, translationFn) {
+            var key, minKey;
+
+            for (key in from) {
+
+				/**
+				 * For in returns array indices as keys, so let's cast them to numbers
+				 */
+                if (from instanceof Array) key = parseInt(key, 10);
+
+				/**
+				 * In case something has extended Object prototypes
+				 */
+                if (!from.hasOwnProperty(key)) continue;
+
+				/**
+				 * Translate the key to a one letter substitute
+				 */
+                minKey = this[translationFn](key, this._keys);
+
+				/**
+				 * For Arrays and Objects, create a new Array/Object
+				 * on the minified object and recurse into it
+				 */
+                if (typeof from[key] === 'object') {
+                    to[minKey] = from[key] instanceof Array ? [] : {};
+                    this._nextLevel(from[key], to[minKey], translationFn);
+
+					/**
+					 * For primitive values (Strings, Numbers, Boolean etc.)
+					 * minify the value
+					 */
+                } else {
+                    to[minKey] = this[translationFn](from[key], this._values);
+                }
+            }
+        },
+
+		/**
+		 * Minifies value based on a dictionary
+		 *
+		 * @param   {String|Boolean} value
+		 * @param   {Array<String|Boolean>} dictionary
+		 *
+		 * @returns {String} The minified version
+		 */
+        _min: function (value, dictionary) {
+			/**
+			 * If a value actually is a single character, prefix it
+			 * with ___ to avoid mistaking it for a minification code
+			 */
+            if (typeof value === 'string' && value.length === 1) {
+                return '___' + value;
+            }
+
+            var index = lm.utils.indexOf(value, dictionary);
+
+			/**
+			 * value not found in the dictionary, return it unmodified
+			 */
+            if (index === -1) {
+                return value;
+
+				/**
+				 * value found in dictionary, return its base36 counterpart
+				 */
+            } else {
+                return index.toString(36);
+            }
+        },
+
+        _max: function (value, dictionary) {
+			/**
+			 * value is a single character. Assume that it's a translation
+			 * and return the original value from the dictionary
+			 */
+            if (typeof value === 'string' && value.length === 1) {
+                return dictionary[parseInt(value, 36)];
+            }
+
+			/**
+			 * value originally was a single character and was prefixed with ___
+			 * to avoid mistaking it for a translation. Remove the prefix
+			 * and return the original character
+			 */
+            if (typeof value === 'string' && value.substr(0, 3) === '___') {
+                return value[3];
+            }
+			/**
+			 * value was not minified
+			 */
+            return value;
+        }
+    });
+
+	/**
+	 * An EventEmitter singleton that propagates events
+	 * across multiple windows. This is a little bit trickier since
+	 * windows are allowed to open childWindows in their own right
+	 *
+	 * This means that we deal with a tree of windows. Hence the rules for event propagation are:
+	 *
+	 * - Propagate events from this layout to both parents and children
+	 * - Propagate events from parent to this and children
+	 * - Propagate events from children to the other children (but not the emitting one) and the parent
+	 *
+	 * @constructor
+	 *
+	 * @param {lm.LayoutManager} layoutManager
+	 */
+    lm.utils.EventHub = function (layoutManager) {
+        lm.utils.EventEmitter.call(this);
+        this._layoutManager = layoutManager;
+        this._dontPropagateToParent = null;
+        this._childEventSource = null;
+        this.on(lm.utils.EventEmitter.ALL_EVENT, lm.utils.fnBind(this._onEventFromThis, this));
+        this._boundOnEventFromChild = lm.utils.fnBind(this._onEventFromChild, this);
+        $(window).on('gl_child_event', this._boundOnEventFromChild);
+    };
+
+	/**
+	 * Called on every event emitted on this eventHub, regardles of origin.
+	 *
+	 * @private
+	 *
+	 * @param {Mixed}
+	 *
+	 * @returns {void}
+	 */
+    lm.utils.EventHub.prototype._onEventFromThis = function () {
+        var args = Array.prototype.slice.call(arguments);
+
+        if (this._layoutManager.isSubWindow && args[0] !== this._dontPropagateToParent) {
+            this._propagateToParent(args);
+        }
+        this._propagateToChildren(args);
+
+        //Reset
+        this._dontPropagateToParent = null;
+        this._childEventSource = null;
+    };
+
+	/**
+	 * Called by the parent layout.
+	 *
+	 * @param   {Array} args Event name + arguments
+	 *
+	 * @returns {void}
+	 */
+    lm.utils.EventHub.prototype._$onEventFromParent = function (args) {
+        this._dontPropagateToParent = args[0];
+        this.emit.apply(this, args);
+    };
+
+	/**
+	 * Callback for child events raised on the window
+	 *
+	 * @param   {DOMEvent} event
+	 * @private
+	 *
+	 * @returns {void}
+	 */
+    lm.utils.EventHub.prototype._onEventFromChild = function (event) {
+        this._childEventSource = event.originalEvent.__gl;
+        this.emit.apply(this, event.originalEvent.__glArgs);
+    };
+
+	/**
+	 * Propagates the event to the parent by emitting
+	 * it on the parent's DOM window
+	 *
+	 * @param   {Array} args Event name + arguments
+	 * @private
+	 *
+	 * @returns {void}
+	 */
+    lm.utils.EventHub.prototype._propagateToParent = function (args) {
+        var event,
+            eventName = 'gl_child_event';
+
+        if (document.createEvent) {
+            event = window.opener.document.createEvent('HTMLEvents');
+            event.initEvent(eventName, true, true);
+        } else {
+            event = window.opener.document.createEventObject();
+            event.eventType = eventName;
+        }
+
+        event.eventName = eventName;
+        event.__glArgs = args;
+        event.__gl = this._layoutManager;
+
+        if (document.createEvent) {
+            window.opener.dispatchEvent(event);
+        } else {
+            window.opener.fireEvent('on' + event.eventType, event);
+        }
+    };
+
+	/**
+	 * Propagate events to children
+	 *
+	 * @param   {Array} args Event name + arguments
+	 * @private
+	 *
+	 * @returns {void}
+	 */
+    lm.utils.EventHub.prototype._propagateToChildren = function (args) {
+        var childGl, i;
+
+        for (i = 0; i < this._layoutManager.openPopouts.length; i++) {
+            childGl = this._layoutManager.openPopouts[i].getGlInstance();
+
+            if (childGl && childGl !== this._childEventSource) {
+                childGl.eventHub._$onEventFromParent(args);
+            }
+        }
+    };
+
+
+	/**
+	 * Destroys the EventHub
+	 *
+	 * @public
+	 * @returns {void}
+	 */
+
+    lm.utils.EventHub.prototype.destroy = function () {
+        $(window).off('gl_child_event', this._boundOnEventFromChild);
+    };
+	/**
+	 * A specialised GoldenLayout component that binds GoldenLayout container
+	 * lifecycle events to react components
+	 *
+	 * @constructor
+	 *
+	 * @param {lm.container.ItemContainer} container
+	 * @param {Object} state state is not required for react components
+	 */
+    lm.utils.ReactComponentHandler = function (container, state) {
+        this._reactComponent = null;
+        this._originalComponentWillUpdate = null;
+        this._container = container;
+        this._initialState = state;
+        this._reactClass = this._getReactClass();
+        this._container.on('open', this._render, this);
+        this._container.on('destroy', this._destroy, this);
+    };
+
+    lm.utils.copy(lm.utils.ReactComponentHandler.prototype, {
+
+		/**
+		 * Creates the react class and component and hydrates it with
+		 * the initial state - if one is present
+		 *
+		 * By default, react's getInitialState will be used
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _render: function () {
+            this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]);
+            this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () {
+            };
+            this._reactComponent.componentWillUpdate = this._onUpdate.bind(this);
+            if (this._container.getState()) {
+                this._reactComponent.setState(this._container.getState());
+            }
+        },
+
+		/**
+		 * Removes the component from the DOM and thus invokes React's unmount lifecycle
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _destroy: function () {
+            ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
+            this._container.off('open', this._render, this);
+            this._container.off('destroy', this._destroy, this);
+        },
+
+		/**
+		 * Hooks into React's state management and applies the componentstate
+		 * to GoldenLayout
+		 *
+		 * @private
+		 * @returns {void}
+		 */
+        _onUpdate: function (nextProps, nextState) {
+            this._container.setState(nextState);
+            this._originalComponentWillUpdate.call(this._reactComponent, nextProps, nextState);
+        },
+
+		/**
+		 * Retrieves the react class from GoldenLayout's registry
+		 *
+		 * @private
+		 * @returns {React.Class}
+		 */
+        _getReactClass: function () {
+            var componentName = this._container._config.component;
+            var reactClass;
+
+            if (!componentName) {
+                throw new Error('No react component name. type: react-component needs a field `component`');
+            }
+
+            reactClass = this._container.layoutManager.getComponent(componentName);
+
+            if (!reactClass) {
+                throw new Error('React component "' + componentName + '" not found. ' +
+                    'Please register all components with GoldenLayout using `registerComponent(name, component)`');
+            }
+
+            return reactClass;
+        },
+
+		/**
+		 * Copies and extends the properties array and returns the React element
+		 *
+		 * @private
+		 * @returns {React.Element}
+		 */
+        _getReactComponent: function () {
+            var defaultProps = {
+                glEventHub: this._container.layoutManager.eventHub,
+                glContainer: this._container,
+            };
+            var props = $.extend(defaultProps, this._container._config.props);
+            return React.createElement(this._reactClass, props);
+        }
+    });
+})(window.$);
\ No newline at end of file
diff --git a/src/client/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<SubCollectionViewProps> {
@@ -286,6 +284,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     tabCreated = async (tab: any) => {
         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(`<div class="messageCounter">0</div>`);
@@ -325,16 +326,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             .click(action(function () {
                 //if (confirm('really close this?')) {
                 stack.remove();
-                //}
-            }));
-        stack.header.controlsContainer.find('.lm_popout') //get the close icon
-            .off('click') //unbind the current click handler
-            .click(action(function () {
-                var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
-                let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
-            }));
-        stack.header.controlsContainer.find('.lm_close') //unbind the current click handler
-            .click(async function () {
                 stack.contentItems.map(async (contentItem: any) => {
                     let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
@@ -342,7 +333,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         CollectionDockingView.Instance._removedDocs.push(theDoc);
                     }
                 });
-            });
+                //}
+            }));
+        stack.header.controlsContainer.find('.lm_popout') //get the close icon
+            .off('click') //unbind the current click handler
+            .click(action(function () {
+                stack.config.fixed = !stack.config.fixed;
+                // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+                // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
+            }));
     }
 
     render() {
@@ -351,6 +350,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
         );
     }
+
 }
 
 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<TreeViewProps> {
 
     @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<CollectionFreeF
     public toggleIcon = async (): Promise<void> => {
         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<CollectionFreeF
         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)) {
+            if (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 ((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);
-                                    }
-                                })
-                            }
-                        });
+                        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<T extends Field> extends ObjectField {
         return list;
     }
 
-    [key: number]: FieldResult<T>;
+    [key: number]: T | (T extends RefField ? Promise<T> : never);
 
     @serializable(alias("fields", list(autoObject())))
     private get __fields() {
-- 
cgit v1.2.3-70-g09d2


From e96e3382104a2ae8dbe41252c0b38206b983e94a Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 6 May 2019 13:10:19 -0400
Subject: addd doc list cast

---
 .../views/collections/CollectionSchemaView.tsx       | 10 ++++------
 .../views/nodes/CollectionFreeFormDocumentView.tsx   | 20 +++++++++-----------
 src/new_fields/Doc.ts                                |  5 +++++
 src/new_fields/List.ts                               |  2 +-
 src/new_fields/Types.ts                              |  2 +-
 5 files changed, 20 insertions(+), 19 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 4fee9db85..16818affd 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -19,7 +19,7 @@ import { DocumentView } from "../nodes/DocumentView";
 import { FieldView, FieldViewProps } from "../nodes/FieldView";
 import "./CollectionSchemaView.scss";
 import { CollectionSubView } from "./CollectionSubView";
-import { Opt, Field, Doc } from "../../../new_fields/Doc";
+import { Opt, Field, Doc, DocListCast } from "../../../new_fields/Doc";
 import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
 import { listSpec } from "../../../new_fields/Schema";
 import { List } from "../../../new_fields/List";
@@ -111,17 +111,15 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
                         }
                         return applyToDoc(props.Document, script.run);
                     }}
-                    OnFillDown={(value: string) => {
+                    OnFillDown={async (value: string) => {
                         let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
                         if (!script.compiled) {
                             return;
                         }
                         const run = script.run;
                         //TODO This should be able to be refactored to compile the script once
-                        const val = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc));
-                        if (val) {
-                            val.forEach(doc => applyToDoc(doc, run));
-                        }
+                        const val = await DocListCast(this.props.Document[this.props.fieldKey])
+                        val && val.forEach(doc => applyToDoc(doc, run));
                     }}>
                 </EditableView>
             </div >
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2c8e6aef3..24a75049a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -6,10 +6,10 @@ import "./DocumentView.scss";
 import React = require("react");
 import { DocComponent } from "../DocComponent";
 import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, Cast, NumCast, BoolCast, PromiseValue } from "../../../new_fields/Types";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
 import { OmitKeys, Utils } from "../../../Utils";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 
@@ -125,15 +125,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     public toggleIcon = async (): Promise<void> => {
         SelectionManager.DeselectAll();
         let isMinimized: boolean | undefined;
-        let maximizedDocs = Cast(this.props.Document.maximizedDocs, listSpec(Doc));
+        let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
         let minimizedDoc: Doc | undefined = this.props.Document;
         if (!maximizedDocs) {
             minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
-            if (minimizedDoc) maximizedDocs = Cast(minimizedDoc.maximizedDocs, listSpec(Doc));
+            if (minimizedDoc) maximizedDocs = await DocListCast(minimizedDoc.maximizedDocs);
         }
         if (minimizedDoc && maximizedDocs) {
             let minimizedTarget = minimizedDoc;
-            (await Promise.all(maximizedDocs)).forEach(maximizedDoc => {
+            maximizedDocs.forEach(maximizedDoc => {
                 let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
                 if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
                     if (isMinimized === undefined) {
@@ -167,16 +167,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             if (BoolCast(this.props.Document.isButton, false)) {
-                let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc));
+                let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
                 if (maximizedDocs) {   // bcz: need a better way to associate behaviors with click events on widget-documents
                     if ((metaKey && !this.props.Document.maximizeOnRight) || (!metaKey && this.props.Document.maximizeOnRight)) {
-                        let dataDocs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
+                        let dataDocs = await DocListCast(CollectionDockingView.Instance.props.Document.data);
                         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) {
+                            maximizedDocs.forEach(maxDoc => {
+                                if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) {
                                     CollectionDockingView.Instance.AddRightSplit(maxDoc);
                                 } else {
                                     CollectionDockingView.Instance.CloseRightSplit(maxDoc);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index b2863c632..70dd8361b 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -29,6 +29,11 @@ const SelfProxy = Symbol("SelfProxy");
 export const WidthSym = Symbol("Width");
 export const HeightSym = Symbol("Height");
 
+export function DocListCast(field: FieldResult) {
+    const list = Cast(field, listSpec(Doc))
+    return list ? Promise.all(list) : Promise.resolve(undefined);
+}
+
 @Deserializable("doc").withFields(["id"])
 export class Doc extends RefField {
     constructor(id?: FieldId, forceSave?: boolean) {
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index 213ed21aa..5aba64406 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -236,5 +236,5 @@ class ListImpl<T extends Field> extends ObjectField {
 
     private [Self] = this;
 }
-export type List<T extends Field> = ListImpl<T> & T[];
+export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
 export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
\ No newline at end of file
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index 60f08dc90..c07d38786 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -1,4 +1,4 @@
-import { Field, Opt, FieldResult } from "./Doc";
+import { Field, Opt, FieldResult, Doc } from "./Doc";
 import { List } from "./List";
 
 export type ToType<T extends ToConstructor<Field> | ListSpec<Field>> =
-- 
cgit v1.2.3-70-g09d2


From 8ed190ae29f90442c3589ee692578150b8ad8667 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 6 May 2019 15:39:14 -0400
Subject: switched to alt key for toggling how minimized views appear

---
 src/client/views/collections/CollectionDockingView.tsx    | 5 +++--
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 5 ++---
 2 files changed, 5 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 05c467763..d894909d0 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -134,8 +134,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             newContentItem.config.width = 50;
         }
         if (minimize) {
-            newContentItem.config.width = 10;
-            newContentItem.config.height = 10;
+            // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
+            // newContentItem.config.width = 10;
+            // newContentItem.config.height = 10;
         }
         newContentItem.callDownwards('_$init');
         this.layoutChanged();
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 24a75049a..91ef1e59d 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -162,14 +162,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
     onClick = async (e: React.MouseEvent) => {
         e.stopPropagation();
-        let ctrlKey = e.ctrlKey;
-        let metaKey = e.metaKey;
+        let altKey = e.altKey;
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             if (BoolCast(this.props.Document.isButton, false)) {
                 let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
                 if (maximizedDocs) {   // bcz: need a better way to associate behaviors with click events on widget-documents
-                    if ((metaKey && !this.props.Document.maximizeOnRight) || (!metaKey && this.props.Document.maximizeOnRight)) {
+                    if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
                         let dataDocs = await DocListCast(CollectionDockingView.Instance.props.Document.data);
                         if (dataDocs) {
                             SelectionManager.DeselectAll();
-- 
cgit v1.2.3-70-g09d2


From 5aa66529751fb638d8f6b721ab1606fb9dc48d3f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 6 May 2019 22:53:52 -0400
Subject: fixed icon box titles.  fixed artifact related to maximizing icons.

---
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx |  3 ++-
 src/client/views/nodes/IconBox.tsx                        | 15 +++++++--------
 2 files changed, 9 insertions(+), 9 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 91ef1e59d..f1083f859 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -9,7 +9,7 @@ import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schem
 import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
 import { OmitKeys, Utils } from "../../../Utils";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 
@@ -148,6 +148,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
                         maxw !== undefined && maxh !== undefined) {
                         let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
+                        maximizedDoc.width = maximizedDoc.height = 1;
                         maximizedDoc.isMinimized = false;
                         maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
                     }
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index b521d5ce6..19abec4af 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -2,13 +2,12 @@ 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, reaction, IReactionDisposer } from "mobx";
+import { computed, observable, runInAction, reaction, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
-import { SelectionManager } from "../../util/SelectionManager";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./IconBox.scss";
 import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { IconField } from "../../../new_fields/IconField";
 import { ContextMenu } from "../ContextMenu";
 import Measure from "react-measure";
@@ -28,9 +27,9 @@ export class IconBox extends React.Component<FieldViewProps> {
     _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" : "");
+            async () => {
+                let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
+                this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
             }, { fireImmediately: true });
     }
     componentWillUnmount() {
@@ -63,11 +62,11 @@ export class IconBox extends React.Component<FieldViewProps> {
     @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), []);
-        let label = !hideLabel && maxDoc && labelField ? (maxDoc.length === 1 ? maxDoc[0][labelField] : this.props.Document[labelField]) : "";
+        let firstDoc = maxDoc && maxDoc.length > 0 && maxDoc[0] instanceof Doc ? maxDoc[0] as Doc : undefined;
+        let label = !hideLabel && firstDoc && labelField ? firstDoc[labelField] : "";
         return (
             <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
                 {this.minimizedIcon}
-- 
cgit v1.2.3-70-g09d2


From 4ba63508738ec7ea68d7e8a9a7932c2e764ac545 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 6 May 2019 23:25:25 -0400
Subject: fixed undo for minimizing docs

---
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f1083f859..c4f85847a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -12,6 +12,7 @@ import { SelectionManager } from "../../util/SelectionManager";
 import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
 }
@@ -95,6 +96,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
+        if (first) {
+            if (maximizing) target.width = target.height = 1;
+        }
         setTimeout(() => {
             let now = Date.now();
             let progress = Math.min(1, (now - stime) / 200);
@@ -133,6 +137,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         }
         if (minimizedDoc && maximizedDocs) {
             let minimizedTarget = minimizedDoc;
+            CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
             maximizedDocs.forEach(maximizedDoc => {
                 let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
                 if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
@@ -148,14 +153,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
                         maxw !== undefined && maxh !== undefined) {
                         let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
-                        maximizedDoc.width = maximizedDoc.height = 1;
                         maximizedDoc.isMinimized = false;
                         maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
                     }
                 }
             });
+            setTimeout(() => {
+                CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+                CollectionFreeFormDocumentView._undoBatch = undefined;
+            }, 500);
         }
     }
+    static _undoBatch?: UndoManager.Batch = undefined;
     onPointerDown = (e: React.PointerEvent): void => {
         this._downX = e.clientX;
         this._downY = e.clientY;
-- 
cgit v1.2.3-70-g09d2


From 26141a697ae52a7edf3cc6845ce2153111f8860e Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 7 May 2019 15:26:29 -0400
Subject: added Bullet template

---
 src/client/views/DocumentDecorations.tsx             | 15 ++++++++++++---
 src/client/views/TemplateMenu.tsx                    | 20 +++++++++++++++-----
 src/client/views/Templates.tsx                       | 18 +++++-------------
 src/client/views/collections/CollectionBaseView.tsx  | 10 +++++++---
 .../views/nodes/CollectionFreeFormDocumentView.tsx   |  9 +++++----
 5 files changed, 44 insertions(+), 28 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e2f445874..8ae71fdc8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -507,9 +507,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
 
         let templates: Map<Template, boolean> = new Map();
-        let doc = SelectionManager.SelectedDocuments()[0];
         Array.from(Object.values(Templates.TemplateList)).map(template => {
-            let docTemps = doc.templates;
+            let docTemps = SelectionManager.SelectedDocuments().reduce((res: string[], doc: DocumentView, i) => {
+                let temps = doc.props.Document.templates;
+                if (temps instanceof List) {
+                    temps.map(temp => {
+                        if (temp !== Templates.Bullet.Layout || i === 0) {
+                            res.push(temp);
+                        }
+                    })
+                }
+                return res
+            }, [] as string[]);
             let checked = false;
             docTemps.forEach(temp => {
                 if (template.Layout === temp) {
@@ -560,7 +569,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                             <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
                         </div>
                     </div>
-                    <TemplateMenu doc={doc} templates={templates} />
+                    <TemplateMenu docs={SelectionManager.SelectedDocuments()} templates={templates} />
                 </div>
             </div >
         </div>
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f29d9c8a1..376feb5a5 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -4,6 +4,8 @@ import { observer } from "mobx-react";
 import './DocumentDecorations.scss';
 import { Template } from "./Templates";
 import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../new_fields/List";
+import { Doc } from "../../new_fields/Doc";
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -25,24 +27,32 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
 }
 
 export interface TemplateMenuProps {
-    doc: DocumentView;
+    docs: DocumentView[];
     templates: Map<Template, boolean>;
 }
 
 @observer
 export class TemplateMenu extends React.Component<TemplateMenuProps> {
-
     @observable private _hidden: boolean = true;
 
-
     @action
     toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
         if (event.target.checked) {
-            this.props.doc.addTemplate(template);
+            if (template.Name == "Bullet") {
+                this.props.docs[0].addTemplate(template);
+                this.props.docs[0].props.Document.maximizedDocs = new List<Doc>(this.props.docs.filter((v, i) => i !== 0).map(v => v.props.Document));
+            } else {
+                this.props.docs.map(d => d.addTemplate(template));
+            }
             this.props.templates.set(template, true);
             this.props.templates.forEach((checked, template) => console.log("Set Checked + " + checked + " " + this.props.templates.get(template)));
         } else {
-            this.props.doc.removeTemplate(template);
+            if (template.Name == "Bullet") {
+                this.props.docs[0].removeTemplate(template);
+                this.props.docs[0].props.Document.maximizedDocs = undefined;
+            } else {
+                this.props.docs.map(d => d.removeTemplate(template));
+            }
             this.props.templates.set(template, false);
             this.props.templates.forEach((checked, template) => console.log("Unset Checked + " + checked + " " + this.props.templates.get(template)));
         }
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 738272913..51fca4c41 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -54,21 +54,13 @@ export namespace Templates {
         `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
         <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.title}</div></div>`
     );
-    export const Summary = new Template("Title", TemplatePosition.InnerTop,
-        `<div style="height:100%; width:100%;position:absolute; margin:0; padding: 0">
-            <div style="height:60%; width:100%;position:absolute;background:yellow; padding:0; margin:0">
-                {layout}
-            </div>
-            <div style="bottom:0px; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
-                <FieldView {...props} fieldKey={"doc1"} />
-            </div>
-            <div style="bottom:0; left: 50%; height:40%; width:50%; position:absolute; background-color: rgba(0, 0, 0, .4); color: white;">
-                <FieldView {...props} fieldKey={"doc2"} />
-            </div>
-        </div>`
+
+    export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+        `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+        <div className="isBullet" style="height:25px; width:25px; margin-left:-25px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px"/></div>`
     );
 
-    export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption];
+    export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption, Bullet];
 
     export function sortTemplates(a: Template, b: Template) {
         if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index cbb568c07..14b92af48 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -95,15 +95,18 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         if (!this.createsCycle(doc, props.Document)) {
             //TODO This won't create the field if it doesn't already exist
             const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
+            let alreadyAdded = true;
             if (value !== undefined) {
-                if (allowDuplicates || !value.some(v => v[Id] === doc[Id])) {
+                if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+                    alreadyAdded = false;
                     value.push(doc);
                 }
             } else {
+                alreadyAdded = false;
                 Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
             }
             // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
-            if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) {
+            if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
                 let zoom = NumCast(this.props.Document.scale, 1);
                 Doc.SetOnPrototype(doc, "zoomBasis", zoom);
             }
@@ -118,7 +121,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
         let index = -1;
         for (let i = 0; i < value.length; i++) {
-            if (value[i][Id] === doc[Id]) {
+            let v = value[i];
+            if (v instanceof Doc && v[Id] === doc[Id]) {
                 index = i;
                 break;
             }
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index c4f85847a..df78d92e2 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -127,6 +127,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
     @action
     public toggleIcon = async (): Promise<void> => {
+        UndoManager.GetOpenBatches().forEach(batch => console.log(batch.batchName));
         SelectionManager.DeselectAll();
         let isMinimized: boolean | undefined;
         let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
@@ -144,8 +145,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     if (isMinimized === undefined) {
                         isMinimized = BoolCast(maximizedDoc.isMinimized, false);
                     }
-                    let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2;
-                    let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2;
+                    let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale / 2;
+                    let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale / 2;
                     let maxx = NumCast(maximizedDoc.x, undefined);
                     let maxy = NumCast(maximizedDoc.y, undefined);
                     let maxw = NumCast(maximizedDoc.width, undefined);
@@ -153,7 +154,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
                         maxw !== undefined && maxh !== undefined) {
                         let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
-                        maximizedDoc.isMinimized = false;
+                        if (isMinimized) maximizedDoc.isMinimized = false;
                         maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
                     }
                 }
@@ -175,7 +176,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         let altKey = e.altKey;
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
-            if (BoolCast(this.props.Document.isButton, false)) {
+            if (BoolCast(this.props.Document.isButton, false) || (e.target as any).className === "isBullet") {
                 let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
                 if (maximizedDocs) {   // bcz: need a better way to associate behaviors with click events on widget-documents
                     if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
-- 
cgit v1.2.3-70-g09d2


From d588a4543f1ccc06d1e45fe748007b730c18c042 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 8 May 2019 00:45:14 -0400
Subject: small fixes to titles, templates, minimizing, and  keyvalue deletion

---
 src/client/views/Templates.tsx                             | 12 ++++++++++--
 .../views/collections/collectionFreeForm/MarqueeView.tsx   | 13 +++++++++++--
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx  |  6 +++---
 src/client/views/nodes/DocumentContentsView.tsx            | 14 ++++++++++----
 src/client/views/nodes/KeyValuePair.tsx                    |  5 +++--
 5 files changed, 37 insertions(+), 13 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index 51fca4c41..de94a1578 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -50,6 +50,10 @@ export namespace Templates {
         `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={"caption"}/></div> </div>`
     );
 
+    export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+        `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
+        <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.title}</div></div>`
+    );
     export const Title = new Template("Title", TemplatePosition.InnerTop,
         `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
         <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{props.Document.title}</div></div>`
@@ -57,10 +61,14 @@ export namespace Templates {
 
     export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
         `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div>
-        <div className="isBullet" style="height:25px; width:25px; margin-left:-25px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px"/></div>`
+        <div id="isBullet" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+        <img  id="isBullet"  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg==" 
+         width="15px" height="15px"/>
+         </div>
+        </div>`
     );
 
-    export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption, Bullet];
+    export const TemplateList: Template[] = [Title, TitleOverlay, OuterCaption, InnerCaption, SideCaption, Bullet];
 
     export function sortTemplates(a: Template, b: Template) {
         if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c9b0b28f7..b9a6792ed 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -63,8 +63,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 let text = await navigator.clipboard.readText();
                 let ns = text.split("\n").filter(t => t != "\r");
                 for (let i = 0; i < ns.length - 1; i++) {
-                    while (!(ns[i].endsWith("-\r") || ns[i].endsWith(".\r") || ns[i].endsWith(":\r")) && i < ns.length - 1) {
-                        ns.splice(i, 2, ns[i].substr(0, ns[i].length - 1) + ns[i + 1].trimLeft());
+                    if (ns[i].trim() === "") {
+                        ns.splice(i, 1);
+                        continue;
+                    }
+                    while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+                        ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+                        ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+                        let sub = ns[i].endsWith("\r") ? 1 : 0;
+                        let br = ns[i + 1].trim() === "";
+                        ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+                        if (br) break;
                     }
                 }
                 ns.map(line => {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index df78d92e2..0e34cb626 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -145,8 +145,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     if (isMinimized === undefined) {
                         isMinimized = BoolCast(maximizedDoc.isMinimized, false);
                     }
-                    let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale / 2;
-                    let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale / 2;
+                    let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
+                    let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2;
                     let maxx = NumCast(maximizedDoc.x, undefined);
                     let maxy = NumCast(maximizedDoc.y, undefined);
                     let maxw = NumCast(maximizedDoc.width, undefined);
@@ -176,7 +176,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         let altKey = e.altKey;
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
-            if (BoolCast(this.props.Document.isButton, false) || (e.target as any).className === "isBullet") {
+            if (BoolCast(this.props.Document.isButton, false) || (e.target as any).id === "isBullet") {
                 let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
                 if (maximizedDocs) {   // bcz: need a better way to associate behaviors with click events on widget-documents
                     if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bbc927b5a..1dc6343e8 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -64,10 +64,16 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         let base = baseLayout;
         let layout = baseLayout;
 
-        this.templates.forEach(template => {
-            layout = template.replace("{layout}", base);
-            base = layout;
-        });
+        // bcz: templates are intended for the main document layout.  However, 
+        // a DocumentContentsView is also used to render the annotation overlay for a document.
+        // So we detect that here by checking the layoutKey.  This should probably be moved into
+        // a prop so that the overlay can explicitly turn off templates.
+        if (this.props.layoutKey !== "backgroundLayout") {
+            this.templates.forEach(template => {
+                layout = template.replace("{layout}", base);
+                base = layout;
+            });
+        }
         return layout;
     }
 
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 203fb5625..5de660d57 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -46,8 +46,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                 <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
                     <div className="keyValuePair-td-key-container">
                         <button className="keyValuePair-td-key-delete" onClick={() => {
-                            let field = FieldValue(props.Document[props.fieldKey]);
-                            field && (props.Document[props.fieldKey] = undefined);
+                            if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1)
+                                props.Document[props.fieldKey] = undefined;
+                            else props.Document.proto![props.fieldKey] = undefined;
                         }}>
                             X
                         </button>
-- 
cgit v1.2.3-70-g09d2


From a573867d2443a806f174db58e2a920db3405934c Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 8 May 2019 10:23:28 -0400
Subject: added delete from non-data keys in tree view.  fixed undo
 iconanimating problem

---
 src/client/views/collections/CollectionTreeView.tsx  | 20 ++++++++++++--------
 .../views/nodes/CollectionFreeFormDocumentView.tsx   |  4 +++-
 2 files changed, 15 insertions(+), 9 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 6fa374464..6e690236f 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -68,8 +68,8 @@ class TreeView extends React.Component<TreeViewProps> {
     }
 
     @action
-    remove = (document: Document) => {
-        let children = Cast(this.props.document.data, listSpec(Doc), []);
+    remove = (document: Document, key: string) => {
+        let children = Cast(this.props.document[key], listSpec(Doc), []);
         if (children) {
             children.splice(children.indexOf(document), 1);
         }
@@ -81,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
             return true;
         }
         //TODO This should check if it was removed
-        this.remove(document);
+        this.remove(document, "data");
         return addDoc(document);
     }
 
@@ -136,7 +136,11 @@ class TreeView extends React.Component<TreeViewProps> {
             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.addItem({
+                description: "Delete", event: undoBatch(() => {
+                    this.props.deleteDoc(this.props.document);
+                })
+            });
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
             e.stopPropagation();
         }
@@ -160,7 +164,7 @@ class TreeView extends React.Component<TreeViewProps> {
                     contentElement.push(<ul key={key + "more"}>
                         {(key === "data") ? (null) :
                             <span className="collectionTreeView-keyHeader" key={key}>{key}</span>}
-                        {TreeView.GetChildElements(docList, key !== "data", this.remove, this.move, this.props.dropAction)}
+                        {TreeView.GetChildElements(docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)}
                     </ul >);
                 } else
                     bulletType = BulletType.Collapsed;
@@ -175,9 +179,9 @@ class TreeView extends React.Component<TreeViewProps> {
             </li>
         </div>;
     }
-    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 =>
-            <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+    public static GetChildElements(docs: (Doc | Promise<Doc>)[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
+        return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child =>
+            <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
     }
 }
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 0e34cb626..5efc75793 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -138,7 +138,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         }
         if (minimizedDoc && maximizedDocs) {
             let minimizedTarget = minimizedDoc;
-            CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+            if (!CollectionFreeFormDocumentView._undoBatch) {
+                CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+            }
             maximizedDocs.forEach(maximizedDoc => {
                 let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
                 if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
-- 
cgit v1.2.3-70-g09d2


From 9573fe783fe9edfa38421d806a473d4cc496733f Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 8 May 2019 11:16:30 -0400
Subject: cleaned up template menu.  made icon doc deletable.

---
 src/client/util/UndoManager.ts                            | 5 ++++-
 src/client/views/DocumentDecorations.scss                 | 7 ++++---
 src/client/views/DocumentDecorations.tsx                  | 7 +++----
 src/client/views/TemplateMenu.tsx                         | 4 +---
 src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 1 -
 5 files changed, 12 insertions(+), 12 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 0b5280c4a..c0ed015bd 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,7 +1,6 @@
 import { observable, action, runInAction } from "mobx";
 import 'source-map-support/register';
 import { Without } from "../../Utils";
-import { string } from "prop-types";
 
 function getBatchName(target: any, key: string | symbol): string {
     let keyName = key.toString();
@@ -94,6 +93,10 @@ export namespace UndoManager {
         return redoStack.length > 0;
     }
 
+    export function PrintBatches(): void {
+        GetOpenBatches().forEach(batch => console.log(batch.batchName));
+    }
+
     let openBatches: Batch[] = [];
     export function GetOpenBatches(): Without<Batch, 'end'>[] {
         return openBatches;
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 158b02b5a..6a2e33836 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -210,14 +210,15 @@ $linkGap : 3px;
     position: absolute;
     top: 0;
     left: 30px;
-    width: 150px;
-    line-height: 25px;
-    max-height: 175px;
+    width: max-content;
     font-family: $sans-serif;
     font-size: 12px;
     background-color: $light-color-secondary;
     padding: 2px 12px;
     list-style: none;
+    .templateToggle {
+        text-align: left;
+    }
 
     input {
         margin-right: 10px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8ae71fdc8..e3eb034fa 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -29,6 +29,7 @@ import { CollectionFreeFormView } from "./collections/collectionFreeForm/Collect
 import { CollectionView } from "./collections/CollectionView";
 import { createCipher } from "crypto";
 import { FieldView } from "./nodes/FieldView";
+import { DocumentManager } from "../util/DocumentManager";
 
 library.add(faLink);
 
@@ -277,13 +278,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
         let doc = docView.props.Document;
         let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
-        if (!iconDoc) {
+
+        if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
             const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
             iconDoc = this.createIcon([docView], layout);
         }
-        if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined) {
-            SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!);
-        }
         return iconDoc;
     }
     moveIconDoc(iconDoc: Doc) {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 376feb5a5..d74982ef8 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -15,7 +15,7 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
     render() {
         if (this.props.template) {
             return (
-                <li>
+                <li className="templateToggle">
                     <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} />
                     {this.props.template.Name}
                 </li>
@@ -45,7 +45,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 this.props.docs.map(d => d.addTemplate(template));
             }
             this.props.templates.set(template, true);
-            this.props.templates.forEach((checked, template) => console.log("Set Checked + " + checked + " " + this.props.templates.get(template)));
         } else {
             if (template.Name == "Bullet") {
                 this.props.docs[0].removeTemplate(template);
@@ -54,7 +53,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 this.props.docs.map(d => d.removeTemplate(template));
             }
             this.props.templates.set(template, false);
-            this.props.templates.forEach((checked, template) => console.log("Unset Checked + " + checked + " " + this.props.templates.get(template)));
         }
     }
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 5efc75793..a9c53933e 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -127,7 +127,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
     @action
     public toggleIcon = async (): Promise<void> => {
-        UndoManager.GetOpenBatches().forEach(batch => console.log(batch.batchName));
         SelectionManager.DeselectAll();
         let isMinimized: boolean | undefined;
         let maximizedDocs = await DocListCast(this.props.Document.maximizedDocs);
-- 
cgit v1.2.3-70-g09d2


From 6f9316683929b86e27fd4e2e30609670cd89f964 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 8 May 2019 12:10:07 -0400
Subject: adjusted titling of icons and summaries.

---
 .../collectionFreeForm/MarqueeView.scss            |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 55 ++++++++++++----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  1 +
 src/client/views/nodes/DocumentView.tsx            | 23 ++++++---
 src/client/views/nodes/IconBox.tsx                 | 11 -----
 5 files changed, 48 insertions(+), 44 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index ae0a9fd48..6e8ec8662 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
         white-space:nowrap;
     }
     .marquee-legend::after {
-        content: "Press: C (collection), or Delete"
+        content: "Press: c (collection), s (summary), r (replace) or Delete"
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b9a6792ed..9764db1d4 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -9,7 +9,7 @@ import { PreviewCursor } from "../../PreviewCursor";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
-import { Utils } from "../../../../Utils";
+import { Utils, deepCopy } from "../../../../Utils";
 import { Doc } from "../../../../new_fields/Doc";
 import { NumCast, Cast } from "../../../../new_fields/Types";
 import { InkField, StrokeData } from "../../../../new_fields/InkField";
@@ -68,6 +68,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                         continue;
                     }
                     while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
+                        ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
                         ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
                         ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
                         let sub = ns[i].endsWith("\r") ? 1 : 0;
@@ -182,12 +183,19 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this.cleanupInteractions(false);
             e.stopPropagation();
         }
-        if (e.key === "c" || e.key === "r" || e.key === "R" || e.key === "e") {
+        if (e.key === "c" || e.key === "r" || e.key === "s" || 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 === "s") {
+                    let dCopy = Doc.MakeCopy(d);
+                    dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+                    dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+                    dCopy.page = -1;
+                    return dCopy;
+                }
+                else if (e.key !== "r") {
                     this.props.removeDocument(d);
                     d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
                     d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
@@ -208,25 +216,20 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 width: bounds.width * zoomBasis,
                 height: bounds.height * zoomBasis,
                 ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
-                title: "a nested collection"
+                title: "a nested collection",
             });
 
             this.marqueeInkDelete(inkData);
             // SelectionManager.DeselectAll();
-            if (e.key === "r" || e.key === "R") {
+            if (e.key === "s" || 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") {
+                if (e.key === "s") {
                     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);
-                    }
+                    newCollection.proto!.summaryDoc = summary;
+                    selected = [newCollection];
                 }
                 summary.proto!.maximizedDocs = new List<Doc>(selected);
                 summary.proto!.isButton = true;
@@ -243,20 +246,20 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 this.props.addDocument(newCollection, false);
             }
             this.cleanupInteractions(false);
-        }
-        if (e.key === "s") {
-            this._commandExecuted = true;
-            e.stopPropagation();
-            e.preventDefault();
-            let bounds = this.Bounds;
-            let selected = this.marqueeSelect();
-            SelectionManager.DeselectAll();
-            let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
-            this.props.addLiveTextDocument(summary);
-            selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
+        } else
+            if (e.key === "s") {
+                // this._commandExecuted = true;
+                // e.stopPropagation();
+                // e.preventDefault();
+                // let bounds = this.Bounds;
+                // let selected = this.marqueeSelect();
+                // SelectionManager.DeselectAll();
+                // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+                // this.props.addLiveTextDocument(summary);
+                // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!));
 
-            this.cleanupInteractions(false);
-        }
+                // this.cleanupInteractions(false);
+            }
     }
     @action
     marqueeInkSelect(ink: Map<any, any>) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a9c53933e..1f8d22ab0 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -185,6 +185,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                         if (dataDocs) {
                             SelectionManager.DeselectAll();
                             maximizedDocs.forEach(maxDoc => {
+                                maxDoc.isMinimized = false;
                                 if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) {
                                     CollectionDockingView.Instance.AddRightSplit(maxDoc);
                                 } else {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a20a8a93b..38efeeba5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, runInAction } from "mobx";
+import { action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
 import { emptyFunction, Utils } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
@@ -16,10 +16,10 @@ import { Template, Templates } from "./../Templates";
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import React = require("react");
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Opt, Doc, WidthSym, HeightSym, DocListCast } from "../../../new_fields/Doc";
 import { DocComponent } from "../DocComponent";
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { FieldValue, StrCast, BoolCast } from "../../../new_fields/Types";
+import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types";
 import { List } from "../../../new_fields/List";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
@@ -99,6 +99,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     set templates(templates: List<string>) { this.props.Document.templates = templates; }
     screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
 
+    _reactionDisposer?: IReactionDisposer;
     @action
     componentDidMount() {
         if (this._mainCont.current) {
@@ -106,6 +107,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 handlers: { drop: this.drop.bind(this) }
             });
         }
+        this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+            async () => {
+                let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
+                if (maxDoc && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
+                    this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : "");
+                }
+                let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
+                if (sumDoc instanceof Doc) {
+                    this.props.Document.title = sumDoc.title + ".expanded";
+                }
+            }, { fireImmediately: true });
         DocumentManager.Instance.DocumentViews.push(this);
     }
     @action
@@ -121,9 +133,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
     @action
     componentWillUnmount() {
-        if (this._dropDisposer) {
-            this._dropDisposer();
-        }
+        if (this._reactionDisposer) this._reactionDisposer();
+        if (this._dropDisposer) this._dropDisposer();
         DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
     }
 
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 19abec4af..4bcb4c636 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,17 +24,6 @@ library.add(faFilm);
 @observer
 export class IconBox extends React.Component<FieldViewProps> {
     public static LayoutString() { return FieldView.LayoutString(IconBox); }
-    _reactionDisposer?: IReactionDisposer;
-    componentDidMount() {
-        this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs],
-            async () => {
-                let maxDoc = await DocListCast(this.props.Document.maximizedDocs);
-                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 : "<p>Error loading icon data</p>"; }
     @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
-- 
cgit v1.2.3-70-g09d2


From c67fc234cf4a524c19989134d878797563207e93 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 8 May 2019 16:05:02 -0400
Subject: fixes for templates and for dragging

---
 src/client/util/DragManager.ts                           | 16 +++++++++-------
 .../collectionFreeForm/CollectionFreeFormView.tsx        | 12 ++++--------
 .../views/nodes/CollectionFreeFormDocumentView.tsx       |  2 +-
 src/client/views/nodes/DocumentContentsView.tsx          | 13 +++++++------
 src/client/views/nodes/FormattedTextBox.tsx              |  2 +-
 5 files changed, 22 insertions(+), 23 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a3dbe6e43..ec192eaff 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -3,7 +3,7 @@ import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
 import { MainOverlayTextBox } from "../views/MainOverlayTextBox";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DocListCast } from "../../new_fields/Doc";
 import { Cast } from "../../new_fields/Types";
 import { listSpec } from "../../new_fields/Schema";
 
@@ -42,12 +42,14 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
 
 export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
     let srcTarg = sourceDoc.proto;
-    let draggedDocs = srcTarg ?
-        Cast(srcTarg.linkedToDocs, listSpec(Doc), []).map(linkDoc =>
-            Cast(linkDoc.linkedTo, Doc) as Doc) : [];
-    let draggedFromDocs = srcTarg ?
-        Cast(srcTarg.linkedFromDocs, listSpec(Doc), []).map(linkDoc =>
-            Cast(linkDoc.linkedFrom, Doc) as Doc) : [];
+    let draggedDocs: Doc[] = [];
+    let draggedFromDocs: Doc[] = []
+    if (srcTarg) {
+        let linkToDocs = await DocListCast(srcTarg.linkedToDocs);
+        let linkFromDocs = await DocListCast(srcTarg.linkedFromDocs);
+        if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+        if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+    }
     draggedDocs.push(...draggedFromDocs);
     if (draggedDocs.length) {
         let moddrag: Doc[] = [];
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7fa945891..fcc73d5b6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -326,10 +326,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 @observer
 class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
     @computed get overlayView() {
-        let overlayLayout = Cast(this.props.Document.overlayLayout, "string", "");
-        return !overlayLayout ? (null) :
-            (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
-                isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
+        return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+            isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
     }
     render() {
         return this.overlayView;
@@ -339,10 +337,8 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
 @observer
 class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
     @computed get backgroundView() {
-        let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", "");
-        return !backgroundLayout ? (null) :
-            (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
-                isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+        return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+            isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
     }
     render() {
         return this.backgroundView;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 1f8d22ab0..470c0c2f8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -170,7 +170,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     onPointerDown = (e: React.PointerEvent): void => {
         this._downX = e.clientX;
         this._downY = e.clientY;
-        e.stopPropagation();
+        // e.stopPropagation();
     }
     onClick = async (e: React.MouseEvent) => {
         e.stopPropagation();
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 294e17720..f404b7bc6 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed, trace } from "mobx";
 import { observer } from "mobx-react";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -45,7 +45,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     select: (ctrl: boolean) => void,
     layoutKey: string
 }> {
-    @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); }
+    @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", this.props.layoutKey === "layout" ? "<p>Error loading layout data</p>" : ""); }
 
     CreateBindings(): JsxBindings {
         return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
@@ -58,17 +58,17 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         }
         return new List<string>();
     }
-    set templates(templates: List<string>) { this.props.Document.templates = templates; }
-    get finalLayout() {
+    @computed get finalLayout() {
         const baseLayout = this.layout;
         let base = baseLayout;
         let layout = baseLayout;
 
-        // bcz: templates are intended only for a document's primary layout (not background).  However, 
+        // bcz: templates are intended only for a document's primary layout or overlay (not background).  However, 
         // a DocumentContentsView is used to render  annotation overlays, so we detect that here 
         // by checking the layoutKey.  This should probably be moved into
         // a prop so that the overlay can explicitly turn off templates.
-        if (this.props.layoutKey !== "backgroundLayout") {
+        if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
+            (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
             this.templates.forEach(template => {
                 let self = this;
                 function convertConstantsToNative(match: string, offset: number, x: string) {
@@ -84,6 +84,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     }
 
     render() {
+        if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
             components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
             bindings={this.CreateBindings()}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8d2f1c780..ee6000da7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -154,7 +154,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let text = StrCast(this.props.Document.documentText);
             if (text.startsWith("@@@")) {
                 this.props.Document.proto!.documentText = undefined;
-                this._editorView.dispatch(this._editorView.state.tr.insertText(text.substr(3)));
+                this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
             }
         }
 
-- 
cgit v1.2.3-70-g09d2

