diff -r 000000000000 -r 6474c204b198 browser/components/tabview/items.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/items.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,1077 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// ********** +// Title: items.js + +// ########## +// Class: Item +// Superclass for all visible objects (s and s). +// +// If you subclass, in addition to the things Item provides, you need to also provide these methods: +// setBounds - function(rect, immediately, options) +// setZ - function(value) +// close - function() +// save - function() +// +// Subclasses of Item must also provide the interface. +// +// Make sure to call _init() from your subclass's constructor. +function Item() { + // Variable: isAnItem + // Always true for Items + this.isAnItem = true; + + // Variable: bounds + // The position and size of this Item, represented as a . + // This should never be modified without using setBounds() + this.bounds = null; + + // Variable: zIndex + // The z-index for this item. + this.zIndex = 0; + + // Variable: container + // The outermost DOM element that describes this item on screen. + this.container = null; + + // Variable: parent + // The groupItem that this item is a child of + this.parent = null; + + // Variable: userSize + // A that describes the last size specifically chosen by the user. + // Used by unsquish. + this.userSize = null; + + // Variable: dragOptions + // Used by + // + // Possible properties: + // cancelClass - A space-delimited list of classes that should cancel a drag + // start - A function to be called when a drag starts + // drag - A function to be called each time the mouse moves during drag + // stop - A function to be called when the drag is done + this.dragOptions = null; + + // Variable: dropOptions + // Used by if the item is set to droppable. + // + // Possible properties: + // accept - A function to determine if a particular item should be accepted for dropping + // over - A function to be called when an item is over this item + // out - A function to be called when an item leaves this item + // drop - A function to be called when an item is dropped in this item + this.dropOptions = null; + + // Variable: resizeOptions + // Used by + // + // Possible properties: + // minWidth - Minimum width allowable during resize + // minHeight - Minimum height allowable during resize + // aspectRatio - true if we should respect aspect ratio; default false + // start - A function to be called when resizing starts + // resize - A function to be called each time the mouse moves during resize + // stop - A function to be called when the resize is done + this.resizeOptions = null; + + // Variable: isDragging + // Boolean for whether the item is currently being dragged or not. + this.isDragging = false; +}; + +Item.prototype = { + // ---------- + // Function: _init + // Initializes the object. To be called from the subclass's intialization function. + // + // Parameters: + // container - the outermost DOM element that describes this item onscreen. + _init: function Item__init(container) { + Utils.assert(typeof this.addSubscriber == 'function' && + typeof this.removeSubscriber == 'function' && + typeof this._sendToSubscribers == 'function', + 'Subclass must implement the Subscribable interface'); + Utils.assert(Utils.isDOMElement(container), 'container must be a DOM element'); + Utils.assert(typeof this.setBounds == 'function', 'Subclass must provide setBounds'); + Utils.assert(typeof this.setZ == 'function', 'Subclass must provide setZ'); + Utils.assert(typeof this.close == 'function', 'Subclass must provide close'); + Utils.assert(typeof this.save == 'function', 'Subclass must provide save'); + Utils.assert(Utils.isRect(this.bounds), 'Subclass must provide bounds'); + + this.container = container; + this.$container = iQ(container); + + iQ(this.container).data('item', this); + + // ___ drag + this.dragOptions = { + cancelClass: 'close stackExpander', + start: function(e, ui) { + UI.setActive(this); + if (this.isAGroupItem) + this._unfreezeItemSize(); + // if we start dragging a tab within a group, start with dropSpace on. + else if (this.parent != null) + this.parent._dropSpaceActive = true; + drag.info = new Drag(this, e); + }, + drag: function(e) { + drag.info.drag(e); + }, + stop: function() { + drag.info.stop(); + + if (!this.isAGroupItem && !this.parent) { + new GroupItem([drag.info.$el], {focusTitle: true}); + gTabView.firstUseExperienced = true; + } + + drag.info = null; + }, + // The minimum the mouse must move after mouseDown in order to move an + // item + minDragDistance: 3 + }; + + // ___ drop + this.dropOptions = { + over: function() {}, + out: function() { + let groupItem = drag.info.item.parent; + if (groupItem) + groupItem.remove(drag.info.$el, {dontClose: true}); + iQ(this.container).removeClass("acceptsDrop"); + }, + drop: function(event) { + iQ(this.container).removeClass("acceptsDrop"); + }, + // Function: dropAcceptFunction + // Given a DOM element, returns true if it should accept tabs being dropped on it. + // Private to this file. + accept: function dropAcceptFunction(item) { + return (item && item.isATabItem && (!item.parent || !item.parent.expanded)); + } + }; + + // ___ resize + var self = this; + this.resizeOptions = { + aspectRatio: self.keepProportional, + minWidth: 90, + minHeight: 90, + start: function(e,ui) { + UI.setActive(this); + resize.info = new Drag(this, e); + }, + resize: function(e,ui) { + resize.info.snap(UI.rtl ? 'topright' : 'topleft', false, self.keepProportional); + }, + stop: function() { + self.setUserSize(); + self.pushAway(); + resize.info.stop(); + resize.info = null; + } + }; + }, + + // ---------- + // Function: getBounds + // Returns a copy of the Item's bounds as a . + getBounds: function Item_getBounds() { + Utils.assert(Utils.isRect(this.bounds), 'this.bounds should be a rect'); + return new Rect(this.bounds); + }, + + // ---------- + // Function: overlapsWithOtherItems + // Returns true if this Item overlaps with any other Item on the screen. + overlapsWithOtherItems: function Item_overlapsWithOtherItems() { + var self = this; + var items = Items.getTopLevelItems(); + var bounds = this.getBounds(); + return items.some(function(item) { + if (item == self) // can't overlap with yourself. + return false; + var myBounds = item.getBounds(); + return myBounds.intersects(bounds); + } ); + }, + + // ---------- + // Function: setPosition + // Moves the Item to the specified location. + // + // Parameters: + // left - the new left coordinate relative to the window + // top - the new top coordinate relative to the window + // immediately - if false or omitted, animates to the new position; + // otherwise goes there immediately + setPosition: function Item_setPosition(left, top, immediately) { + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); + this.setBounds(new Rect(left, top, this.bounds.width, this.bounds.height), immediately); + }, + + // ---------- + // Function: setSize + // Resizes the Item to the specified size. + // + // Parameters: + // width - the new width in pixels + // height - the new height in pixels + // immediately - if false or omitted, animates to the new size; + // otherwise resizes immediately + setSize: function Item_setSize(width, height, immediately) { + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); + this.setBounds(new Rect(this.bounds.left, this.bounds.top, width, height), immediately); + }, + + // ---------- + // Function: setUserSize + // Remembers the current size as one the user has chosen. + setUserSize: function Item_setUserSize() { + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); + this.userSize = new Point(this.bounds.width, this.bounds.height); + this.save(); + }, + + // ---------- + // Function: getZ + // Returns the zIndex of the Item. + getZ: function Item_getZ() { + return this.zIndex; + }, + + // ---------- + // Function: setRotation + // Rotates the object to the given number of degrees. + setRotation: function Item_setRotation(degrees) { + var value = degrees ? "rotate(%deg)".replace(/%/, degrees) : null; + iQ(this.container).css({"transform": value}); + }, + + // ---------- + // Function: setParent + // Sets the receiver's parent to the given . + setParent: function Item_setParent(parent) { + this.parent = parent; + this.removeTrenches(); + this.save(); + }, + + // ---------- + // Function: pushAway + // Pushes all other items away so none overlap this Item. + // + // Parameters: + // immediately - boolean for doing the pushAway without animation + pushAway: function Item_pushAway(immediately) { + var items = Items.getTopLevelItems(); + + // we need at least two top-level items to push something away + if (items.length < 2) + return; + + var buffer = Math.floor(Items.defaultGutter / 2); + + // setup each Item's pushAwayData attribute: + items.forEach(function pushAway_setupPushAwayData(item) { + var data = {}; + data.bounds = item.getBounds(); + data.startBounds = new Rect(data.bounds); + // Infinity = (as yet) unaffected + data.generation = Infinity; + item.pushAwayData = data; + }); + + // The first item is a 0-generation pushed item. It all starts here. + var itemsToPush = [this]; + this.pushAwayData.generation = 0; + + var pushOne = function Item_pushAway_pushOne(baseItem) { + // the baseItem is an n-generation pushed item. (n could be 0) + var baseData = baseItem.pushAwayData; + var bb = new Rect(baseData.bounds); + + // make the bounds larger, adding a +buffer margin to each side. + bb.inset(-buffer, -buffer); + // bbc = center of the base's bounds + var bbc = bb.center(); + + items.forEach(function Item_pushAway_pushOne_pushEach(item) { + if (item == baseItem) + return; + + var data = item.pushAwayData; + // if the item under consideration has already been pushed, or has a lower + // "generation" (and thus an implictly greater placement priority) then don't move it. + if (data.generation <= baseData.generation) + return; + + // box = this item's current bounds, with a +buffer margin. + var bounds = data.bounds; + var box = new Rect(bounds); + box.inset(-buffer, -buffer); + + // if the item under consideration overlaps with the base item... + if (box.intersects(bb)) { + + // Let's push it a little. + + // First, decide in which direction and how far to push. This is the offset. + var offset = new Point(); + // center = the current item's center. + var center = box.center(); + + // Consider the relationship between the current item (box) + the base item. + // If it's more vertically stacked than "side by side"... + if (Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) { + // push vertically. + if (center.y > bbc.y) + offset.y = bb.bottom - box.top; + else + offset.y = bb.top - box.bottom; + } else { // if they're more "side by side" than stacked vertically... + // push horizontally. + if (center.x > bbc.x) + offset.x = bb.right - box.left; + else + offset.x = bb.left - box.right; + } + + // Actually push the Item. + bounds.offset(offset); + + // This item now becomes an (n+1)-generation pushed item. + data.generation = baseData.generation + 1; + // keep track of who pushed this item. + data.pusher = baseItem; + // add this item to the queue, so that it, in turn, can push some other things. + itemsToPush.push(item); + } + }); + }; + + // push each of the itemsToPush, one at a time. + // itemsToPush starts with just [this], but pushOne can add more items to the stack. + // Maximally, this could run through all Items on the screen. + while (itemsToPush.length) + pushOne(itemsToPush.shift()); + + // ___ Squish! + var pageBounds = Items.getSafeWindowBounds(); + items.forEach(function Item_pushAway_squish(item) { + var data = item.pushAwayData; + if (data.generation == 0) + return; + + let apply = function Item_pushAway_squish_apply(item, posStep, posStep2, sizeStep) { + var data = item.pushAwayData; + if (data.generation == 0) + return; + + var bounds = data.bounds; + bounds.width -= sizeStep.x; + bounds.height -= sizeStep.y; + bounds.left += posStep.x; + bounds.top += posStep.y; + + let validSize; + if (item.isAGroupItem) { + validSize = GroupItems.calcValidSize( + new Point(bounds.width, bounds.height)); + bounds.width = validSize.x; + bounds.height = validSize.y; + } else { + if (sizeStep.y > sizeStep.x) { + validSize = TabItems.calcValidSize(new Point(-1, bounds.height)); + bounds.left += (bounds.width - validSize.x) / 2; + bounds.width = validSize.x; + } else { + validSize = TabItems.calcValidSize(new Point(bounds.width, -1)); + bounds.top += (bounds.height - validSize.y) / 2; + bounds.height = validSize.y; + } + } + + var pusher = data.pusher; + if (pusher) { + var newPosStep = new Point(posStep.x + posStep2.x, posStep.y + posStep2.y); + apply(pusher, newPosStep, posStep2, sizeStep); + } + } + + var bounds = data.bounds; + var posStep = new Point(); + var posStep2 = new Point(); + var sizeStep = new Point(); + + if (bounds.left < pageBounds.left) { + posStep.x = pageBounds.left - bounds.left; + sizeStep.x = posStep.x / data.generation; + posStep2.x = -sizeStep.x; + } else if (bounds.right > pageBounds.right) { // this may be less of a problem post-601534 + posStep.x = pageBounds.right - bounds.right; + sizeStep.x = -posStep.x / data.generation; + posStep.x += sizeStep.x; + posStep2.x = sizeStep.x; + } + + if (bounds.top < pageBounds.top) { + posStep.y = pageBounds.top - bounds.top; + sizeStep.y = posStep.y / data.generation; + posStep2.y = -sizeStep.y; + } else if (bounds.bottom > pageBounds.bottom) { // this may be less of a problem post-601534 + posStep.y = pageBounds.bottom - bounds.bottom; + sizeStep.y = -posStep.y / data.generation; + posStep.y += sizeStep.y; + posStep2.y = sizeStep.y; + } + + if (posStep.x || posStep.y || sizeStep.x || sizeStep.y) + apply(item, posStep, posStep2, sizeStep); + }); + + // ___ Unsquish + var pairs = []; + items.forEach(function Item_pushAway_setupUnsquish(item) { + var data = item.pushAwayData; + pairs.push({ + item: item, + bounds: data.bounds + }); + }); + + Items.unsquish(pairs); + + // ___ Apply changes + items.forEach(function Item_pushAway_setBounds(item) { + var data = item.pushAwayData; + var bounds = data.bounds; + if (!bounds.equals(data.startBounds)) { + item.setBounds(bounds, immediately); + } + }); + }, + + // ---------- + // Function: setTrenches + // Sets up/moves the trenches for snapping to this item. + setTrenches: function Item_setTrenches(rect) { + if (this.parent !== null) + return; + + if (!this.borderTrenches) + this.borderTrenches = Trenches.registerWithItem(this,"border"); + + var bT = this.borderTrenches; + Trenches.getById(bT.left).setWithRect(rect); + Trenches.getById(bT.right).setWithRect(rect); + Trenches.getById(bT.top).setWithRect(rect); + Trenches.getById(bT.bottom).setWithRect(rect); + + if (!this.guideTrenches) + this.guideTrenches = Trenches.registerWithItem(this,"guide"); + + var gT = this.guideTrenches; + Trenches.getById(gT.left).setWithRect(rect); + Trenches.getById(gT.right).setWithRect(rect); + Trenches.getById(gT.top).setWithRect(rect); + Trenches.getById(gT.bottom).setWithRect(rect); + + }, + + // ---------- + // Function: removeTrenches + // Removes the trenches for snapping to this item. + removeTrenches: function Item_removeTrenches() { + for (var edge in this.borderTrenches) { + Trenches.unregister(this.borderTrenches[edge]); // unregister can take an array + } + this.borderTrenches = null; + for (var edge in this.guideTrenches) { + Trenches.unregister(this.guideTrenches[edge]); // unregister can take an array + } + this.guideTrenches = null; + }, + + // ---------- + // Function: snap + // The snap function used during groupItem creation via drag-out + // + // Parameters: + // immediately - bool for having the drag do the final positioning without animation + snap: function Item_snap(immediately) { + // make the snapping work with a wider range! + var defaultRadius = Trenches.defaultRadius; + Trenches.defaultRadius = 2 * defaultRadius; // bump up from 10 to 20! + + var FauxDragInfo = new Drag(this, {}); + FauxDragInfo.snap('none', false); + FauxDragInfo.stop(immediately); + + Trenches.defaultRadius = defaultRadius; + }, + + // ---------- + // Function: draggable + // Enables dragging on this item. Note: not to be called multiple times on the same item! + draggable: function Item_draggable() { + try { + Utils.assert(this.dragOptions, 'dragOptions'); + + var cancelClasses = []; + if (typeof this.dragOptions.cancelClass == 'string') + cancelClasses = this.dragOptions.cancelClass.split(' '); + + var self = this; + var $container = iQ(this.container); + var startMouse; + var startPos; + var startSent; + var startEvent; + var droppables; + var dropTarget; + + // determine the best drop target based on the current mouse coordinates + let determineBestDropTarget = function (e, box) { + // drop events + var best = { + dropTarget: null, + score: 0 + }; + + droppables.forEach(function(droppable) { + var intersection = box.intersection(droppable.bounds); + if (intersection && intersection.area() > best.score) { + var possibleDropTarget = droppable.item; + var accept = true; + if (possibleDropTarget != dropTarget) { + var dropOptions = possibleDropTarget.dropOptions; + if (dropOptions && typeof dropOptions.accept == "function") + accept = dropOptions.accept.apply(possibleDropTarget, [self]); + } + + if (accept) { + best.dropTarget = possibleDropTarget; + best.score = intersection.area(); + } + } + }); + + return best.dropTarget; + } + + // ___ mousemove + var handleMouseMove = function(e) { + // global drag tracking + drag.lastMoveTime = Date.now(); + + // positioning + var mouse = new Point(e.pageX, e.pageY); + if (!startSent) { + if(Math.abs(mouse.x - startMouse.x) > self.dragOptions.minDragDistance || + Math.abs(mouse.y - startMouse.y) > self.dragOptions.minDragDistance) { + if (typeof self.dragOptions.start == "function") + self.dragOptions.start.apply(self, + [startEvent, {position: {left: startPos.x, top: startPos.y}}]); + startSent = true; + } + } + if (startSent) { + // drag events + var box = self.getBounds(); + box.left = startPos.x + (mouse.x - startMouse.x); + box.top = startPos.y + (mouse.y - startMouse.y); + self.setBounds(box, true); + + if (typeof self.dragOptions.drag == "function") + self.dragOptions.drag.apply(self, [e]); + + let bestDropTarget = determineBestDropTarget(e, box); + + if (bestDropTarget != dropTarget) { + var dropOptions; + if (dropTarget) { + dropOptions = dropTarget.dropOptions; + if (dropOptions && typeof dropOptions.out == "function") + dropOptions.out.apply(dropTarget, [e]); + } + + dropTarget = bestDropTarget; + + if (dropTarget) { + dropOptions = dropTarget.dropOptions; + if (dropOptions && typeof dropOptions.over == "function") + dropOptions.over.apply(dropTarget, [e]); + } + } + if (dropTarget) { + dropOptions = dropTarget.dropOptions; + if (dropOptions && typeof dropOptions.move == "function") + dropOptions.move.apply(dropTarget, [e]); + } + } + + e.preventDefault(); + }; + + // ___ mouseup + var handleMouseUp = function(e) { + iQ(gWindow) + .unbind('mousemove', handleMouseMove) + .unbind('mouseup', handleMouseUp); + + if (startSent && dropTarget) { + var dropOptions = dropTarget.dropOptions; + if (dropOptions && typeof dropOptions.drop == "function") + dropOptions.drop.apply(dropTarget, [e]); + } + + if (startSent && typeof self.dragOptions.stop == "function") + self.dragOptions.stop.apply(self, [e]); + + e.preventDefault(); + }; + + // ___ mousedown + $container.mousedown(function(e) { + if (!Utils.isLeftClick(e)) + return; + + var cancel = false; + var $target = iQ(e.target); + cancelClasses.forEach(function(className) { + if ($target.hasClass(className)) + cancel = true; + }); + + if (cancel) { + e.preventDefault(); + return; + } + + startMouse = new Point(e.pageX, e.pageY); + let bounds = self.getBounds(); + startPos = bounds.position(); + startEvent = e; + startSent = false; + + droppables = []; + iQ('.iq-droppable').each(function(elem) { + if (elem != self.container) { + var item = Items.item(elem); + droppables.push({ + item: item, + bounds: item.getBounds() + }); + } + }); + + dropTarget = determineBestDropTarget(e, bounds); + + iQ(gWindow) + .mousemove(handleMouseMove) + .mouseup(handleMouseUp); + + e.preventDefault(); + }); + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // Function: droppable + // Enables or disables dropping on this item. + droppable: function Item_droppable(value) { + try { + var $container = iQ(this.container); + if (value) { + Utils.assert(this.dropOptions, 'dropOptions'); + $container.addClass('iq-droppable'); + } else + $container.removeClass('iq-droppable'); + } catch(e) { + Utils.log(e); + } + }, + + // ---------- + // Function: resizable + // Enables or disables resizing of this item. + resizable: function Item_resizable(value) { + try { + var $container = iQ(this.container); + iQ('.iq-resizable-handle', $container).remove(); + + if (!value) { + $container.removeClass('iq-resizable'); + } else { + Utils.assert(this.resizeOptions, 'resizeOptions'); + + $container.addClass('iq-resizable'); + + var self = this; + var startMouse; + var startSize; + var startAspect; + + // ___ mousemove + var handleMouseMove = function(e) { + // global resize tracking + resize.lastMoveTime = Date.now(); + + var mouse = new Point(e.pageX, e.pageY); + var box = self.getBounds(); + if (UI.rtl) { + var minWidth = (self.resizeOptions.minWidth || 0); + var oldWidth = box.width; + if (minWidth != oldWidth || mouse.x < startMouse.x) { + box.width = Math.max(minWidth, startSize.x - (mouse.x - startMouse.x)); + box.left -= box.width - oldWidth; + } + } else { + box.width = Math.max(self.resizeOptions.minWidth || 0, startSize.x + (mouse.x - startMouse.x)); + } + box.height = Math.max(self.resizeOptions.minHeight || 0, startSize.y + (mouse.y - startMouse.y)); + + if (self.resizeOptions.aspectRatio) { + if (startAspect < 1) + box.height = box.width * startAspect; + else + box.width = box.height / startAspect; + } + + self.setBounds(box, true); + + if (typeof self.resizeOptions.resize == "function") + self.resizeOptions.resize.apply(self, [e]); + + e.preventDefault(); + e.stopPropagation(); + }; + + // ___ mouseup + var handleMouseUp = function(e) { + iQ(gWindow) + .unbind('mousemove', handleMouseMove) + .unbind('mouseup', handleMouseUp); + + if (typeof self.resizeOptions.stop == "function") + self.resizeOptions.stop.apply(self, [e]); + + e.preventDefault(); + e.stopPropagation(); + }; + + // ___ handle + mousedown + iQ('
') + .addClass('iq-resizable-handle iq-resizable-se') + .appendTo($container) + .mousedown(function(e) { + if (!Utils.isLeftClick(e)) + return; + + startMouse = new Point(e.pageX, e.pageY); + startSize = self.getBounds().size(); + startAspect = startSize.y / startSize.x; + + if (typeof self.resizeOptions.start == "function") + self.resizeOptions.start.apply(self, [e]); + + iQ(gWindow) + .mousemove(handleMouseMove) + .mouseup(handleMouseUp); + + e.preventDefault(); + e.stopPropagation(); + }); + } + } catch(e) { + Utils.log(e); + } + } +}; + +// ########## +// Class: Items +// Keeps track of all Items. +let Items = { + // ---------- + // Function: toString + // Prints [Items] for debug use + toString: function Items_toString() { + return "[Items]"; + }, + + // ---------- + // Variable: defaultGutter + // How far apart Items should be from each other and from bounds + defaultGutter: 15, + + // ---------- + // Function: item + // Given a DOM element representing an Item, returns the Item. + item: function Items_item(el) { + return iQ(el).data('item'); + }, + + // ---------- + // Function: getTopLevelItems + // Returns an array of all Items not grouped into groupItems. + getTopLevelItems: function Items_getTopLevelItems() { + var items = []; + + iQ('.tab, .groupItem').each(function(elem) { + var $this = iQ(elem); + var item = $this.data('item'); + if (item && !item.parent && !$this.hasClass('phantom')) + items.push(item); + }); + + return items; + }, + + // ---------- + // Function: getPageBounds + // Returns a defining the area of the page s should stay within. + getPageBounds: function Items_getPageBounds() { + var width = Math.max(100, window.innerWidth); + var height = Math.max(100, window.innerHeight); + return new Rect(0, 0, width, height); + }, + + // ---------- + // Function: getSafeWindowBounds + // Returns the bounds within which it is safe to place all non-stationary s. + getSafeWindowBounds: function Items_getSafeWindowBounds() { + // the safe bounds that would keep it "in the window" + var gutter = Items.defaultGutter; + // Here, I've set the top gutter separately, as the top of the window has its own + // extra chrome which makes a large top gutter unnecessary. + // TODO: set top gutter separately, elsewhere. + var topGutter = 5; + return new Rect(gutter, topGutter, + window.innerWidth - 2 * gutter, window.innerHeight - gutter - topGutter); + + }, + + // ---------- + // Function: arrange + // Arranges the given items in a grid within the given bounds, + // maximizing item size but maintaining standard tab aspect ratio for each + // + // Parameters: + // items - an array of s. Can be null, in which case we won't + // actually move anything. + // bounds - a defining the space to arrange within + // options - an object with various properites (see below) + // + // Possible "options" properties: + // animate - whether to animate; default: true. + // z - the z index to set all the items; default: don't change z. + // return - if set to 'widthAndColumns', it'll return an object with the + // width of children and the columns. + // count - overrides the item count for layout purposes; + // default: the actual item count + // columns - (int) a preset number of columns to use + // dropPos - a which should have a one-tab space left open, used + // when a tab is dragged over. + // + // Returns: + // By default, an object with three properties: `rects`, the list of s, + // `dropIndex`, the index which a dragged tab should have if dropped + // (null if no `dropPos` was specified), and the number of columns (`columns`). + // If the `return` option is set to 'widthAndColumns', an object with the + // width value of the child items (`childWidth`) and the number of columns + // (`columns`) is returned. + arrange: function Items_arrange(items, bounds, options) { + if (!options) + options = {}; + var animate = "animate" in options ? options.animate : true; + var immediately = !animate; + + var rects = []; + + var count = options.count || (items ? items.length : 0); + if (options.addTab) + count++; + if (!count) { + let dropIndex = (Utils.isPoint(options.dropPos)) ? 0 : null; + return {rects: rects, dropIndex: dropIndex}; + } + + var columns = options.columns || 1; + // We'll assume for the time being that all the items have the same styling + // and that the margin is the same width around. + var itemMargin = items && items.length ? + parseInt(iQ(items[0].container).css('margin-left')) : 0; + var padding = itemMargin * 2; + var rows; + var tabWidth; + var tabHeight; + var totalHeight; + + function figure() { + rows = Math.ceil(count / columns); + let validSize = TabItems.calcValidSize( + new Point((bounds.width - (padding * columns)) / columns, -1), + options); + tabWidth = validSize.x; + tabHeight = validSize.y; + + totalHeight = (tabHeight * rows) + (padding * rows); + } + + figure(); + + while (rows > 1 && totalHeight > bounds.height) { + columns++; + figure(); + } + + if (rows == 1) { + let validSize = TabItems.calcValidSize(new Point(tabWidth, + bounds.height - 2 * itemMargin), options); + tabWidth = validSize.x; + tabHeight = validSize.y; + } + + if (options.return == 'widthAndColumns') + return {childWidth: tabWidth, columns: columns}; + + let initialOffset = 0; + if (UI.rtl) { + initialOffset = bounds.width - tabWidth - padding; + } + var box = new Rect(bounds.left + initialOffset, bounds.top, tabWidth, tabHeight); + + var column = 0; + + var dropIndex = false; + var dropRect = false; + if (Utils.isPoint(options.dropPos)) + dropRect = new Rect(options.dropPos.x, options.dropPos.y, 1, 1); + for (let a = 0; a < count; a++) { + // If we had a dropPos, see if this is where we should place it + if (dropRect) { + let activeBox = new Rect(box); + activeBox.inset(-itemMargin - 1, -itemMargin - 1); + // if the designated position (dropRect) is within the active box, + // this is where, if we drop the tab being dragged, it should land! + if (activeBox.contains(dropRect)) + dropIndex = a; + } + + // record the box. + rects.push(new Rect(box)); + + box.left += (UI.rtl ? -1 : 1) * (box.width + padding); + column++; + if (column == columns) { + box.left = bounds.left + initialOffset; + box.top += box.height + padding; + column = 0; + } + } + + return {rects: rects, dropIndex: dropIndex, columns: columns}; + }, + + // ---------- + // Function: unsquish + // Checks to see which items can now be unsquished. + // + // Parameters: + // pairs - an array of objects, each with two properties: item and bounds. The bounds are + // modified as appropriate, but the items are not changed. If pairs is null, the + // operation is performed directly on all of the top level items. + // ignore - an to not include in calculations (because it's about to be closed, for instance) + unsquish: function Items_unsquish(pairs, ignore) { + var pairsProvided = (pairs ? true : false); + if (!pairsProvided) { + var items = Items.getTopLevelItems(); + pairs = []; + items.forEach(function(item) { + pairs.push({ + item: item, + bounds: item.getBounds() + }); + }); + } + + var pageBounds = Items.getSafeWindowBounds(); + pairs.forEach(function(pair) { + var item = pair.item; + if (item == ignore) + return; + + var bounds = pair.bounds; + var newBounds = new Rect(bounds); + + var newSize; + if (Utils.isPoint(item.userSize)) + newSize = new Point(item.userSize); + else if (item.isAGroupItem) + newSize = GroupItems.calcValidSize( + new Point(GroupItems.minGroupWidth, -1)); + else + newSize = TabItems.calcValidSize( + new Point(TabItems.tabWidth, -1)); + + if (item.isAGroupItem) { + newBounds.width = Math.max(newBounds.width, newSize.x); + newBounds.height = Math.max(newBounds.height, newSize.y); + } else { + if (bounds.width < newSize.x) { + newBounds.width = newSize.x; + newBounds.height = newSize.y; + } + } + + newBounds.left -= (newBounds.width - bounds.width) / 2; + newBounds.top -= (newBounds.height - bounds.height) / 2; + + var offset = new Point(); + if (newBounds.left < pageBounds.left) + offset.x = pageBounds.left - newBounds.left; + else if (newBounds.right > pageBounds.right) + offset.x = pageBounds.right - newBounds.right; + + if (newBounds.top < pageBounds.top) + offset.y = pageBounds.top - newBounds.top; + else if (newBounds.bottom > pageBounds.bottom) + offset.y = pageBounds.bottom - newBounds.bottom; + + newBounds.offset(offset); + + if (!bounds.equals(newBounds)) { + var blocked = false; + pairs.forEach(function(pair2) { + if (pair2 == pair || pair2.item == ignore) + return; + + var bounds2 = pair2.bounds; + if (bounds2.intersects(newBounds)) + blocked = true; + return; + }); + + if (!blocked) { + pair.bounds.copy(newBounds); + } + } + return; + }); + + if (!pairsProvided) { + pairs.forEach(function(pair) { + pair.item.setBounds(pair.bounds); + }); + } + } +};