// Title: items.js (revision-a) // ########## // Class: Item // Superclass for all visible objects ( and s). // If you subclass, in addition to the things Item provides, you need to also // provide these methods: // reloadBounds: function() // setBounds: function(rect, immediately) // setZ: function(value) // close: function() // addOnClose: function(referenceObject, callback) // removeOnClose: function(referenceObject) // ... and this property: // defaultSize, a Point // Make sure to call _init() from your subclass's constructor. window.Item = function() { // Variable: isAnItem // Always true for Items this.isAnItem = true; // Variable: bounds // The position and size of this Item, represented as a . this.bounds = null; // Variable: debug // When set to true, displays a rectangle on the screen that corresponds with bounds. // May be used for additional debugging features in the future. this.debug = false; // Variable: $debug // If is true, this will be the jQuery object for the visible rectangle. this.$debug = null; // Variable: container // The outermost DOM element that describes this item on screen. this.container = null; // Variable: locked // Affects whether an item can be pushed, closed, renamed, etc this.locked = false; // Variable: parent // The group 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: locked // True if the item should not be changed. this.locked = false; }; window.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(container) { Utils.assert('container must be a DOM element', Utils.isDOMElement(container)); Utils.assert('Subclass must provide reloadBounds', typeof(this.reloadBounds) == 'function'); Utils.assert('Subclass must provide setBounds', typeof(this.setBounds) == 'function'); Utils.assert('Subclass must provide setZ', typeof(this.setZ) == 'function'); Utils.assert('Subclass must provide close', typeof(this.close) == 'function'); Utils.assert('Subclass must provide addOnClose', typeof(this.addOnClose) == 'function'); Utils.assert('Subclass must provide removeOnClose', typeof(this.removeOnClose) == 'function'); Utils.assert('Subclass must provide defaultSize', this.defaultSize); this.container = container; if(this.debug) { this.$debug = $('
') .css({ border: '2px solid green', zIndex: -10, position: 'absolute' }) .appendTo($('body')); } this.reloadBounds(); Utils.assert('reloadBounds must set up this.bounds', this.bounds); $(this.container).data('item', this); }, // ---------- // Function: getBounds // Returns a copy of the Item's bounds as a . getBounds: function() { return new Rect(this.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(left, top, immediately) { 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(width, height, immediately) { 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() { this.userSize = new Point(this.bounds.width, this.bounds.height); }, // ---------- // Function: getZ // Returns the zIndex of the Item. getZ: function() { return parseInt($(this.container).css('zIndex')); }, // ---------- // Function: setRotation // Rotates the object to the given number of degrees. setRotation: function(degrees) { var value = "rotate(%deg)".replace(/%/, degrees); $(this.container).css({"-moz-transform": value}); }, // ---------- // Function: pushAway // Pushes all other items away so none overlap this Item. pushAway: function() { var buffer = 10; var items = Items.getTopLevelItems(); $.each(items, function(index, item) { var data = {}; data.bounds = item.getBounds(); data.startBounds = new Rect(data.bounds); data.generation = Infinity; item.pushAwayData = data; }); var itemsToPush = [this]; this.pushAwayData.generation = 0; var pushOne = function(baseItem) { var baseData = baseItem.pushAwayData; var bb = new Rect(baseData.bounds); bb.inset(-buffer, -buffer); var bbc = bb.center(); $.each(items, function(index, item) { if(item == baseItem || item.locked) return; var data = item.pushAwayData; if(data.generation <= baseData.generation) return; var bounds = data.bounds; var box = new Rect(bounds); box.inset(-buffer, -buffer); if(box.intersects(bb)) { var offset = new Point(); var center = box.center(); if(Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) { /* offset.x = Math.floor((Math.random() * 10) - 5); */ if(center.y > bbc.y) offset.y = bb.bottom - box.top; else offset.y = bb.top - box.bottom; } else { /* offset.y = Math.floor((Math.random() * 10) - 5); */ if(center.x > bbc.x) offset.x = bb.right - box.left; else offset.x = bb.left - box.right; } bounds.offset(offset); data.generation = baseData.generation + 1; data.pusher = baseItem; itemsToPush.push(item); } }); }; while(itemsToPush.length) pushOne(itemsToPush.shift()); // ___ Squish! var pageBounds = Items.getPageBounds(); if(Items.squishMode == 'squish') { $.each(items, function(index, item) { var data = item.pushAwayData; if(data.generation == 0 || item.locked) return; function 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; if(!item.isAGroup) { if(sizeStep.y > sizeStep.x) { var newWidth = bounds.height * (TabItems.tabWidth / TabItems.tabHeight); bounds.left += (bounds.width - newWidth) / 2; bounds.width = newWidth; } else { var newHeight = bounds.width * (TabItems.tabHeight / TabItems.tabWidth); bounds.top += (bounds.height - newHeight) / 2; bounds.height = newHeight; } } var pusher = data.pusher; if(pusher) apply(pusher, posStep.plus(posStep2), 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) { 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) { 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); }); } else if(Items.squishMode == 'all') { var newPageBounds = null; $.each(items, function(index, item) { if(item.locked) return; var data = item.pushAwayData; var bounds = data.bounds; newPageBounds = (newPageBounds ? newPageBounds.union(bounds) : new Rect(bounds)); }); var wScale = pageBounds.width / newPageBounds.width; var hScale = pageBounds.height / newPageBounds.height; var scale = Math.min(hScale, wScale); $.each(items, function(index, item) { if(item.locked) return; var data = item.pushAwayData; var bounds = data.bounds; bounds.left -= newPageBounds.left; bounds.left *= scale; bounds.width *= scale; bounds.top -= newPageBounds.top; bounds.top *= scale; bounds.height *= scale; }); } // ___ Unsquish $.each(items, function(index, item) { if(item.locked) return; var data = item.pushAwayData; var bounds = data.bounds; var newBounds = new Rect(bounds); var newSize; if(item.userSize) newSize = new Point(item.userSize); else newSize = new Point(TabItems.tabWidth, TabItems.tabHeight); if(item.isAGroup) { 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; $.each(items, function(index, item2) { if(item2 == item) return; var data2 = item2.pushAwayData; var bounds2 = data2.bounds; if(bounds2.intersects(newBounds)) { blocked = true; return false; } }); if(!blocked) { data.bounds = newBounds; } } }); // ___ Apply changes $.each(items, function(index, item) { var data = item.pushAwayData; var bounds = data.bounds; if(!bounds.equals(data.startBounds)) { item.setBounds(bounds); } }); }, // ---------- // Function: _updateDebugBounds // Called by a subclass when its bounds change, to update the debugging rectangles on screen. // This functionality is enabled only by the debug property. _updateDebugBounds: function() { if(this.$debug) { this.$debug.css({ left: this.bounds.left, top: this.bounds.top, width: this.bounds.width, height: this.bounds.height }); } } }; // ########## // Class: Items // Keeps track of all Items. window.Items = { // ---------- // Variable: squishMode // How to deal when things go off the edge. // Options include: all, squish, push squishMode: 'squish', // ---------- // Function: init // Initialize the object init: function() { }, // ---------- // Function: item // Given a DOM element representing an Item, returns the Item. item: function(el) { return $(el).data('item'); }, // ---------- // Function: getTopLevelItems // Returns an array of all Items not grouped into groups. getTopLevelItems: function() { var items = []; $('.tab, .group').each(function() { $this = $(this); var item = $this.data('item'); if(!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() { var top = 20; var bottom = 20;//TabItems.tabHeight + 10; // MAGIC NUMBER: giving room for the "new tabs" group var width = Math.max(100, window.innerWidth); var height = Math.max(100, window.innerHeight - (top + bottom)); return new Rect(0, top, width, height); }, // ---------- // 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 // bounds - a defining the space to arrange within // options - an object with options. If options.animate is false, doesn't animate, otherwise it does. // If options.z is defined, set all of the items to that z, otherwise leave their z alone. arrange: function(items, bounds, options) { var animate; if(!options || typeof(options.animate) == 'undefined') animate = true; else animate = options.animate; if(typeof(options) == 'undefined') options = {}; var tabAspect = TabItems.tabHeight / TabItems.tabWidth; var count = items.length; if(!count) return; var columns = 1; var padding = options.padding || 0; var yScale = 1.1; // to allow for titles var rows; var tabWidth; var tabHeight; var totalHeight; function figure() { rows = Math.ceil(count / columns); tabWidth = (bounds.width - (padding * (columns - 1))) / columns; tabHeight = tabWidth * tabAspect; totalHeight = (tabHeight * yScale * rows) + (padding * (rows - 1)); } figure(); while(rows > 1 && totalHeight > bounds.height) { columns++; figure(); } if(rows == 1) { tabWidth = Math.min(bounds.width / Math.max(2, count), bounds.height / tabAspect); tabHeight = tabWidth * tabAspect; } var box = new Rect(bounds.left, bounds.top, tabWidth, tabHeight); var row = 0; var column = 0; var immediately; $.each(items, function(index, item) { /* if(animate == 'sometimes') immediately = (typeof(item.groupData.row) == 'undefined' || item.groupData.row == row); else */ immediately = !animate; if(!item.locked) { item.setBounds(box, immediately); item.setRotation(0); if(options.z) item.setZ(options.z); } /* item.groupData.column = column; item.groupData.row = row; */ box.left += box.width + padding; column++; if(column == columns) { box.left = bounds.left; box.top += (box.height * yScale) + padding; column = 0; row++; } }); } }; window.Items.init();