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