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

          Error loading icon data

          "; } @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 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, 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 { @computed get overlayView() { - let overlayLayout = Cast(this.props.Document.overlayLayout, "string", ""); - return !overlayLayout ? (null) : - (); + return (); } render() { return this.overlayView; @@ -339,10 +337,8 @@ class CollectionFreeFormOverlayView extends React.Component { @observer class CollectionFreeFormBackgroundView extends React.Component boolean }> { @computed get backgroundView() { - let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", ""); - return !backgroundLayout ? (null) : - (); + return (); } 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 { 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 void, layoutKey: string }> { - @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "

          Error loading layout data

          "); } + @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", this.props.layoutKey === "layout" ? "

          Error loading layout data

          " : ""); } 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(); } - set templates(templates: List) { 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 Date: Thu, 9 May 2019 17:39:44 -0400 Subject: various cleanup to icons, summaries... --- src/client/util/DocumentManager.ts | 18 ++++++++++++-- src/client/util/SelectionManager.ts | 17 +++++++++++++ src/client/views/DocumentDecorations.tsx | 28 ++++++++++++++++++---- src/client/views/TemplateMenu.tsx | 16 +++++++++---- .../CollectionFreeFormLinkView.scss | 8 +++---- .../CollectionFreeFormLinkView.tsx | 6 ++--- .../CollectionFreeFormLinksView.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 17 ++++++------- .../views/nodes/CollectionFreeFormDocumentView.tsx | 17 ++++++------- src/client/views/nodes/DocumentView.tsx | 14 +++++++---- 10 files changed, 104 insertions(+), 39 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 4c264c7ec..779b07ce5 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,8 +1,9 @@ import { computed, observable } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; import { Doc } from '../../new_fields/Doc'; -import { FieldValue, Cast } from '../../new_fields/Types'; +import { FieldValue, Cast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; +import { SelectionManager } from './SelectionManager'; export class DocumentManager { @@ -70,7 +71,7 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { - return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { + return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); if (linksList && linksList.length) { pairs.push(...linksList.reduce((pairs, link) => { @@ -84,6 +85,19 @@ export class DocumentManager { return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); } + linksList = Cast(dv.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + if (linksList && linksList.length) { + pairs.push(...linksList.reduce((pairs, link) => { + if (link) { + let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc)); + if (linkFromDoc) { + DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 => + pairs.push({ a: dv, b: docView1, l: link })); + } + } + return pairs; + }, pairs)); + } return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index a3a8172c7..8c92c2023 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,6 +2,7 @@ import { observable, action } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { NumCast } from "../../new_fields/Types"; export namespace SelectionManager { class Manager { @@ -68,4 +69,20 @@ export namespace SelectionManager { export function SelectedDocuments(): Array { return manager.SelectedDocuments; } + export function ViewsSortedVertically(): DocumentView[] { + let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => { + if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1; + if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1; + return 0; + }); + return sorted; + } + export function ViewsSortedHorizontally(): DocumentView[] { + let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => { + if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1; + if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1; + return 0; + }); + return sorted; + } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 705e7a6d8..4786b4de6 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -249,7 +249,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) { selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc); } - !this._removeIcon && selectedDocs.length === 1 && this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized()); + if (!this._removeIcon) { + if (selectedDocs.length === 1) + this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized()); + else { + let docViews = SelectionManager.ViewsSortedVertically(); + let topDocView = docViews[0]; + let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); + if (ind !== -1) { + topDocView.templates.splice(ind, 1); + topDocView.props.Document.subBulletDocs = undefined; + } else { + topDocView.addTemplate(Templates.Bullet); + topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document)); + } + } + } this._removeIcon = false; } runInAction(() => this._minimizedX = this._minimizedY = 0); @@ -410,7 +425,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> break; } - FormattedTextBox.InputBoxOverlay = undefined; + runInAction(() => FormattedTextBox.InputBoxOverlay = undefined); SelectionManager.SelectedDocuments().forEach(element => { const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); @@ -507,7 +522,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let templates: Map = new Map(); Array.from(Object.values(Templates.TemplateList)).map(template => { - let docTemps = SelectionManager.SelectedDocuments().reduce((res: string[], doc: DocumentView, i) => { + let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => { + if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1; + if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1; + return 0; + }); + let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => { let temps = doc.props.Document.templates; if (temps instanceof List) { temps.map(temp => { @@ -568,7 +588,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
          - + diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index d74982ef8..e2b3bd07a 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -6,6 +6,7 @@ import { Template } from "./Templates"; import { DocumentView } from "./nodes/DocumentView"; import { List } from "../../new_fields/List"; import { Doc } from "../../new_fields/Doc"; +import { NumCast } from "../../new_fields/Types"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -35,20 +36,27 @@ export interface TemplateMenuProps { export class TemplateMenu extends React.Component { @observable private _hidden: boolean = true; + constructor(props: TemplateMenuProps) { + super(props); + console.log(""); + } + @action toggleTemplate = (event: React.ChangeEvent, template: Template): void => { if (event.target.checked) { if (template.Name == "Bullet") { - this.props.docs[0].addTemplate(template); - this.props.docs[0].props.Document.maximizedDocs = new List(this.props.docs.filter((v, i) => i !== 0).map(v => v.props.Document)); + let topDocView = this.props.docs[0]; + topDocView.addTemplate(template); + topDocView.props.Document.subBulletDocs = new List(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document)); } else { this.props.docs.map(d => d.addTemplate(template)); } this.props.templates.set(template, true); } else { if (template.Name == "Bullet") { - this.props.docs[0].removeTemplate(template); - this.props.docs[0].props.Document.maximizedDocs = undefined; + let topDocView = this.props.docs[0]; + topDocView.removeTemplate(template); + topDocView.props.Document.subBulletDocs = undefined; } else { this.props.docs.map(d => d.removeTemplate(template)); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 3e8a8a442..737ffba7d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,12 +1,12 @@ .collectionfreeformlinkview-linkLine { stroke: black; - stroke-width: 3; transform: translate(10000px,10000px); + opacity: 0.5; pointer-events: all; } .collectionfreeformlinkview-linkCircle { - stroke: black; - stroke-width: 3; + stroke: rgb(0,0,0); + opacity: 0.5; transform: translate(10000px,10000px); pointer-events: all; -} \ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 3b700b053..63d2f7642 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -47,11 +47,11 @@ export class CollectionFreeFormLinkView extends React.Component - + ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index cbfbb1d2c..1d4584cfe 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -84,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component d).map(d => d as Doc). filter(child => child[Id] === collid).map(view => DocumentManager.Instance.getDocumentViews(view).map(view => diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 6057aaeba..d8855fe66 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -96,7 +96,8 @@ export class MarqueeView extends React.Component document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); - e.stopPropagation(); + // bcz: do we need this? it kills the context menu on the main collection + // e.stopPropagation(); } if (e.altKey) { e.preventDefault(); @@ -228,14 +229,14 @@ export class MarqueeView extends React.Component newCollection.proto!.summaryDoc = summary; selected = [newCollection]; } - summary.proto!.maximizedDocs = new List(selected); + summary.proto!.summarizedDocs = new List(selected); summary.proto!.isButton = true; - selected.map(maximizedDoc => { - let maxx = NumCast(maximizedDoc.x, undefined); - let maxy = NumCast(maximizedDoc.y, undefined); - let maxw = NumCast(maximizedDoc.width, undefined); - let maxh = NumCast(maximizedDoc.height, undefined); - maximizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) + selected.map(summarizedDoc => { + let maxx = NumCast(summarizedDoc.x, undefined); + let maxy = NumCast(summarizedDoc.y, undefined); + let maxw = NumCast(summarizedDoc.width, undefined); + let maxh = NumCast(summarizedDoc.height, undefined); + summarizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) }); this.props.addLiveTextDocument(summary); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 470c0c2f8..92033ea44 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -6,7 +6,7 @@ import "./DocumentView.scss"; import React = require("react"); import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc"; @@ -65,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); - toggleMinimized = () => this.toggleIcon(); + toggleMinimized = async () => this.toggleIcon(await DocListCast(this.props.Document.maximizedDocs)); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()).scale(1 / this.zoom) @@ -126,10 +126,9 @@ export class CollectionFreeFormDocumentView extends DocComponent => { + public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise => { SelectionManager.DeselectAll(); let isMinimized: boolean | undefined; - 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); @@ -177,8 +176,10 @@ export class CollectionFreeFormDocumentView extends DocComponent this.props.addDocument!(await maxDoc, false)); - this.toggleIcon(); + this.toggleIcon(maximizedDocs); } } } @@ -230,7 +231,7 @@ export class CollectionFreeFormDocumentView extends DocComponent(Docu handlers: { drop: this.drop.bind(this) } }); } + // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], async () => { let maxDoc = await DocListCast(this.props.Document.maximizedDocs); @@ -142,10 +143,11 @@ export class DocumentView extends DocComponent(Docu e.stopPropagation(); } - startDragging(x: number, y: number, dropAction: dropActionType) { + startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { if (this._mainCont.current) { + let allConnected = dragSubBullets ? [this.props.Document, ...Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc)] : [this.props.Document]; const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData([this.props.Document]); + let dragData = new DragManager.DocumentDragData(allConnected); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.xOffset = xoff; @@ -167,15 +169,17 @@ export class DocumentView extends DocComponent(Docu SelectionManager.SelectDoc(this, e.ctrlKey); } } + _hitIsBullet = false; onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } + this._hitIsBullet = (e.target && (e.target as any).id === "isBullet"); if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined); + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitIsBullet); } else if (this.props.Document) { CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); } @@ -193,7 +197,7 @@ export class DocumentView extends DocComponent(Docu document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitIsBullet); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers -- cgit v1.2.3-70-g09d2 From 7da76d2538ebde21d7a878b5096d5a673e5d6375 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Fri, 10 May 2019 23:01:08 -0400 Subject: added summary screen grab icon as a template view of a text document. converted Cast(...ListSpec(Doc)) pattern to DocListCast --- src/client/northstar/dash-nodes/HistogramBox.tsx | 10 +++---- src/client/util/DocumentManager.ts | 6 ++-- src/client/util/DragManager.ts | 6 ++-- src/client/views/PresentationView.tsx | 11 +++++-- src/client/views/Templates.tsx | 35 +++++++++++++++------- .../views/collections/CollectionBaseView.tsx | 8 ++--- .../views/collections/CollectionSchemaView.tsx | 6 ++-- src/client/views/collections/CollectionSubView.tsx | 4 +-- .../views/collections/CollectionTreeView.tsx | 22 +++++++------- .../CollectionFreeFormLinksView.tsx | 12 ++++---- .../collections/collectionFreeForm/MarqueeView.tsx | 24 +++++++++++---- .../views/nodes/CollectionFreeFormDocumentView.tsx | 28 +++++++++-------- src/client/views/nodes/DocumentView.tsx | 23 +++++++------- src/client/views/nodes/IconBox.tsx | 4 +-- src/client/views/nodes/ImageBox.tsx | 14 ++++++--- src/client/views/nodes/LinkMenu.tsx | 6 ++-- src/new_fields/Doc.ts | 10 +++++-- 17 files changed, 137 insertions(+), 92 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index 5e7b867b3..ed556cf45 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -19,7 +19,7 @@ import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives"; import { StyleConstants } from "../utils/StyleContants"; import { NumCast, Cast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/RefField"; @@ -117,15 +117,15 @@ export class HistogramBox extends React.Component { runInAction(() => { this.HistoOp = histoOp ? histoOp.HistoOp : HistogramOperation.Empty; if (this.HistoOp !== HistogramOperation.Empty) { - reaction(() => Cast(this.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); - reaction(() => Cast(this.props.Document.brushingDocs, listSpec(Doc), []).length, + reaction(() => DocListCast(this.props.Document.linkedFromDocs), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); + reaction(() => DocListCast(this.props.Document.brushingDocs).length, () => { - let brushingDocs = Cast(this.props.Document.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let brushingDocs = DocListCast(this.props.Document.brushingDocs); const proto = this.props.Document.proto; if (proto) { this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => { brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]; - let brushed = Cast(brush.brushingDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let brushed = DocListCast(brush.brushingDocs); return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] }; })); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index a8b643d4d..9a7a94228 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,6 +1,6 @@ import { computed, observable } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; -import { Doc } from '../../new_fields/Doc'; +import { Doc, DocListCast } from '../../new_fields/Doc'; import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; @@ -73,7 +73,7 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { - let linksList = Cast(dv.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let linksList = DocListCast(dv.props.Document.linkedToDocs); if (linksList && linksList.length) { pairs.push(...linksList.reduce((pairs, link) => { if (link) { @@ -86,7 +86,7 @@ export class DocumentManager { return pairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); } - linksList = Cast(dv.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + linksList = DocListCast(dv.props.Document.linkedFromDocs); if (linksList && linksList.length) { pairs.push(...linksList.reduce((pairs, link) => { if (link) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c0402f0c9..2da0d5b51 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,5 +1,5 @@ import { action, runInAction } from "mobx"; -import { Doc, DocListCast } from "../../new_fields/Doc"; +import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { Cast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -43,8 +43,8 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n let draggedDocs: Doc[] = []; let draggedFromDocs: Doc[] = [] if (srcTarg) { - let linkToDocs = await DocListCast(srcTarg.linkedToDocs); - let linkFromDocs = await DocListCast(srcTarg.linkedFromDocs); + let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs); + let linkFromDocs = await DocListCastAsync(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); } diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx index 4853eb151..3fb24a339 100644 --- a/src/client/views/PresentationView.tsx +++ b/src/client/views/PresentationView.tsx @@ -141,8 +141,15 @@ export class PresentationView extends React.Component { (activeW) => { if (activeW && activeW instanceof Doc) { PromiseValue(Cast(activeW.presentationView, Doc)). - then(pv => runInAction(() => - self.Document = pv ? pv : (activeW.presentationView = new Doc()))) + then(pv => runInAction(() => { + if (pv) self.Document = pv; + else { + pv = new Doc(); + pv.title = "Presentation Doc"; + activeW.presentationView = pv; + self.Document = pv; + } + })) } }, { fireImmediately: true }); diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index bbedc95f1..a98870b04 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -5,6 +5,7 @@ export enum TemplatePosition { InnerBottom, InnerRight, InnerLeft, + TopRight, OutterTop, OutterBottom, OutterRight, @@ -39,30 +40,42 @@ export namespace Templates { // export const BasicLayout = new Template("Basic layout", "{layout}"); export const Caption = new Template("Caption", TemplatePosition.OutterBottom, - `
          ` - ); + `
          + +
          ` ); + export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop, - `
          {layout}
          -
          - {props.Document.title} -
          ` - ); + `
          +
          {layout}
          +
          + {props.Document.title} +
          +
          ` ); + export const Title = new Template("Title", TemplatePosition.InnerTop, `
          {layout}
          {props.Document.title} -
          ` - ); + ` ); export const Bullet = new Template("Bullet", TemplatePosition.InnerTop, `
          {layout}
          -
          - +
          ` ); + export function ImageOverlay(width: number, height: number, field: string = "thumbnail") { + return (`
          +
          {layout}
          +
          + +
          +
          `); + } + export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet]; export function sortTemplates(a: Template, b: Template) { diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 2b1f7bb37..645296d27 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; -import { Doc, FieldResult, Opt } from '../../../new_fields/Doc'; +import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc'; import { listSpec } from '../../../new_fields/Schema'; import { List } from '../../../new_fields/List'; import { Id } from '../../../new_fields/RefField'; @@ -63,13 +63,13 @@ export class CollectionBaseView extends React.Component { if (!(documentToAdd instanceof Doc)) { return false; } - let data = Cast(documentToAdd.data, listSpec(Doc), []).filter(d => d).map(d => d as Doc); - for (const doc of data.filter(d => d instanceof Document)) { + let data = DocListCast(documentToAdd.data); + for (const doc of data) { if (this.createsCycle(doc, containerDocument)) { return true; } } - let annots = Cast(documentToAdd.annotations, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let annots = DocListCast(documentToAdd.annotations); for (const annot of annots) { if (this.createsCycle(annot, containerDocument)) { return true; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index ae949b2ed..506e60a65 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, DocListCast } from "../../../new_fields/Doc"; +import { Opt, Field, Doc, DocListCastAsync, 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"; @@ -118,7 +118,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } const run = script.run; //TODO This should be able to be refactored to compile the script once - const val = await DocListCast(this.props.Document[this.props.fieldKey]) + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]) val && val.forEach(doc => applyToDoc(doc, run)); }}> @@ -276,7 +276,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } get documentKeysCheckList() { - const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(d => d).map(d => d as Doc); + const docs = DocListCast(this.props.Document[this.props.fieldKey]); let keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 0b08e150a..a86d250bd 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -10,7 +10,7 @@ import * as rp from 'request-promise'; import { CollectionView } from "./CollectionView"; import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; -import { Doc, Opt, FieldResult } from "../../../new_fields/Doc"; +import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types"; @@ -49,7 +49,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { get children() { //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc)).map(doc => doc as Doc); + return DocListCast(this.props.Document[this.props.fieldKey]); } @action diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 33787f06b..78c84cc89 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -18,9 +18,7 @@ import { Main } from '../Main'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; -import { Utils } from '../../../Utils'; import { List } from '../../../new_fields/List'; -import { indexOf } from 'typescript-collections/dist/lib/arrays'; export interface TreeViewProps { @@ -155,16 +153,21 @@ class TreeView extends React.Component { let keys = Array.from(Object.keys(this.props.document)); if (this.props.document.proto instanceof Doc) { keys.push(...Array.from(Object.keys(this.props.document.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } keys.map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - if (docList instanceof List && docList.length && docList[0] instanceof Doc) { + let docList = DocListCast(this.props.document[key]); + let doc = Cast(this.props.document[key], Doc); + if (doc instanceof Doc || docList.length) { if (!this._collapsed) { bulletType = BulletType.Collapsible; + let spacing = (key === "data") ? 0 : -10; contentElement.push(
            {(key === "data") ? (null) : - {key}} - {TreeView.GetChildElements(docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} + {key}} +
            + {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} +
          ); } else bulletType = BulletType.Collapsed; @@ -179,7 +182,7 @@ class TreeView extends React.Component { ; } - public static GetChildElements(docs: (Doc | Promise)[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { + public static GetChildElements(docs: 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 => ); } @@ -203,12 +206,11 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } render() { - const children = this.children; let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!children) { + if (!this.children) { return (null); } - let childElements = TreeView.GetChildElements(children, false, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); return (
          { - let doclist = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); - return { doclist: doclist ? doclist : [], xs: doclist instanceof List ? doclist.map(d => d instanceof Doc && d.x) : [] }; + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); + return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) }; }, - async () => { - let doclist = await DocListCast(this.props.Document[this.props.fieldKey]); + () => { + let doclist = DocListCast(this.props.Document[this.props.fieldKey]); let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : []; views.forEach((dstDoc, i) => { views.forEach((srcDoc, j) => { @@ -84,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component d).map(d => d as Doc). + DocListCast(this.props.Document[this.props.fieldKey]). filter(child => child[Id] === collid).map(view => DocumentManager.Instance.getDocumentViews(view).map(view => diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1bf39e335..9ace0272a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,3 +1,4 @@ +import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Docs } from "../../../documents/Documents"; @@ -14,6 +15,8 @@ import { Doc } from "../../../../new_fields/Doc"; import { NumCast, Cast } from "../../../../new_fields/Types"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; +import { ImageField } from "../../../../new_fields/URLField"; +import { Template, Templates } from "../../Templates"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -30,6 +33,7 @@ interface MarqueeViewProps { @observer export class MarqueeView extends React.Component { + private _mainCont = React.createRef(); @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @@ -166,7 +170,7 @@ export class MarqueeView extends React.Component @undoBatch @action - marqueeCommand = (e: KeyboardEvent) => { + marqueeCommand = async (e: KeyboardEvent) => { if (this._commandExecuted) { return; } @@ -224,13 +228,17 @@ export class MarqueeView extends React.Component 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-" }); + let dataUrl = await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width, height: bounds.height, quality: 1 }); + summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); + + summary.proto!.templates = new List([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); if (e.key === "s" || e.key === "p") { summary.proto!.maximizeOnRight = true; newCollection.proto!.summaryDoc = summary; selected = [newCollection]; } summary.proto!.summarizedDocs = new List(selected); - summary.proto!.isButton = true; + //summary.proto!.isButton = true; selected.map(summarizedDoc => { let maxx = NumCast(summarizedDoc.x, undefined); let maxy = NumCast(summarizedDoc.y, undefined); @@ -313,17 +321,21 @@ export class MarqueeView extends React.Component @computed get marqueeDiv() { - let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return
          + return
          ; } render() { + let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); return
          - {this.props.children} - {!this._visible ? (null) : this.marqueeDiv} +
          +
          + {this.props.children} +
          + {!this._visible ? null : this.marqueeDiv} +
          ; } } \ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index b05f2eea2..39d216da0 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -9,10 +9,10 @@ import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schem import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, DocListCast, } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { UndoManager } from "../../util/UndoManager"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -65,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); - toggleMinimized = async () => this.toggleIcon(await DocListCast(this.props.Document.maximizedDocs)); + toggleMinimized = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs)); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()).scale(1 / this.zoom) @@ -132,7 +132,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { + expandedDocs.forEach(maxDoc => { maxDoc.isMinimized = false; if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) { CollectionDockingView.Instance.AddRightSplit(maxDoc); @@ -195,8 +197,8 @@ export class CollectionFreeFormDocumentView extends DocComponent this.props.addDocument!(await maxDoc, false)); - this.toggleIcon(maximizedDocs); + this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(await maxDoc, false)); + this.toggleIcon(expandedDocs); } } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5aa74c703..90f67db7c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,13 +12,13 @@ import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; -import { Template, Templates } from "./../Templates"; +import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { Opt, Doc, WidthSym, HeightSym, DocListCast } from "../../../new_fields/Doc"; +import { Opt, Doc, WidthSym, HeightSym, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -109,8 +109,8 @@ export class DocumentView extends DocComponent(Docu } // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], - async () => { - let maxDoc = await DocListCast(this.props.Document.maximizedDocs); + () => { + let maxDoc = 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" : ""); } @@ -145,7 +145,7 @@ export class DocumentView extends DocComponent(Docu startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { if (this._mainCont.current) { - let allConnected = dragSubBullets ? [this.props.Document, ...Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc)] : [this.props.Document]; + let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData(allConnected); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); @@ -169,17 +169,17 @@ export class DocumentView extends DocComponent(Docu SelectionManager.SelectDoc(this, e.ctrlKey); } } - _hitIsBullet = false; + _hitExpander = false; onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - this._hitIsBullet = (e.target && (e.target as any).id === "isBullet") || Cast(this.props.Document.subBulletDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc).length > 0; + this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitIsBullet); + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander); } else if (this.props.Document) { CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); } @@ -197,7 +197,7 @@ export class DocumentView extends DocComponent(Docu document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitIsBullet); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -232,6 +232,7 @@ export class DocumentView extends DocComponent(Docu ContextMenu.Instance.clearItems(); SelectionManager.DeselectAll(); } + @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { @@ -259,7 +260,6 @@ export class DocumentView extends DocComponent(Docu } } - @action addTemplate = (template: Template) => { this.templates.push(template.Layout); @@ -307,7 +307,6 @@ export class DocumentView extends DocComponent(Docu } } - isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 4bcb4c636..48aecdcd0 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -2,12 +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 { computed, observable, runInAction, reaction, IReactionDisposer } from "mobx"; +import { computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { FieldView, FieldViewProps } from './FieldView'; import "./IconBox.scss"; import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { IconField } from "../../../new_fields/IconField"; import { ContextMenu } from "../ContextMenu"; import Measure from "react-measure"; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0e9e904a8..f9659a4b2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,5 @@ -import { action, observable } from 'mobx'; +import { action, observable, trace } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app @@ -157,15 +157,21 @@ export class ImageBox extends DocComponent(ImageD } render() { + trace(); let field = this.Document[this.props.fieldKey]; let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; if (field instanceof ImageField) paths = [field.url.href]; else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href); - let nativeWidth = FieldValue(this.Document.nativeWidth, 1); + let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50); let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; + let id = this.props.id; return ( -
          - Image not found +
          + {paths.length > 1 ? this.dots(paths) : (null)} {this.lightbox(paths)}
          ); diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 24901913d..7bf13d5f9 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -5,7 +5,7 @@ import { LinkBox } from "./LinkBox"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Cast, FieldValue } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { Id } from "../../../new_fields/RefField"; @@ -31,8 +31,8 @@ export class LinkMenu extends React.Component { render() { //get list of links from document - let linkFrom = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); - let linkTo = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []).filter(d => d).map(d => d as Doc); + let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs); + let linkTo = DocListCast(this.props.docView.props.Document.linkedToDoc); if (this._editingLink === undefined) { return (
          diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 2ab145fa3..46ccb3e90 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -29,13 +29,17 @@ export const SelfProxy = Symbol("SelfProxy"); export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); -export function DocListCast(field: FieldResult): Promise; -export function DocListCast(field: FieldResult, defaultValue: Doc[]): Promise; -export function DocListCast(field: FieldResult, defaultValue?: Doc[]) { +export function DocListCastAsync(field: FieldResult): Promise; +export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise; +export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { const list = Cast(field, listSpec(Doc)); return list ? Promise.all(list) : Promise.resolve(defaultValue); } +export function DocListCast(field: FieldResult) { + return Cast(field, listSpec(Doc), []).filter(d => d && d instanceof Doc).map(d => d as Doc) +} + @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { constructor(id?: FieldId, forceSave?: boolean) { -- cgit v1.2.3-70-g09d2 From b9d32e8e07137484c57f3d1ab89090b27d56cfe6 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 12 May 2019 21:52:04 -0400 Subject: finally fixed disappearing videobox and changed marquee div structure --- src/client/views/collections/CollectionTreeView.tsx | 5 +++++ src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 8 ++++---- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- 3 files changed, 10 insertions(+), 5 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 9bd32a89f..d22418b2c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -19,6 +19,7 @@ import { CurrentUserUtils } from '../../../server/authentication/models/current_ import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; import { List } from '../../../new_fields/List'; +import { Docs } from '../../documents/Documents'; export interface TreeViewProps { @@ -141,6 +142,10 @@ class TreeView extends React.Component { if (!e.isPropagationStopped() && this.props.document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) }); + ContextMenu.Instance.addItem({ + description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document, + { title: this.props.document.title + ".kvp", width: 300, height: 300 })) + }); 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)) }); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 611ee7fd6..080c484f4 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -358,7 +358,7 @@ export class MarqueeView extends React.Component @computed get marqueeDiv() { let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - return
          + return
          ; } @@ -366,11 +366,11 @@ export class MarqueeView extends React.Component render() { let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); return
          -
          -
          +
          + {!this._visible ? null : this.marqueeDiv} +
          {this.props.children}
          - {!this._visible ? null : this.marqueeDiv}
          ; } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 39d216da0..817a23ce8 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -228,7 +228,7 @@ export class CollectionFreeFormDocumentView extends DocComponent fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; return (
          Date: Mon, 13 May 2019 10:56:12 -0400 Subject: small fixes --- src/client/util/DocumentManager.ts | 10 +++++++--- src/client/views/InkingControl.tsx | 9 +++------ .../views/collections/CollectionDockingView.tsx | 22 +++++++++++++++------- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 6 ++---- src/new_fields/Doc.ts | 12 +++++++++++- 6 files changed, 39 insertions(+), 22 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 9a7a94228..47bcb153f 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -5,6 +5,7 @@ import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { Id } from '../../new_fields/RefField'; export class DocumentManager { @@ -26,13 +27,13 @@ export class DocumentManager { // this.DocumentViews = new Array(); } - public getDocumentView(toFind: Doc): DocumentView | null { + public getDocumentViewById(id: string): DocumentView | null { let toReturn: DocumentView | null = null; //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document === toFind) { + if (view.props.Document[Id] === id) { toReturn = view; return; } @@ -40,7 +41,7 @@ export class DocumentManager { if (!toReturn) { DocumentManager.Instance.DocumentViews.map(view => { let doc = view.props.Document.proto; - if (doc && Object.is(doc, toFind)) { + if (doc && doc.Id === id) { toReturn = view; } }); @@ -48,6 +49,9 @@ export class DocumentManager { return toReturn; } + + public getDocumentView(toFind: Doc): DocumentView | null { return this.getDocumentViewById(toFind[Id]); } + public getDocumentViews(toFind: Doc): DocumentView[] { let toReturn: DocumentView[] = []; diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 4b3dbd4e0..17d4a1e49 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -35,12 +35,9 @@ export class InkingControl extends React.Component { @action switchColor = (color: ColorResult): void => { this._selectedColor = color.hex; - if (SelectionManager.SelectedDocuments().length === 1) { - var sdoc = SelectionManager.SelectedDocuments()[0]; - if (sdoc.props.ContainingCollectionView) { - Doc.SetOnPrototype(sdoc.props.Document, "backgroundColor", color.hex); - } - } + SelectionManager.SelectedDocuments().forEach(doc => + doc.props.ContainingCollectionView && Doc.SetOnPrototype(doc.props.Document, "backgroundColor", color.hex) + ); } @action diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 8739a213f..0aa067972 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -19,6 +19,7 @@ import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); import { ParentDocSelector } from './ParentDocumentSelector'; +import { DocumentManager } from '../../util/DocumentManager'; @observer export class CollectionDockingView extends React.Component { @@ -73,26 +74,33 @@ export class CollectionDockingView extends React.Component { + retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - child.contentItems[0].config.props.documentId == document[Id]) { + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { child.contentItems[0].remove(); this.layoutChanged(document); - this.stateChanged(); + return true; } else - child.contentItems.map((tab: any, j: number) => { - if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); let docs = Cast(this.props.Document.data, listSpec(Doc)); docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); - this.stateChanged(); + return true; } + return false; }); + return false; }) } + if (retVal) { + this.stateChanged(); + } + return retVal; } @action diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 080c484f4..1c29ebaae 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -79,7 +79,7 @@ export class MarqueeView extends React.Component y += 40 * this.props.getTransform().Scale; }) })(); - } else if (e.key === "t" && e.ctrlKey) { + } else if (e.key === "b" && e.ctrlKey) { //heuristically converts pasted text into a table. // assumes each entry is separated by a tab // skips all rows until it gets to a row with more than one entry diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 817a23ce8..2a041bf70 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -189,10 +189,8 @@ export class CollectionFreeFormDocumentView extends DocComponent { maxDoc.isMinimized = false; - if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) { - CollectionDockingView.Instance.AddRightSplit(maxDoc); - } else { - CollectionDockingView.Instance.CloseRightSplit(maxDoc); + if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { + CollectionDockingView.Instance.AddRightSplit(Doc.MakeCopy(maxDoc)); } }); } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 625ba0d6a..a8c9d28e1 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -134,7 +134,8 @@ export namespace Doc { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult; } export async function SetOnPrototype(doc: Doc, key: string, value: Field) { - const proto = doc.proto; + const proto = Object.getOwnPropertyNames(doc).indexOf("isProto") == -1 ? doc.proto : doc; + if (proto) { proto[key] = value; } @@ -160,6 +161,15 @@ export namespace Doc { return doc; } + // compare whether documents or their protos match + export function AreProtosEqual(doc: Doc, other: Doc) { + let r = (doc[Id] === other[Id]); + let r2 = (doc.proto && doc.proto.Id === other[Id]); + let r3 = (other.proto && other.proto.Id === doc[Id]); + let r4 = (doc.proto && other.proto && doc.proto[Id] === other.proto[Id]); + return r || r2 || r3 || r4 ? true : false; + } + export function MakeAlias(doc: Doc) { const alias = new Doc; -- cgit v1.2.3-70-g09d2 From 75496b6ef50206f78e23ce998b7c932d06f23239 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Tue, 14 May 2019 23:19:54 -0400 Subject: changed Make Button to work with links --- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 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 2a041bf70..2d4900509 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -13,6 +13,7 @@ import { Doc, DocListCastAsync, DocListCast, } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { UndoManager } from "../../util/UndoManager"; +import { DocumentManager } from "../../util/DocumentManager"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -181,7 +182,11 @@ export class CollectionFreeFormDocumentView extends DocComponent this.props.addDocument!(await maxDoc, false)); + this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); this.toggleIcon(expandedDocs); } } + let linkedDocs = [ + ...(linkedFromDocs ? linkedFromDocs.map(l => l.linkedFrom as Doc) : []), + ...(linkedToDocs ? linkedToDocs.map(l => l.linkedTo as Doc) : [])]; + if (linkedDocs) { + DocumentManager.Instance.jumpToDocument(linkedDocs[0]); + } } } } -- cgit v1.2.3-70-g09d2 From 4ed116991b86e4667fd78eb9ea034e3a9cc75a3e Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 15 May 2019 00:42:35 -0400 Subject: small fixes to start allowing trails of links. --- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 10 +++++----- src/client/views/nodes/DocumentView.tsx | 4 ++-- src/client/views/nodes/FormattedTextBox.tsx | 2 +- 3 files changed, 8 insertions(+), 8 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 2d4900509..fc1dc2b1e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -204,11 +204,11 @@ export class CollectionFreeFormDocumentView extends DocComponent l.linkedFrom as Doc) : []), - ...(linkedToDocs ? linkedToDocs.map(l => l.linkedTo as Doc) : [])]; - if (linkedDocs) { - DocumentManager.Instance.jumpToDocument(linkedDocs[0]); + let linkedFwdDocs = [ + linkedToDocs ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + linkedFromDocs ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + if (linkedFwdDocs) { + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0]); } } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c97bcbd94..b88d18046 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -221,8 +221,8 @@ export class DocumentView extends DocComponent(Docu let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; doc.isButton = !BoolCast(doc.isButton, false); if (doc.isButton && !doc.nativeWidth) { - doc.nativeWidth = doc[WidthSym](); - doc.nativeHeight = doc[HeightSym](); + doc.nativeWidth = this.props.Document[WidthSym](); + doc.nativeHeight = this.props.Document[HeightSym](); } } fullScreenClicked = (e: React.MouseEvent): void => { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 7ebc31b0a..cb09ad42c 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -229,7 +229,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.target && (e.target as any).href) { let href = (e.target as any).href; if (href.indexOf(DocServer.prepend("/doc/")) === 0) { - let docid = href.replace(DocServer.prepend("/doc/"), ""); + let docid = href.replace(DocServer.prepend("/doc/"), "").split("%")[0]; DocServer.GetRefField(docid).then(action((f: Opt) => { if (f instanceof Doc) { if (DocumentManager.Instance.getDocumentView(f)) -- cgit v1.2.3-70-g09d2 From dae85deb62168cbfae4557aa8632896592d71cf9 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 15 May 2019 09:10:41 -0400 Subject: set hover highlight for search results --- src/client/views/SearchItem.tsx | 12 +++++++++++- src/client/views/collections/CollectionTreeView.tsx | 2 +- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 4 +++- src/new_fields/Doc.ts | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx index 0da0bdae8..01c7316d6 100644 --- a/src/client/views/SearchItem.tsx +++ b/src/client/views/SearchItem.tsx @@ -40,6 +40,14 @@ export class SearchItem extends React.Component { faCaretUp; return ; } + onPointerEnter = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = true; + Doc.SetOnPrototype(this.props.doc, "protoBrush", true); + } + onPointerLeave = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = false; + Doc.SetOnPrototype(this.props.doc, "protoBrush", false); + } collectionRef = React.createRef(); startDocDrag = () => { @@ -53,7 +61,9 @@ export class SearchItem extends React.Component { } render() { return ( -
          +
          title: {this.props.doc.title}
          {/*
          Type: {this.props.doc.layout}
          */} {/*
          {SearchItem.DocumentIcon(this.layout)}
          */} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d22418b2c..70c09d97c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -130,7 +130,7 @@ class TreeView extends React.Component {
          ); return (
          {editableView(StrCast(this.props.document.title))} {openRight} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index fc1dc2b1e..925945b17 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -247,7 +247,9 @@ export class CollectionFreeFormDocumentView extends DocComponent; } export async function SetOnPrototype(doc: Doc, key: string, value: Field) { - const proto = Object.getOwnPropertyNames(doc).indexOf("isProto") == -1 ? doc.proto : doc; + const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") == -1 ? doc.proto : doc; if (proto) { proto[key] = value; -- cgit v1.2.3-70-g09d2 From d79d721baa25947dfbbd17eda173835d9ae93fa6 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 15 May 2019 15:05:57 -0400 Subject: several smaller fixes. --- src/client/views/DocumentDecorations.tsx | 30 +++++++------ .../views/collections/ParentDocumentSelector.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 17 ++----- .../views/nodes/CollectionFreeFormDocumentView.tsx | 52 ++++++++++++---------- src/client/views/nodes/DocumentView.tsx | 8 ++-- src/client/views/nodes/FormattedTextBox.tsx | 2 +- src/client/views/nodes/IconBox.scss | 4 +- src/client/views/nodes/IconBox.tsx | 27 ++++++++--- src/new_fields/List.ts | 10 +++++ 9 files changed, 87 insertions(+), 65 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 627bedde2..8d85f7e2c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -252,18 +252,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (!this._removeIcon) { if (selectedDocs.length === 1) this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized()); - else { - let docViews = SelectionManager.ViewsSortedVertically(); - let topDocView = docViews[0]; - let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); - if (ind !== -1) { - topDocView.templates.splice(ind, 1); - topDocView.props.Document.subBulletDocs = undefined; - } else { - topDocView.addTemplate(Templates.Bullet); - topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document)); + else + if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) { + let docViews = SelectionManager.ViewsSortedVertically(); + let topDocView = docViews[0]; + let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); + if (ind !== -1) { + topDocView.templates.splice(ind, 1); + topDocView.props.Document.subBulletDocs = undefined; + } else { + topDocView.addTemplate(Templates.Bullet); + topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document)); + } } - } } this._removeIcon = false; } @@ -275,9 +277,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let doc = selected[0].props.Document; let iconDoc = Docs.IconDocument(layoutString); iconDoc.isButton = true; - iconDoc.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title); - iconDoc.labelField = this._fieldKey; - iconDoc[this._fieldKey] = selected.length > 1 ? "collection" : undefined; + iconDoc.proto!.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title); + iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey; + iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined; iconDoc.isMinimized = false; iconDoc.width = Number(MINIMIZED_ICON_SIZE); iconDoc.height = Number(MINIMIZED_ICON_SIZE); diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index dd1516da7..52f7914f3 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -18,7 +18,7 @@ export class SelectorContextMenu extends React.Component<{ Document: Doc }> { } async fetchDocuments() { - const docs = await SearchUtil.Search(`data_l:${this.props.Document[Id]}`, true); + const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); runInAction(() => this._docs = docs); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index be7cddca6..ae4852aa2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -212,6 +212,8 @@ export class MarqueeView extends React.Component } if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { this._commandExecuted = true; + e.stopPropagation(); + (e as any).propagationIsStopped = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); let ink = Cast(this.props.container.props.Document.ink, InkField); if (ink) { @@ -291,20 +293,7 @@ export class MarqueeView extends React.Component this.props.selectDocuments([newCollection]); } this.cleanupInteractions(false); - } else - if (e.key === "s") { - // this._commandExecuted = true; - // e.stopPropagation(); - // e.preventDefault(); - // let bounds = this.Bounds; - // let selected = this.marqueeSelect(); - // SelectionManager.DeselectAll(); - // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - // this.props.addLiveTextDocument(summary); - // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!)); - - // this.cleanupInteractions(false); - } + } } @action marqueeInkSelect(ink: Map) { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 925945b17..38ac5cb90 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,19 +1,19 @@ -import { computed, trace, action, reaction, IReactionDisposer } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; +import { UndoManager } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocComponent } from "../DocComponent"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; -import { OmitKeys, Utils } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCastAsync, DocListCast, } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { UndoManager } from "../../util/UndoManager"; -import { DocumentManager } from "../../util/DocumentManager"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -140,7 +140,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { + maximizedDocs.map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { if (isMinimized === undefined) { @@ -182,12 +182,16 @@ export class CollectionFreeFormDocumentView extends DocComponent(Docu this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], () => { let maxDoc = 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" : ""); + if (maxDoc.length === 1 && StrCast(maxDoc[0].title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { + this.props.Document.proto!.title = maxDoc[0].title + ".icon"; } let sumDoc = Cast(this.props.Document.summaryDoc, Doc); if (sumDoc instanceof Doc) { @@ -332,8 +332,8 @@ export class DocumentView extends DocComponent(Docu render() { var scaling = this.props.ContentScaling(); - var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%"; - var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%"; + var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto"); + var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; return (
          ) => { if (f instanceof Doc) { if (DocumentManager.Instance.getDocumentView(f)) diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss index f6d9860a3..893dc2d36 100644 --- a/src/client/views/nodes/IconBox.scss +++ b/src/client/views/nodes/IconBox.scss @@ -4,13 +4,13 @@ position: inherit; left:0; top:0; - height: 100%; + height: auto; width: max-content; // overflow: hidden; pointer-events: all; svg { width: $MINIMIZED_ICON_SIZE !important; - height: 100%; + height: auto; background: white; } .iconBox-label { diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 071930633..b42eb44a5 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { FieldView, FieldViewProps } from './FieldView'; import "./IconBox.scss"; import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { Doc } 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"; @@ -41,25 +41,40 @@ export class IconBox extends React.Component { setLabelField = (e: React.MouseEvent): void => { this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); } + setUseOwnTitleField = (e: React.MouseEvent): void => { + this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); + } specificContextMenu = (e: React.MouseEvent): void => { ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label", + description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", event: this.setLabelField }); + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) { + ContextMenu.Instance.addItem({ + description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label", + event: this.setUseOwnTitleField + }); + } } @observable _panelWidth: number = 0; @observable _panelHeight: number = 0; render() { let labelField = StrCast(this.props.Document.labelField); let hideLabel = BoolCast(this.props.Document.hideLabel); - let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []); - let firstDoc = maxDoc && maxDoc.length > 0 && maxDoc[0] instanceof Doc ? maxDoc[0] as Doc : undefined; - let label = !hideLabel && firstDoc && labelField ? firstDoc[labelField] : ""; + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + let firstDoc = maxDocs.length ? maxDocs[0] : undefined; + let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title); return (
          {this.minimizedIcon} - runInAction(() => { if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); })}> + runInAction(() => { + if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) { + this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); + if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth; + } + })}> {({ measureRef }) => {label} } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index 88a65eba4..70e36f911 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -230,6 +230,16 @@ class ListImpl extends ObjectField { const list = new Proxy(this, { set: setter, get: listGetter, + ownKeys: target => Object.keys(target.__fields), + getOwnPropertyDescriptor: (target, prop) => { + if (prop in target.__fields) { + return { + configurable: true,//TODO Should configurable be true? + enumerable: true, + }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, deleteProperty: deleteProperty, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); -- cgit v1.2.3-70-g09d2 From 09afa3cc843a69eca59b63018e6f50f8e2bee089 Mon Sep 17 00:00:00 2001 From: bob Date: Wed, 15 May 2019 16:09:47 -0400 Subject: small fixes. stopped view from panning when switching to tab dragging. --- src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 3 +-- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 9 ++++++--- src/client/views/nodes/DocumentView.tsx | 9 +++++---- 3 files changed, 12 insertions(+), 9 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index ae4852aa2..865bae729 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -257,7 +257,7 @@ export class MarqueeView extends React.Component width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, - title: "a nested collection", + title: e.key === "s" ? "-summary-" : e.key === "r" ? "-replacement-" : e.key === "p" ? "-summary-" : "a nested collection", }); this.marqueeInkDelete(inkData); @@ -283,7 +283,6 @@ export class MarqueeView extends React.Component let maxy = NumCast(summarizedDoc.y, undefined); let maxw = NumCast(summarizedDoc.width, undefined); let maxh = NumCast(summarizedDoc.height, undefined); - summarizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) }); this.props.addLiveTextDocument(summary); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 38ac5cb90..631bf1ba8 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -170,7 +170,9 @@ export class CollectionFreeFormDocumentView extends DocComponent { this._downX = e.clientX; this._downY = e.clientY; - // e.stopPropagation(); + if (e.button === 0 && e.altKey) { + e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started + } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document } onClick = async (e: React.MouseEvent) => { e.stopPropagation(); @@ -192,14 +194,15 @@ export class CollectionFreeFormDocumentView extends DocComponent { maxDoc.isMinimized = false; if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { - CollectionDockingView.Instance.AddRightSplit(Doc.MakeCopy(maxDoc)); + CollectionDockingView.Instance.AddRightSplit(maxDoc.proto ? Doc.MakeDelegate(maxDoc.proto) : maxDoc); } }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 27896f02b..260630216 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -112,12 +112,12 @@ export class DocumentView extends DocComponent(Docu this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], () => { let maxDoc = DocListCast(this.props.Document.maximizedDocs); - if (maxDoc.length === 1 && StrCast(maxDoc[0].title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { - this.props.Document.proto!.title = maxDoc[0].title + ".icon"; + if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { + this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; } let sumDoc = Cast(this.props.Document.summaryDoc, Doc); - if (sumDoc instanceof Doc) { - this.props.Document.title = sumDoc.title + ".expanded"; + if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { + this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; } }, { fireImmediately: true }); DocumentManager.Instance.DocumentViews.push(this); @@ -186,6 +186,7 @@ export class DocumentView extends DocComponent(Docu } e.stopPropagation(); } else if (this.active) { + //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); -- cgit v1.2.3-70-g09d2 From 6e767972574c9cd8a2cd1e10a8fa327839ca3525 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Thu, 16 May 2019 01:17:44 -0400 Subject: changes to make maximizing stuff apply to data documents. problem with zooiming after resizing a summarized doc --- src/client/views/DocumentDecorations.tsx | 4 ++-- src/client/views/TemplateMenu.tsx | 2 +- src/client/views/Templates.tsx | 15 ++++++------ .../collections/collectionFreeForm/MarqueeView.tsx | 8 ++++--- .../views/nodes/CollectionFreeFormDocumentView.tsx | 28 ++++++++++------------ 5 files changed, 28 insertions(+), 29 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 8d85f7e2c..b2c65a31f 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -263,7 +263,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> topDocView.props.Document.subBulletDocs = undefined; } else { topDocView.addTemplate(Templates.Bullet); - topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document)); + topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); } } } @@ -285,7 +285,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> iconDoc.height = Number(MINIMIZED_ICON_SIZE); iconDoc.x = NumCast(doc.x); iconDoc.y = NumCast(doc.y) - 24; - iconDoc.maximizedDocs = new List(selected.map(s => s.props.Document)); + iconDoc.maximizedDocs = new List(selected.map(s => s.props.Document.proto!)); doc.minimizedDoc = iconDoc; selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false); return iconDoc; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 22c4edc25..cfe1b0663 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -46,7 +46,7 @@ export class TemplateMenu extends React.Component { if (template.Name == "Bullet") { let topDocView = this.props.docs[0]; topDocView.addTemplate(template); - topDocView.props.Document.subBulletDocs = new List(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document)); + topDocView.props.Document.subBulletDocs = new List(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); } else { this.props.docs.map(d => d.addTemplate(template)); } diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index ca0c04d1b..f0ee2856b 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -62,18 +62,19 @@ export namespace Templates {
          ` ); export const Bullet = new Template("Bullet", TemplatePosition.InnerTop, - `
          {layout}
          -
          - -
          + `
          +
          {layout}
          +
          + +
          ` ); export function ImageOverlay(width: number, height: number, field: string = "thumbnail") { return (`
          -
          {layout}
          -
          +
          {layout}
          +
          `); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 865bae729..c063115e7 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -264,7 +264,6 @@ export class MarqueeView extends React.Component // SelectionManager.DeselectAll(); if (e.key === "s" || e.key === "r" || e.key === "p") { e.preventDefault(); - let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); let dataUrl = await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width, height: bounds.height, quality: 1 }); @@ -272,12 +271,15 @@ export class MarqueeView extends React.Component summary.proto!.templates = new List([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); if (e.key === "s" || e.key === "p") { - summary.proto!.maximizeOnRight = true; + // summary.proto!.maximizeOnRight = true; newCollection.proto!.summaryDoc = summary; selected = [newCollection]; + newCollection.proto!.isMinimized = true; + this.props.addDocument(newCollection, false); } - summary.proto!.summarizedDocs = new List(selected); + summary.proto!.summarizedDocs = new List(selected.map(s => s.proto!)); //summary.proto!.isButton = true; + let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); selected.map(summarizedDoc => { let maxx = NumCast(summarizedDoc.x, undefined); let maxy = NumCast(summarizedDoc.y, undefined); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 631bf1ba8..03538ae66 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -87,7 +87,8 @@ export class CollectionFreeFormDocumentView extends DocComponent { let now = Date.now(); @@ -115,13 +116,13 @@ export class CollectionFreeFormDocumentView extends DocComponent([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0]) + maximizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]) } } }); @@ -194,7 +190,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { maxDoc.isMinimized = false; if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { - CollectionDockingView.Instance.AddRightSplit(maxDoc.proto ? Doc.MakeDelegate(maxDoc.proto) : maxDoc); + CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(maxDoc)); } }); } } else { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); + //if (altKey) this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); this.toggleIcon(expandedDocs); } } -- cgit v1.2.3-70-g09d2 From 22d9a6540f2994331c397154f6a1fecd6e46ef8c Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 16 May 2019 11:19:50 -0400 Subject: fixed glitch in unminimizing --- .../views/nodes/CollectionFreeFormDocumentView.tsx | 51 ++++++++++------------ 1 file changed, 24 insertions(+), 27 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 03538ae66..b82d02af5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -20,7 +20,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { const schema = createSchema({ zoomBasis: "number", - zIndex: "number" + zIndex: "number", }); //TODO Types: The import order is wrong, so positionSchema is undefined @@ -43,9 +43,8 @@ export class CollectionFreeFormDocumentView extends DocComponent this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); @@ -87,8 +82,8 @@ export class CollectionFreeFormDocumentView extends DocComponent { let now = Date.now(); let progress = Math.min(1, (now - stime) / 200); let pval = maximizing ? [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; - target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - target.x = pval[0]; - target.y = pval[1]; + this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + this.props.Document.x = pval[0]; + this.props.Document.y = pval[1]; + if (first) { + this.props.Document.proto!.willMaximize = false; + } if (now < stime + 200) { - this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing); + this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); } else { if (!maximizing) { - target.proto!.isMinimized = true; - target.x = targ[0]; - target.y = targ[1]; - target.width = width; - target.height = height; + this.props.Document.proto!.isMinimized = true; + this.props.Document.x = targ[0]; + this.props.Document.y = targ[1]; + this.props.Document.width = width; + this.props.Document.height = height; } - target.proto!.isIconAnimating = undefined; + this.props.Document.proto!.isIconAnimating = undefined; } }, 2); @@ -151,7 +147,8 @@ export class CollectionFreeFormDocumentView extends DocComponent([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]) } } @@ -263,7 +260,7 @@ export class CollectionFreeFormDocumentView extends DocComponent {this.docView} -- cgit v1.2.3-70-g09d2 From 5c7926ebe144644814043c43c0cdc2cf8866e4cd Mon Sep 17 00:00:00 2001 From: bob Date: Thu, 16 May 2019 11:36:15 -0400 Subject: fixed highlighting thickness as much as possible. seems to be a minimum width of around 0.1 which is a problem zooming in. --- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 b82d02af5..7c7ca9e25 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -247,11 +247,11 @@ export class CollectionFreeFormDocumentView extends DocComponent Date: Thu, 16 May 2019 18:32:57 -0400 Subject: Fixed linter errors --- src/client/goldenLayout.d.ts | 3 +++ src/client/northstar/dash-fields/HistogramField.ts | 2 +- src/client/northstar/model/ModelHelpers.ts | 9 +++---- src/client/util/DragManager.ts | 2 +- src/client/util/RichTextSchema.tsx | 16 +++++------ src/client/util/TooltipTextMenu.tsx | 13 ++++----- src/client/views/DocumentDecorations.tsx | 31 +++++++++++----------- src/client/views/PresentationView.tsx | 4 +-- src/client/views/TemplateMenu.tsx | 4 +-- .../views/collections/CollectionDockingView.tsx | 5 ++-- .../views/collections/CollectionSchemaView.tsx | 10 +++---- .../views/collections/CollectionTreeView.tsx | 9 ++++--- .../collections/collectionFreeForm/MarqueeView.tsx | 13 ++++----- .../views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FormattedTextBox.tsx | 8 +++--- src/client/views/nodes/ImageBox.tsx | 2 +- src/client/views/nodes/KeyValueBox.tsx | 2 +- src/client/views/nodes/KeyValuePair.tsx | 3 ++- src/client/views/nodes/VideoBox.tsx | 22 +++++++-------- src/new_fields/CursorField.ts | 6 ++--- src/new_fields/Doc.ts | 4 +-- src/new_fields/ObjectField.ts | 2 +- .../authentication/models/current_user_utils.ts | 2 +- 24 files changed, 91 insertions(+), 85 deletions(-) create mode 100644 src/client/goldenLayout.d.ts (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/goldenLayout.d.ts b/src/client/goldenLayout.d.ts new file mode 100644 index 000000000..b50240563 --- /dev/null +++ b/src/client/goldenLayout.d.ts @@ -0,0 +1,3 @@ + +declare const GoldenLayout: any; +export = GoldenLayout; \ No newline at end of file diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts index aabc77bb2..1ee2189b9 100644 --- a/src/client/northstar/dash-fields/HistogramField.ts +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -52,7 +52,7 @@ export class HistogramField extends ObjectField { [Copy]() { let y = this.HistoOp; - let z = this.HistoOp["Copy"]; + let z = this.HistoOp.Copy; return new HistogramField(HistogramOperation.Duplicate(this.HistoOp)); } } \ No newline at end of file diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts index 80bb71224..88e6e72b8 100644 --- a/src/client/northstar/model/ModelHelpers.ts +++ b/src/client/northstar/model/ModelHelpers.ts @@ -32,12 +32,9 @@ export class ModelHelpers { public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number { return Array.from(histogramResult.aggregateParameters!).findIndex((value, i, set) => { - if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters) - return true; - if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters) - return true; - if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters) - return true; + if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters) return true; + if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters) return true; + if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters) return true; return false; }); } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 26da34e67..05eb5c38a 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -41,7 +41,7 @@ export function SetupDrag(_reference: React.RefObject, docFunc: () export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) { let srcTarg = sourceDoc.proto; let draggedDocs: Doc[] = []; - let draggedFromDocs: Doc[] = [] + let draggedFromDocs: Doc[] = []; if (srcTarg) { let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs); let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs); diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index c0e6f7899..3e3e98206 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -97,13 +97,13 @@ export const nodes: { [index: string]: NodeSpec } = { title: dom.getAttribute("title"), alt: dom.getAttribute("alt"), width: Math.min(100, Number(dom.getAttribute("width"))), - } + }; } }], // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why? toDOM(node) { - const attrs = { style: `width: ${node.attrs.width}` } - return ["img", { ...node.attrs, ...attrs }] + const attrs = { style: `width: ${node.attrs.width}` }; + return ["img", { ...node.attrs, ...attrs }]; } }, @@ -375,7 +375,7 @@ export class ImageResizeView { const currentX = e.pageX; const diffInPx = currentX - startX; self._outer.style.width = `${startWidth + diffInPx}`; - } + }; const onpointerup = () => { document.removeEventListener("pointermove", onpointermove); @@ -384,11 +384,11 @@ export class ImageResizeView { view.state.tr.setNodeMarkup(getPos(), null, { src: node.attrs.src, width: self._outer.style.width }) .setSelection(view.state.selection)); - } + }; - document.addEventListener("pointermove", onpointermove) - document.addEventListener("pointerup", onpointerup) - } + document.addEventListener("pointermove", onpointermove); + document.addEventListener("pointerup", onpointerup); + }; this._outer.appendChild(this._handle); this._outer.appendChild(this._img); diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 6eb654319..223921428 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -175,7 +175,7 @@ export class TooltipTextMenu { this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; this.linkText.style.color = "white"; - this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); } + this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; linkBtn.style.width = "20px"; @@ -192,8 +192,9 @@ export class TooltipTextMenu { let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); @@ -201,7 +202,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.linkDrag = document.createElement("img"); this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png"; this.linkDrag.style.width = "20px"; @@ -216,12 +217,12 @@ export class TooltipTextMenu { { handlers: { dragComplete: action(() => { - let m = dragData.droppedDocuments as Doc[]; + let m = dragData.droppedDocuments; this.makeLink(DocServer.prepend("/doc/" + m[0][Id])); }), }, hideSource: false - }) + }); }; this.linkEditor.appendChild(this.linkDrag); this.linkEditor.appendChild(this.linkText); @@ -239,7 +240,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.tooltip.appendChild(this.linkEditor); } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index b2c65a31f..7083b1003 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -250,22 +250,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc); } if (!this._removeIcon) { - if (selectedDocs.length === 1) + if (selectedDocs.length === 1) { this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized()); - else - if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) { - let docViews = SelectionManager.ViewsSortedVertically(); - let topDocView = docViews[0]; - let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); - if (ind !== -1) { - topDocView.templates.splice(ind, 1); - topDocView.props.Document.subBulletDocs = undefined; - } else { - topDocView.addTemplate(Templates.Bullet); - topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); - } + } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) { + let docViews = SelectionManager.ViewsSortedVertically(); + let topDocView = docViews[0]; + let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); + if (ind !== -1) { + topDocView.templates.splice(ind, 1); + topDocView.props.Document.subBulletDocs = undefined; + } else { + topDocView.addTemplate(Templates.Bullet); + topDocView.props.Document.subBulletDocs = new List(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); } + } } this._removeIcon = false; } @@ -537,9 +536,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (temp !== Templates.Bullet.Layout || i === 0) { res.push(temp); } - }) + }); } - return res + return res; }, [] as string[]); let checked = false; docTemps.forEach(temp => { diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx index 098e725c7..7d0dc2913 100644 --- a/src/client/views/PresentationView.tsx +++ b/src/client/views/PresentationView.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; -import React = require("react") +import React = require("react"); import { observable, action, runInAction, reaction } from "mobx"; -import "./PresentationView.scss" +import "./PresentationView.scss"; import "./Main.tsx"; import { DocumentManager } from "../util/DocumentManager"; import { Utils } from "../../Utils"; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index cfe1b0663..e5b679e24 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -43,7 +43,7 @@ export class TemplateMenu extends React.Component { @action toggleTemplate = (event: React.ChangeEvent, template: Template): void => { if (event.target.checked) { - if (template.Name == "Bullet") { + if (template.Name === "Bullet") { let topDocView = this.props.docs[0]; topDocView.addTemplate(template); topDocView.props.Document.subBulletDocs = new List(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); @@ -52,7 +52,7 @@ export class TemplateMenu extends React.Component { } this.props.templates.set(template, true); } else { - if (template.Name == "Bullet") { + if (template.Name === "Bullet") { let topDocView = this.props.docs[0]; topDocView.removeTemplate(template); topDocView.props.Document.subBulletDocs = undefined; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 58f1e33a1..deec64225 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -83,7 +83,7 @@ export class CollectionDockingView extends React.Component tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { child.contentItems[j].remove(); @@ -94,8 +94,9 @@ export class CollectionDockingView extends React.Component doc) { let fieldContentView = ; let reference = React.createRef(); let onItemDown = (e: React.PointerEvent) => - (this.props.CollectionView!.props.isSelected() ? + (this.props.CollectionView.props.isSelected() ? SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined); let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); @@ -127,7 +127,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } const run = script.run; //TODO This should be able to be refactored to compile the script once - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]) + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); val && val.forEach(doc => applyToDoc(doc, run)); }}> @@ -230,14 +230,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { DocListCast(this.props.Document.data).map(doc => { csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; - }) + }); csv.substring(0, csv.length - 1); let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); if (self.props.CollectionView.props.addDocument) { let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }); if (schemaDoc) { - self.props.CollectionView.props.addDocument(schemaDoc, false); + self.props.CollectionView.props.addDocument(schemaDoc, false); } } } @@ -263,7 +263,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get previewDocument(): Doc | undefined { const children = DocListCast(this.props.Document[this.props.fieldKey]); const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; - return selected ? (this.previewScript && this.previewScript != "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; } get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 70c09d97c..6acef434e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -56,7 +56,7 @@ class TreeView extends React.Component { } else { CollectionDockingView.Instance.AddRightSplit(this.props.document); } - }; + } get children() { return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); @@ -184,8 +184,9 @@ class TreeView extends React.Component { {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)}
          ); - } else + } else { bulletType = BulletType.Collapsed; + } } }); return
          {
          ; } public static GetChildElements(docs: 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 => - ); + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child => + ); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 12edb2c2a..c3c4115b8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -63,7 +63,7 @@ export class MarqueeView extends React.Component e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); for (let i = 0; i < ns.length - 1; i++) { while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || ns[i].endsWith(";\r") || ns[i].endsWith(";") || @@ -80,7 +80,7 @@ export class MarqueeView extends React.Component let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); this.props.addDocument(newBox, false); y += 40 * this.props.getTransform().Scale; - }) + }); })(); } else if (e.key === "b" && e.ctrlKey) { //heuristically converts pasted text into a table. @@ -93,9 +93,10 @@ export class MarqueeView extends React.Component e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); - while (ns.length > 0 && ns[0].split("\t").length < 2) + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + while (ns.length > 0 && ns[0].split("\t").length < 2) { ns.splice(0, 1); + } if (ns.length > 0) { let columns = ns[0].split("\t"); let docList: Doc[] = []; @@ -109,7 +110,7 @@ export class MarqueeView extends React.Component let doc = new Doc(); columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); if (groupAttr) { - doc["_group"] = groupAttr; + doc._group = groupAttr; } doc.title = i.toString(); docList.push(doc); @@ -284,7 +285,7 @@ export class MarqueeView extends React.Component // summarizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]); // }); this.props.addLiveTextDocument(summary); - }) + }); } else { this.props.addDocument(newCollection, false); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7c7ca9e25..d4f660b3f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -149,7 +149,7 @@ export class CollectionFreeFormDocumentView extends DocComponent([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]) + maximizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); } } }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index cf8bcbb42..428dd9b36 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -244,7 +244,7 @@ export class DocumentView extends DocComponent(Docu const protoDest = destDoc.proto; const protoSrc = sourceDoc.proto; - if (de.mods == "Control") { + if (de.mods === "Control") { let src = protoSrc ? protoSrc : sourceDoc; let dst = protoDest ? protoDest : destDoc; dst.data = src; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f24d4ae88..f30022508 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -184,7 +184,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos) } + image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } } }); let text = StrCast(this.props.Document.documentText); @@ -232,9 +232,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; DocServer.GetRefField(docid).then(action((f: Opt) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); - else CollectionDockingView.Instance.AddRightSplit(f); + } else { + CollectionDockingView.Instance.AddRightSplit(f); + } } })); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6472ae711..3cc60a6c5 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -165,7 +165,7 @@ export class ImageBox extends DocComponent(ImageD else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href); let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50); let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; - let id = this.props.id; // bcz: used to set id = "isExpander" in templates.tsx + let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx return (
          { @observable private _keyInput: string = ""; @observable private _valueInput: string = ""; @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } - get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document } + get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; } constructor(props: FieldViewProps) { super(props); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 5de660d57..4f7919f50 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 {
          , + ,
          diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index d4f660b3f..001ce3095 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -205,11 +205,12 @@ export class CollectionFreeFormDocumentView extends DocComponent(Docu let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - if (de.mods === "Control") { + if (de.mods === "AltKey") { + const protoDest = destDoc.proto; + const protoSrc = sourceDoc.proto; let src = protoSrc ? protoSrc : sourceDoc; let dst = protoDest ? protoDest : destDoc; - dst.data = src; + dst.data = (src.data! as ObjectField)[Copy](); dst.nativeWidth = src.nativeWidth; dst.nativeHeight = src.nativeHeight; } else { - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); + Doc.MakeLink(sourceDoc, destDoc); de.data.droppedDocuments.push(destDoc); } e.stopPropagation(); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f30022508..bf98fb40b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -111,9 +111,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); + Doc.MakeLink(sourceDoc, destDoc); de.data.droppedDocuments.push(destDoc); e.stopPropagation(); } diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 611cb66b6..68b692aad 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -31,7 +31,7 @@ export class LinkBox extends React.Component { @undoBatch onViewButtonPressed = async (e: React.PointerEvent): Promise => { e.stopPropagation(); - DocumentManager.Instance.jumpToDocument(this.props.pairedDoc); + DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey); } onEditButtonPressed = (e: React.PointerEvent): void => { diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 11117122d..4cf798249 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -6,8 +6,7 @@ import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; +import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/RefField"; interface Props { @@ -24,7 +23,7 @@ export class LinkMenu extends React.Component { return links.map(link => { let doc = FieldValue(Cast(link[key], Doc)); if (doc) { - return this._editingLink = link)} type={type} />; + return this._editingLink = link)} type={type} />; } }); } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index c898f697b..ad3b880cd 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -216,6 +216,8 @@ export namespace Doc { } export function MakeLink(source: Doc, target: Doc) { + let protoSrc = source.proto ? source.proto : source; + let protoTarg = target.proto ? target.proto : target; UndoManager.RunInBatch(() => { let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); //let linkDoc = new Doc; @@ -226,15 +228,15 @@ export namespace Doc { linkDoc.proto!.linkedTo = target; linkDoc.proto!.linkedFrom = source; - let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc)); + let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc)); if (!linkedFrom) { - target.linkedFromDocs = linkedFrom = new List(); + protoTarg.linkedFromDocs = linkedFrom = new List(); } linkedFrom.push(linkDoc); - let linkedTo = Cast(source.linkedToDocs, listSpec(Doc)); + let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc)); if (!linkedTo) { - source.linkedToDocs = linkedTo = new List(); + protoSrc.linkedToDocs = linkedTo = new List(); } linkedTo.push(linkDoc); return linkDoc; -- cgit v1.2.3-70-g09d2 From 2c8aaae6f026199e3675f5e7b4677724d5089ae7 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 17 May 2019 14:53:09 -0400 Subject: added something to follow links in current tab ? --- src/client/util/DocumentManager.ts | 33 +++++++++++++--------- .../views/collections/CollectionSchemaView.tsx | 7 ----- .../views/nodes/CollectionFreeFormDocumentView.tsx | 2 +- 3 files changed, 20 insertions(+), 22 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index c6962ad8d..d06af6dd5 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,11 +1,14 @@ import { computed, observable } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; -import { Doc, DocListCast } from '../../new_fields/Doc'; +import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { Id } from '../../new_fields/RefField'; +import { CollectionView } from '../views/collections/CollectionView'; +import { CollectionPDFView } from '../views/collections/CollectionPDFView'; +import { CollectionVideoView } from '../views/collections/CollectionVideoView'; export class DocumentManager { @@ -27,31 +30,33 @@ export class DocumentManager { // this.DocumentViews = new Array(); } - public getDocumentViewById(id: string): DocumentView | null { + public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { let toReturn: DocumentView | null = null; + let passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; - //gets document view that is in a freeform canvas collection - DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document[Id] === id) { - toReturn = view; - return; - } - }); - if (!toReturn) { + for (let i = 0; i < passes.length; i++) { DocumentManager.Instance.DocumentViews.map(view => { - let doc = view.props.Document.proto; - if (doc && doc[Id] === id) { + if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; + return; } }); + if (!toReturn) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document.proto; + if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + toReturn = view; + } + }); + } } return toReturn; } - public getDocumentView(toFind: Doc): DocumentView | null { - return this.getDocumentViewById(toFind[Id]); + public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { + return this.getDocumentViewById(toFind[Id], preferredCollection); } public getDocumentViews(toFind: Doc): DocumentView[] { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 497dfde88..e2b90e26d 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -24,14 +24,7 @@ import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { List } from "../../../new_fields/List"; import { Id } from "../../../new_fields/RefField"; -import { isUndefined } from "typescript-collections/dist/lib/util"; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { Gateway } from "../../northstar/manager/Gateway"; -import { DocServer } from "../../DocServer"; -import { ColumnAttributeModel } from "../../northstar/core/attribute/AttributeModel"; -import { HistogramOperation } from "../../northstar/operations/HistogramOperation"; -import { AggregateFunction } from "../../northstar/model/idea/idea"; -import { AttributeTransformationModel } from "../../northstar/core/attribute/AttributeTransformationModel"; import { Docs } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 001ce3095..00aa71656 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -187,7 +187,7 @@ export class CollectionFreeFormDocumentView extends DocComponent Date: Fri, 17 May 2019 19:50:32 -0400 Subject: added right-click "open Tab" and double-click open tab --- src/client/views/Main.tsx | 1 + .../views/collections/CollectionDockingView.tsx | 20 ++++++++++++++++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 1 + .../views/nodes/CollectionFreeFormDocumentView.tsx | 19 +++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 2 ++ src/client/views/nodes/FieldView.tsx | 2 ++ src/client/views/nodes/ImageBox.tsx | 4 +++- 7 files changed, 46 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 51ad0aa49..177047a9a 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -166,6 +166,7 @@ export class Main extends React.Component { { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt; - + _stack: any; constructor(props: any) { super(props); + this._stack = (this.props as any).glContainer.parent.parent; DocServer.GetRefField(this.props.documentId).then(action((f: Opt) => this._document = f as Doc)); } @@ -420,6 +432,9 @@ export class DockedFrameRenderer extends React.Component { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } + addDocTab = (doc: Doc) => { + CollectionDockingView.Instance.AddTab(this._stack, doc); + } get content() { if (!this._document) { return (null); @@ -441,6 +456,7 @@ export class DockedFrameRenderer extends React.Component { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} + addDocTab={this.addDocTab} ContainingCollectionView={undefined} />
          ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6e4c79e62..ad32f70dc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -300,6 +300,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { parentActive: this.props.active, whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, }; } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 00aa71656..a0efc3154 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -159,6 +159,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { this._downX = e.clientX; @@ -166,6 +167,24 @@ export class CollectionFreeFormDocumentView extends DocComponent { + + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this.props.addDocTab(this.props.Document); + } + e.stopPropagation(); } onClick = async (e: React.MouseEvent) => { e.stopPropagation(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5264773b0..9a5d21b49 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -57,6 +57,7 @@ export interface DocumentViewProps { whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; bringToFront: (doc: Doc) => void; + addDocTab: (doc: Doc) => void; } const schema = createSchema({ @@ -307,6 +308,7 @@ export class DocumentView extends DocComponent(Docu cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton }); cm.addItem({ description: "Fields", event: this.fieldsClicked }); cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); + cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document) }); cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); cm.addItem({ description: "Find aliases", event: async () => { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 34b6c5e70..b5dfd18eb 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -35,6 +35,7 @@ export interface FieldViewProps { isTopMost: boolean; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocTab: (document: Doc) => boolean; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -87,6 +88,7 @@ export class FieldView extends React.Component { return ( (ImageD let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx return ( -
          +
          Date: Sat, 18 May 2019 09:42:43 -0400 Subject: fixed font error message. Improved opening summaries as either inTab or inPlace and switching with the alt-key --- src/client/views/ContextMenu.tsx | 3 ++- src/client/views/DocumentDecorations.tsx | 2 +- .../views/collections/CollectionDockingView.tsx | 7 +++-- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +-- .../views/nodes/CollectionFreeFormDocumentView.tsx | 30 ++++++++++++---------- src/client/views/nodes/DocumentView.tsx | 4 +-- src/client/views/nodes/FieldView.tsx | 2 +- 7 files changed, 30 insertions(+), 22 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 9ccbb7149..9143c012e 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -5,9 +5,10 @@ import { observer } from "mobx-react" import "./ContextMenu.scss" import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; library.add(faSearch); +library.add(faCircle); @observer export class ContextMenu extends React.Component { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7083b1003..d440ed898 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -279,7 +279,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> iconDoc.proto!.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title); iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey; iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined; - iconDoc.isMinimized = false; + iconDoc.proto!.isMinimized = false; iconDoc.width = Number(MINIMIZED_ICON_SIZE); iconDoc.height = Number(MINIMIZED_ICON_SIZE); iconDoc.x = NumCast(doc.x); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 28624e020..74f8ad242 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -432,8 +432,11 @@ export class DockedFrameRenderer extends React.Component { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - addDocTab = (doc: Doc) => { - CollectionDockingView.Instance.AddTab(this._stack, doc); + addDocTab = (doc: Doc, location: string) => { + if (location === "onRight") + CollectionDockingView.Instance.AddRightSplit(doc); + else + CollectionDockingView.Instance.AddTab(this._stack, doc); } get content() { if (!this._document) { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c3c4115b8..6301fd27e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -272,9 +272,9 @@ export class MarqueeView extends React.Component newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; - this.props.addDocument(newCollection, false); + //this.props.addDocument(newCollection, false); summary.proto!.summarizedDocs = new List(selected.map(s => s.proto!)); - //summary.proto!.maximizeOnRight = true; + summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" //summary.proto!.isButton = true; //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); // selected.map(summarizedDoc => { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a0efc3154..833947ac3 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { DocumentManager } from "../../util/DocumentManager"; import { SelectionManager } from "../../util/SelectionManager"; @@ -139,7 +139,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { + if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { isMinimized = BoolCast(maximizedDoc.isMinimized, false); } @@ -179,10 +179,9 @@ export class CollectionFreeFormDocumentView extends DocComponent { - document.removeEventListener("pointerup", this.onPointerUp); if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { - this.props.addDocTab(this.props.Document); + this.props.addDocTab(this.props.Document, "inTab"); } e.stopPropagation(); } @@ -193,6 +192,7 @@ export class CollectionFreeFormDocumentView extends DocComponent maxDoc.isMinimized = false); + hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); + maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" ? "inTab" : "inPlace"); + if (!hasView && maxLocation === "inPlace") { + this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(Doc.MakeDelegate(maxDoc), false)); + expandedDocs.forEach(maxDoc => maxDoc.isMinimized = true); + } + } + if (!hasView && maxLocation !== "inPlace") { let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); if (dataDocs) { - SelectionManager.DeselectAll(); expandedDocs.forEach(maxDoc => { - maxDoc.isMinimized = false; if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { - CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(maxDoc)); + this.props.addDocTab(Doc.MakeDelegate(maxDoc), maxLocation); } }); } } else { - //if (altKey) this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); this.toggleIcon(expandedDocs); } } else if (linkedToDocs.length || linkedFromDocs.length) { - SelectionManager.DeselectAll(); let linkedFwdDocs = [ linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7167da93b..760e31b49 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -68,7 +68,7 @@ export interface DocumentViewProps { whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; bringToFront: (doc: Doc) => void; - addDocTab: (doc: Doc) => void; + addDocTab: (doc: Doc, where: string) => void; } const schema = createSchema({ @@ -315,7 +315,7 @@ export class DocumentView extends DocComponent(Docu cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton, icon: "expand-arrows-alt" }); cm.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" }); - cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "expand-arrows-alt" }); cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" }); cm.addItem({ description: "Find aliases", event: async () => { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index b5dfd18eb..5c149af99 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -35,7 +35,7 @@ export interface FieldViewProps { isTopMost: boolean; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc) => boolean; + addDocTab: (document: Doc, where: string) => boolean; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; -- cgit v1.2.3-70-g09d2 From 3fd191a2d031e1a186a4eb2e009887399a4bc347 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sat, 18 May 2019 10:50:28 -0400 Subject: more fixes to maximizing docs to default to showing the same prototype each time. --- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 35 ++++++++++++---------- src/new_fields/Doc.ts | 6 ++++ 3 files changed, 26 insertions(+), 17 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 6301fd27e..d102c8af2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -273,7 +273,7 @@ export class MarqueeView extends React.Component selected = [newCollection]; newCollection.x = bounds.left + bounds.width; //this.props.addDocument(newCollection, false); - summary.proto!.summarizedDocs = new List(selected.map(s => s.proto!)); + summary.proto!.summarizedDocs = new List(selected); summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" //summary.proto!.isButton = true; //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 833947ac3..7cb4c1dfe 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -137,7 +137,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { + maximizedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { @@ -204,28 +204,31 @@ export class CollectionFreeFormDocumentView extends DocComponent Doc.GetProto(doc)) let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); - if (!hasView && altKey) { - expandedDocs.forEach(maxDoc => maxDoc.isMinimized = false); - hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" ? "inTab" : "inPlace"); - if (!hasView && maxLocation === "inPlace") { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(Doc.MakeDelegate(maxDoc), false)); - expandedDocs.forEach(maxDoc => maxDoc.isMinimized = true); + let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); + if (altKey) { + maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"); + if (!maxLocation || maxLocation === "inPlace") { + let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); + let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false); + expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); + let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); + if (!hasView) { + this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false)); + } + expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); } } - if (!hasView && maxLocation !== "inPlace") { + if (maxLocation && maxLocation !== "inPlace") { let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); if (dataDocs) { - expandedDocs.forEach(maxDoc => { - if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { - this.props.addDocTab(Doc.MakeDelegate(maxDoc), maxLocation); - } - }); + expandedDocs.forEach(maxDoc => + (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && + this.props.addDocTab(getDispDoc(maxDoc), maxLocation))); } } else { - this.toggleIcon(expandedDocs); + this.toggleIcon(expandedProtoDocs); } } else if (linkedToDocs.length || linkedFromDocs.length) { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index a7aa571f8..020f764a2 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -177,6 +177,12 @@ export namespace Doc { return r || r2 || r3 || r4 ? true : false; } + // gets the document's prototype or returns the document if it is a prototype + export function GetProto(doc: Doc) { + return Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto! : doc; + } + + export function MakeAlias(doc: Doc) { const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : undefined; const alias = new Doc; -- cgit v1.2.3-70-g09d2 From 12796ace87581ffeac9d67fa5d7ebff8cecd1a52 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 19 May 2019 11:33:42 -0400 Subject: added opaque background for non-annotation collections. fixed marquee view when there's zoomBasis, fixed double-click opening to stopPropagation. got rid of RIGHT_BTN_DRAG stuff. --- .../views/collections/CollectionBaseView.tsx | 4 ++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 11 +++-------- .../collections/collectionFreeForm/MarqueeView.tsx | 23 ++++++++-------------- .../views/nodes/CollectionFreeFormDocumentView.tsx | 3 ++- src/client/views/nodes/DocumentView.tsx | 5 +---- 5 files changed, 16 insertions(+), 30 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 76270f2e8..54e76fd5e 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -89,7 +89,7 @@ export class CollectionBaseView extends React.Component { } return false; } - @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; } @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { @@ -114,7 +114,7 @@ export class CollectionBaseView extends React.Component { } // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) { - let zoom = NumCast(this.props.Document.scale, 1) / this.props.ScreenToLocalTransform().Scale;// ; + let zoom = NumCast(this.props.Document.scale, 1); Doc.SetOnPrototype(doc, "zoomBasis", zoom); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b4b811da4..9cb8443f4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -37,7 +37,6 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema) @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - public static RIGHT_BTN_DRAG = false; private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; @@ -46,8 +45,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } private panX = () => this.Document.panX || 0; private panY = () => this.Document.panY || 0; private zoomScaling = () => this.Document.scale || 1; @@ -66,7 +65,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return true; } private selectDocuments = (docs: Doc[]) => { - SelectionManager.DeselectAll; + SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => SelectionManager.SelectDoc(dv!, true)); } @@ -112,11 +111,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && - (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || - (e.button === 0 && e.altKey)) && this.props.active())) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && - ((e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) { + if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d102c8af2..4587c2227 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -132,17 +132,18 @@ export class MarqueeView extends React.Component this._downY = this._lastY = e.pageY; this._commandExecuted = false; PreviewCursor.Visible = false; - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { + if (e.button === 2 || (e.button === 0 && e.altKey)) { + if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]); document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); - // bcz: do we need this? it kills the context menu on the main collection + if (e.altKey) { + e.stopPropagation(); + e.preventDefault(); + } + // bcz: do we need this? it kills the context menu on the main collection if !altKey // e.stopPropagation(); } - if (e.altKey) { - e.preventDefault(); - } } @action @@ -248,6 +249,7 @@ export class MarqueeView extends React.Component panX: 0, panY: 0, borderRounding: e.key === "e" ? -1 : undefined, + backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", scale: zoomBasis, width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, @@ -275,15 +277,6 @@ export class MarqueeView extends React.Component //this.props.addDocument(newCollection, false); summary.proto!.summarizedDocs = new List(selected); summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - //summary.proto!.isButton = true; - //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); - // selected.map(summarizedDoc => { - // let maxx = NumCast(summarizedDoc.x, undefined); - // let maxy = NumCast(summarizedDoc.y, undefined); - // let maxw = NumCast(summarizedDoc.width, undefined); - // let maxh = NumCast(summarizedDoc.height, undefined); - // summarizedDoc.isIconAnimating = new List([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]); - // }); this.props.addLiveTextDocument(summary); }); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7cb4c1dfe..8851e4b93 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -180,8 +180,9 @@ export class CollectionFreeFormDocumentView extends DocComponent { document.removeEventListener("pointerup", this.onPointerUp); - if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + if (!(e as any).propagationIsStopped && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { this.props.addDocTab(this.props.Document, "inTab"); + (e as any).propagationIsStopped = true; } e.stopPropagation(); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 760e31b49..bb5daaf4e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -187,9 +187,6 @@ export class DocumentView extends DocComponent(Docu onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; - if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { - return; - } this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; if (e.shiftKey && e.buttons === 1) { CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); @@ -207,7 +204,7 @@ export class DocumentView extends DocComponent(Docu if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { + if (!e.altKey && !this.topMost && e.buttons === 1) { this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); } } -- cgit v1.2.3-70-g09d2 From 01a223f2e6685506cc1e5db69e9062d5ff0d3246 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Sun, 19 May 2019 13:43:37 -0400 Subject: fixed dragging out tree view to drag out user document. fixed opening collections in a tab to show same layout as outside the tab. --- src/client/views/MainView.tsx | 3 ++- src/client/views/collections/CollectionDockingView.tsx | 16 ++++++++++++++-- .../views/nodes/CollectionFreeFormDocumentView.tsx | 16 +++++++++------- 3 files changed, 25 insertions(+), 10 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index bdd3e9650..cdb105e21 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -214,7 +214,8 @@ export class MainView extends React.Component { let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" })); let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" })); - let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); + let addTreeNode = action(() => CurrentUserUtils.UserDocument); + //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" })); let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" })); let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" })); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index eeec3eaf0..06b262d79 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -20,6 +20,7 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); import { ParentDocSelector } from './ParentDocumentSelector'; import { DocumentManager } from '../../util/DocumentManager'; +import { CollectionViewType } from './CollectionBaseView'; @observer export class CollectionDockingView extends React.Component { @@ -431,10 +432,21 @@ export class DockedFrameRenderer extends React.Component { if (this._mainCont.current && this._mainCont.current.children) { let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement); scale = Utils.GetScreenTransform(this._mainCont.current).scale; - return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling()); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / scale); } return Transform.Identity(); } + get scaleToFitMultiplier() { + let docWidth = NumCast(this._document!.width); + let docHeight = NumCast(this._document!.height); + if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1; + if (StrCast(this._document!.layout).indexOf("Collection") === -1 || + NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1; + let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ? + this._panelHeight / docHeight : this._panelWidth / docWidth); + console.log("Scaling = " + scaling); + return scaling; + } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } addDocTab = (doc: Doc, location: string) => { @@ -450,7 +462,7 @@ export class DockedFrameRenderer extends React.Component { } return (
          + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}> (); private _downX: number = 0; private _downY: number = 0; + private _doubleTap = false; + private _lastTap: number = 0; _bringToFrontDisposer?: IReactionDisposer; @computed get transform() { @@ -159,18 +161,16 @@ export class CollectionFreeFormDocumentView extends DocComponent { this._downX = e.clientX; this._downY = e.clientY; + this._doubleTap = false; if (e.button === 0 && e.altKey) { e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document if (Date.now() - this._lastTap < 300) { if (e.buttons === 1) { - this._downX = e.clientX; - this._downY = e.clientY; document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); } @@ -180,14 +180,16 @@ export class CollectionFreeFormDocumentView extends DocComponent { document.removeEventListener("pointerup", this.onPointerUp); - if (!(e as any).propagationIsStopped && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { - this.props.addDocTab(this.props.Document, "inTab"); - (e as any).propagationIsStopped = true; + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this._doubleTap = true; } - e.stopPropagation(); } onClick = async (e: React.MouseEvent) => { e.stopPropagation(); + if (this._doubleTap) { + this.props.addDocTab(this.props.Document, "inTab"); + SelectionManager.DeselectAll(); + } let altKey = e.altKey; if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { -- cgit v1.2.3-70-g09d2 From b789ac4cc3fe013139825f811d7574e1d305c2cc Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Sun, 19 May 2019 20:30:50 -0400 Subject: Added link follow behavior --- src/client/util/DocumentManager.ts | 7 ++++--- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index d06af6dd5..be448098a 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -115,7 +115,7 @@ export class DocumentManager { } @undoBatch - public jumpToDocument = async (docDelegate: Doc, makeCopy: boolean = true): Promise => { + public jumpToDocument = async (docDelegate: Doc, makeCopy: boolean = true, dockFunc?: (doc: Doc) => void): Promise => { let doc = docDelegate.proto ? docDelegate.proto : docDelegate; const page = NumCast(doc.page, undefined); const contextDoc = await Cast(doc.annotationOn, Doc); @@ -128,14 +128,15 @@ export class DocumentManager { docView.props.focus(docView.props.Document); } else { if (!contextDoc) { - CollectionDockingView.Instance.AddRightSplit(docDelegate ? (makeCopy ? Doc.MakeCopy(docDelegate) : docDelegate) : Doc.MakeDelegate(doc)); + const actualDoc = docDelegate ? (makeCopy ? Doc.MakeCopy(docDelegate) : docDelegate) : Doc.MakeDelegate(doc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); } else { let contextView = DocumentManager.Instance.getDocumentView(contextDoc); if (contextView) { contextDoc.panTransformType = "Ease"; contextView.props.focus(contextDoc); } else { - CollectionDockingView.Instance.AddRightSplit(contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); } } } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index fa44ec9f3..5bff08280 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -239,7 +239,8 @@ export class CollectionFreeFormDocumentView extends DocComponent this.props.addDocTab(document, maxLocation)); } } } -- cgit v1.2.3-70-g09d2 From 059faf25a0f8ba80e1d2853dcefa1a70df4cbb6a Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 20 May 2019 17:10:21 -0400 Subject: fixed icons for pdfs. fixed activeItem issue for golden layout. --- src/client/goldenLayout.js | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 1 + src/client/views/nodes/PDFBox.tsx | 58 +++++++++++----------- 3 files changed, 31 insertions(+), 30 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index ab2bcefed..54c9c6068 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -4466,7 +4466,7 @@ } if (this.contentItems.length > 0) { - initialItem = this.contentItems[this.config.activeItemIndex || 0]; + initialItem = this.contentItems[Math.min(this.contentItems.length - 1, this.config.activeItemIndex || 0)]; if (!initialItem) { throw new Error('Configured activeItemIndex out of bounds'); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 5bff08280..fa2b09ba7 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -189,6 +189,7 @@ export class CollectionFreeFormDocumentView extends DocComponent(PdfDocumen @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); } componentDidMount() { - let wasSelected = false; + let wasSelected = this.props.isSelected(); this._reactionDisposer = reaction( - () => this.props.isSelected(), + () => [this.props.isSelected(), this.curPage], () => { - if (this.curPage > 0 && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) { + if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) { this.saveThumbnail(); } wasSelected = this._interactive = this.props.isSelected(); @@ -246,16 +245,19 @@ export class PDFBox extends DocComponent(PdfDocumen @action saveThumbnail = () => { + this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1); this._renderAsSvg = false; setTimeout(() => { let nwidth = FieldValue(this.Document.nativeWidth, 0); let nheight = FieldValue(this.Document.nativeHeight, 0); - htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 }) + htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 }) .then(action((dataUrl: string) => { - SearchBox.convertDataUri(dataUrl, this.Document[Id] + "_" + this.curPage).then((returnedFilename) => { - this.props.Document.thumbnail = new ImageField(new URL(dataUrl)); - this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1); - this._renderAsSvg = true; + SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => { + if (returnedFilename) { + let url = DocServer.prepend(returnedFilename); + this.props.Document.thumbnail = new ImageField(new URL(url)); + } + runInAction(() => this._renderAsSvg = true); }) })) .catch(function (error: any) { @@ -292,7 +294,6 @@ export class PDFBox extends DocComponent(PdfDocumen } @computed get pdfContent() { - trace(); let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); if (!pdfUrl) { return

          No pdf url to render

          ; @@ -309,7 +310,7 @@ export class PDFBox extends DocComponent(PdfDocumen ; let xf = (this.Document.nativeHeight || 0) / this.renderHeight; return
          - + {body}
          ; @@ -317,26 +318,25 @@ export class PDFBox extends DocComponent(PdfDocumen @computed get pdfRenderer() { - let proxy = this._loaded ? (null) : this.imageProxyRenderer; let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); - if ((!this._interactive && proxy) || !pdfUrl) { + let proxy = this.imageProxyRenderer; + if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) { return proxy; } return [ this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), this._currAnno.map((element: any) => element), - this.pdfContent, - proxy + this.pdfContent ]; } @computed get imageProxyRenderer() { let thumbField = this.props.Document.thumbnail; - if (thumbField) { - let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + if (thumbField && this._renderAsSvg) { + let path = // this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" : thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; - return ; + return ; } return (null); } -- cgit v1.2.3-70-g09d2 From 060455feec89c94073636f612ccca54a0aaabb90 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 20 May 2019 21:29:17 -0400 Subject: fixed a few things for Firefox - dragging images, typing spaces, scrool speed. and focusing proeprly with zoomBasis. --- .vscode/launch.json | 9 +++++++++ .../collections/collectionFreeForm/CollectionFreeFormView.tsx | 9 +++------ src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 3 +++ src/client/views/nodes/FormattedTextBox.tsx | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/.vscode/launch.json b/.vscode/launch.json index d245be2ad..3b6549767 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,6 +13,15 @@ "url": "http://localhost:1050/login", "webRoot": "${workspaceFolder}", }, + { + "type": "firefox", + "request": "launch", + "name": "Launch Firefox against localhost", + "sourceMaps": "client", + "reAttach": true, + "url": "http://localhost:1050/login", + "webRoot": "${workspaceFolder}", + }, { "type": "chrome", "request": "launch", diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ee6f4821f..3ec7184d2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -183,7 +183,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return; } e.stopPropagation(); - const coefficient = 1000; + const coefficient = 100; if (e.ctrlKey) { let deltaScale = (1 - (e.deltaY / coefficient)); @@ -215,7 +215,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { - this.panDisposer && clearTimeout(this.panDisposer); this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -243,7 +242,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } - panDisposer?: NodeJS.Timeout; focusDocument = (doc: Doc) => { const panX = this.Document.panX; const panY = this.Document.panY; @@ -265,15 +263,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } SelectionManager.DeselectAll(); - const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newPanX = NumCast(doc.x) + NumCast(doc.width) / NumCast(doc.zoomBasis, 1) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 2; const newState = HistoryUtil.getState(); newState.initializers[id] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); this.setPan(newPanX, newPanY); this.props.Document.panTransformType = "Ease"; this.props.focus(this.props.Document); - this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index fa2b09ba7..7c3180496 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -177,6 +177,9 @@ export class CollectionFreeFormDocumentView extends DocComponent { document.removeEventListener("pointerup", this.onPointerUp); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d15813f9a..5afef221c 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -366,7 +366,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe style={{ pointerEvents: interactive ? "all" : "none", }} - onKeyDown={this.onKeyPress} + // onKeyDown={this.onKeyPress} onKeyPress={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} @@ -378,7 +378,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // tfs: do we need this event handler onWheel={this.onPointerWheel} > -
          +
          ); } -- cgit v1.2.3-70-g09d2 From 3abf1b817a5ed0e2bcacaa08f4a15a9193794a3a Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Tue, 21 May 2019 08:22:24 -0400 Subject: Fixed performance problems --- src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 6 +++--- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 97e610e13..31864f357 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,5 +1,5 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -344,10 +344,10 @@ export class MarqueeView extends React.Component } render() { - let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; return
          - {!this._visible ? null : this.marqueeDiv} + {this._visible ? this.marqueeDiv : null}
          {this.props.children}
          diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7c3180496..bf0c272e3 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; @@ -211,7 +211,7 @@ export class CollectionFreeFormDocumentView extends DocComponent Doc.GetProto(doc)) + let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc)); let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); if (altKey) { @@ -286,7 +286,9 @@ export class CollectionFreeFormDocumentView extends DocComponent Date: Tue, 21 May 2019 09:34:10 -0400 Subject: Fixed speed again? --- src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 10 +++++----- src/client/views/nodes/DocumentContentsView.tsx | 5 +++-- src/client/views/nodes/ImageBox.tsx | 2 +- src/new_fields/Doc.ts | 1 - 4 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 bf0c272e3..aaaa6a9c5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -266,15 +266,15 @@ export class CollectionFreeFormDocumentView extends DocComponent 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800); let fadeUp = .75 * screenWidth; let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; - zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; + // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; return (
          Date: Wed, 22 May 2019 19:51:49 -0400 Subject: A bunch of fixes/changes --- src/client/documents/Documents.ts | 2 + src/client/util/DocumentManager.ts | 6 ++- src/client/views/InkingCanvas.scss | 42 ++++++++++++-------- src/client/views/SearchBox.tsx | 10 +++-- .../views/collections/CollectionPDFView.scss | 39 ++++++++++-------- src/client/views/collections/CollectionSubView.tsx | 4 ++ .../views/collections/CollectionVideoView.tsx | 46 +++++++++++++++++++++- .../collections/collectionFreeForm/MarqueeView.tsx | 5 ++- .../views/nodes/CollectionFreeFormDocumentView.tsx | 9 ++++- src/client/views/nodes/DocumentView.tsx | 12 +++--- src/client/views/nodes/ImageBox.tsx | 6 ++- src/client/views/nodes/KeyValueBox.tsx | 2 +- src/client/views/nodes/PDFBox.tsx | 16 +++++++- src/client/views/nodes/VideoBox.tsx | 8 +++- 14 files changed, 153 insertions(+), 54 deletions(-) (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 323534b2b..ae190a989 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -77,7 +77,9 @@ export namespace DocUtils { linkDoc.proto!.linkTags = "Default"; linkDoc.proto!.linkedTo = target; + linkDoc.proto!.linkedToPage = target.curPage; linkDoc.proto!.linkedFrom = source; + linkDoc.proto!.linkedFromPage = source.curPage; let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc)); if (!linkedFrom) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f113ff1d7..65c4b9e4b 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -115,11 +115,11 @@ export class DocumentManager { } @undoBatch - public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void): Promise => { + public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise => { let doc = Doc.GetProto(docDelegate); - const page = NumCast(doc.page, undefined); const contextDoc = await Cast(doc.annotationOn, Doc); if (contextDoc) { + const page = NumCast(doc.page, linkPage || 0); const curPage = NumCast(contextDoc.curPage, page); if (page !== curPage) contextDoc.curPage = page; } @@ -127,11 +127,13 @@ export class DocumentManager { // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { docView.props.Document.libraryBrush = true; + if (linkPage !== undefined) docView.props.Document.curPage = linkPage; docView.props.focus(docView.props.Document); } else { if (!contextDoc) { const actualDoc = Doc.MakeAlias(docDelegate); actualDoc.libraryBrush = true; + if (linkPage !== undefined) actualDoc.curPage = linkPage; (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); } else { let contextView: DocumentView | null; diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index 8bcaf41dd..d95398f17 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -1,40 +1,50 @@ @import "globalCssVariables"; .inkingCanvas { - opacity:0.99; + opacity: 0.99; + .jsx-parser { - position:absolute; - width:100%; - height:100%; + position: absolute; + width: 100%; + height: 100%; + z-index: -1; // allows annotations to appear on videos when screen is full-size & ... } } -.inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect { + +.inkingCanvas-paths-ink, +.inkingCanvas-paths-markers, +.inkingCanvas-noSelect, +.inkingCanvas-canSelect { position: absolute; top: 0; - left:0; + left: 0; width: 8192px; height: 8192px; - cursor:"crosshair"; + cursor: "crosshair"; pointer-events: auto; - + } -.inkingCanvas-canSelect, -.inkingCanvas-noSelect { - top:-50000px; - left:-50000px; + +.inkingCanvas-canSelect, +.inkingCanvas-noSelect { + top: -50000px; + left: -50000px; width: 100000px; height: 100000px; } -.inkingCanvas-noSelect { + +.inkingCanvas-noSelect { pointer-events: none; cursor: "arrow"; } -.inkingCanvas-paths-ink, .inkingCanvas-paths-markers { + +.inkingCanvas-paths-ink, +.inkingCanvas-paths-markers { pointer-events: none; z-index: 10000; // overlays ink on top of everything cursor: "arrow"; } + .inkingCanvas-paths-markers { mix-blend-mode: multiply; -} - +} \ No newline at end of file diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx index 911fc8d9c..fa9462acf 100644 --- a/src/client/views/SearchBox.tsx +++ b/src/client/views/SearchBox.tsx @@ -21,6 +21,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { SetupDrag } from '../util/DragManager'; import { Docs } from '../documents/Documents'; import { RouteStore } from '../../server/RouteStore'; +import { NumCast } from '../../new_fields/Types'; library.add(faSearch); library.add(faObjectGroup); @@ -143,18 +144,21 @@ export class SearchBox extends React.Component { }); let x = 0; let y = 0; + let maxy = 300; for (const doc of docs) { doc.x = x; doc.y = y; doc.width = 200; - doc.height = 200; + doc.height = 200 * NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); + maxy = Math.max(doc.height, maxy); x += 250; if (x > 1000) { + maxy = 300; x = 0; - y += 250; + y += maxy + 25; } } - return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, title: `Search Docs: "${this.searchString}"` }); + return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` }); } // Useful queries: diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index f6fb79582..50201bae8 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,49 +1,56 @@ .collectionPdfView-buttonTray { - top : 15px; - left : 20px; - position: relative; + top: 15px; + left: 20px; + position: relative; transform-origin: left top; position: absolute; } + .collectionPdfView-thumb { - width:25px; - height:25px; + width: 25px; + height: 25px; transform-origin: left top; position: absolute; background: darkgray; } + .collectionPdfView-slider { - width:25px; - height:25px; + width: 25px; + height: 25px; transform-origin: left top; position: absolute; background: lightgray; } -.collectionPdfView-cont{ + +.collectionPdfView-cont { width: 100%; height: 100%; - position: absolute; + position: absolute; top: 0; - left:0; + left: 0; + z-index: -1; } + .collectionPdfView-cont-dragging { span { user-select: none; } } + .collectionPdfView-backward { - color : white; + color: white; font-size: 24px; - top :0px; - left : 0px; + top: 0px; + left: 0px; position: absolute; background-color: rgba(50, 50, 50, 0.2); } + .collectionPdfView-forward { - color : white; + color: white; font-size: 24px; - top :0px; - left : 45px; + top: 0px; + left: 45px; position: absolute; background-color: rgba(50, 50, 50, 0.2); } \ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 7800b35df..4a21e4465 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -153,6 +153,10 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { @undoBatch @action protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + if (e.ctrlKey) { + e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl + return; + } let html = e.dataTransfer.getData("text/html"); let text = e.dataTransfer.getData("text/plain"); diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 27f23a1a8..7853544d5 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -1,4 +1,5 @@ import { action, observable, trace } from "mobx"; +import * as htmlToImage from "html-to-image"; import { observer } from "mobx-react"; import { ContextMenu } from "../ContextMenu"; import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView"; @@ -6,10 +7,14 @@ import React = require("react"); import "./CollectionVideoView.scss"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { emptyFunction } from "../../../Utils"; +import { emptyFunction, Utils } from "../../../Utils"; import { Id } from "../../../new_fields/FieldSymbols"; import { VideoBox } from "../nodes/VideoBox"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast, Cast, StrCast } from "../../../new_fields/Types"; +import { VideoField } from "../../../new_fields/URLField"; +import { SearchBox } from "../SearchBox"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils } from "../../documents/Documents"; @observer @@ -67,6 +72,43 @@ export class CollectionVideoView extends React.Component { onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 } + + let field = Cast(this.props.Document[this.props.fieldKey], VideoField); + if (field) { + let url = field.url.href; + ContextMenu.Instance.addItem({ + description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" + }); + } + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + ContextMenu.Instance.addItem({ + description: "Take Snapshot", event: async () => { + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height); + + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => { + if (returnedFilename) { + let url = DocServer.prepend(returnedFilename); + let imageSummary = Docs.ImageDocument(url, { + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + }); + this.props.addDocument && this.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + }, + icon: "expand-arrows-alt" + }); } setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d72046800..973ebfbdd 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -296,7 +296,10 @@ export class MarqueeView extends React.Component SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => { if (returnedFilename) { let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.ImageDocument(url, { x: bounds.left, y: bounds.top + 100 / zoomBasis, width: 150, title: "-summary image-" }); + let imageSummary = Docs.ImageDocument(url, { + x: bounds.left, y: bounds.top + 100 / zoomBasis, + width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-" + }); summary.imageSummary = imageSummary; this.props.addDocument(imageSummary, false); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index aaaa6a9c5..66d91f9c2 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -195,6 +195,7 @@ export class CollectionFreeFormDocumentView extends DocComponent l instanceof Promise)) { let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], altKey, document => this.props.addDocTab(document, maxLocation)); + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]); } } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 87c88f57c..01c4d82fb 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -234,12 +234,14 @@ export class DocumentView extends DocComponent(Docu makeButton = (e: React.MouseEvent): void => { let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; doc.isButton = !BoolCast(doc.isButton, false); - if (doc.isButton && !doc.nativeWidth) { - doc.nativeWidth = this.props.Document[WidthSym](); - doc.nativeHeight = this.props.Document[HeightSym](); - } else { + if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally + if (doc.isButton && !doc.nativeWidth) { + doc.nativeWidth = this.props.Document[WidthSym](); + doc.nativeHeight = this.props.Document[HeightSym](); + } else { - doc.nativeWidth = doc.nativeHeight = undefined; + doc.nativeWidth = doc.nativeHeight = undefined; + } } } fullScreenClicked = (e: React.MouseEvent): void => { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 71bda6fea..cc0dc8220 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -47,7 +47,7 @@ export class ImageBox extends DocComponent(ImageD onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; - if (this._photoIndex === 0 && (this.props as any).id !== "isExpander" && (!this.Document.nativeHeight || !this.Document.nativeWidth)) { + if (this._photoIndex === 0 && (this.props as any).id !== "isExpander" && (!this.Document.nativeWidth || !this.Document.nativeHeight || Math.abs(this.Document.nativeWidth / this.Document.nativeHeight - w / h) > 0.05)) { Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w); this.Document.height = FieldValue(this.Document.width, 0) * h / w; } @@ -165,8 +165,10 @@ export class ImageBox extends DocComponent(ImageD } choosePath(url: URL) { - if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1) + const lower = url.href.toLowerCase(); + if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1 || !(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) { return url.href; + } let ext = path.extname(url.href); return url.href.replace(ext, this._curSuffix + ext); } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 8cb576786..849f17aa4 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -78,7 +78,7 @@ export class KeyValueBox extends React.Component { let rows: JSX.Element[] = []; let i = 0; - for (let key in ids) { + for (let key of Object.keys(ids).sort()) { rows.push(); } return rows; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 5b8b15ed8..733bc920f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -23,6 +23,7 @@ import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; var path = require('path'); import React = require("react"); +import { ContextMenu } from "../ContextMenu"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -357,7 +358,7 @@ export class PDFBox extends DocComponent(PdfDocumen @computed get imageProxyRenderer() { let thumbField = this.props.Document.thumbnail; - if (thumbField && this._renderAsSvg) { + if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) { // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; @@ -380,10 +381,21 @@ export class PDFBox extends DocComponent(PdfDocumen } @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true); @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false); + onContextMenu = (e: React.MouseEvent): void => { + let field = Cast(this.Document[this.props.fieldKey], PdfField); + if (field) { + let url = field.url.href; + ContextMenu.Instance.addItem({ + description: "Copy path", event: () => { + Utils.CopyText(url); + }, icon: "expand-arrows-alt" + }); + } + } render() { let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( -
          +
          {this.pdfRenderer}
          ); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 6ae55d151..ab57b4b04 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -88,10 +88,14 @@ export class VideoBox extends DocComponent(VideoD if (vref) { vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); if (this._reactionDisposer) this._reactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.curPage, () => - vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true }); + this._reactionDisposer = reaction(() => this.props.Document.curPage, () => { + if (!this.Playing) { + vref.currentTime = NumCast(this.props.Document.curPage, 0); + } + }, { fireImmediately: true }); } } + videoContent(path: string) { let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : ""); return