1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/tabview/items.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1077 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +// ********** 1.9 +// Title: items.js 1.10 + 1.11 +// ########## 1.12 +// Class: Item 1.13 +// Superclass for all visible objects (<TabItem>s and <GroupItem>s). 1.14 +// 1.15 +// If you subclass, in addition to the things Item provides, you need to also provide these methods: 1.16 +// setBounds - function(rect, immediately, options) 1.17 +// setZ - function(value) 1.18 +// close - function() 1.19 +// save - function() 1.20 +// 1.21 +// Subclasses of Item must also provide the <Subscribable> interface. 1.22 +// 1.23 +// Make sure to call _init() from your subclass's constructor. 1.24 +function Item() { 1.25 + // Variable: isAnItem 1.26 + // Always true for Items 1.27 + this.isAnItem = true; 1.28 + 1.29 + // Variable: bounds 1.30 + // The position and size of this Item, represented as a <Rect>. 1.31 + // This should never be modified without using setBounds() 1.32 + this.bounds = null; 1.33 + 1.34 + // Variable: zIndex 1.35 + // The z-index for this item. 1.36 + this.zIndex = 0; 1.37 + 1.38 + // Variable: container 1.39 + // The outermost DOM element that describes this item on screen. 1.40 + this.container = null; 1.41 + 1.42 + // Variable: parent 1.43 + // The groupItem that this item is a child of 1.44 + this.parent = null; 1.45 + 1.46 + // Variable: userSize 1.47 + // A <Point> that describes the last size specifically chosen by the user. 1.48 + // Used by unsquish. 1.49 + this.userSize = null; 1.50 + 1.51 + // Variable: dragOptions 1.52 + // Used by <draggable> 1.53 + // 1.54 + // Possible properties: 1.55 + // cancelClass - A space-delimited list of classes that should cancel a drag 1.56 + // start - A function to be called when a drag starts 1.57 + // drag - A function to be called each time the mouse moves during drag 1.58 + // stop - A function to be called when the drag is done 1.59 + this.dragOptions = null; 1.60 + 1.61 + // Variable: dropOptions 1.62 + // Used by <draggable> if the item is set to droppable. 1.63 + // 1.64 + // Possible properties: 1.65 + // accept - A function to determine if a particular item should be accepted for dropping 1.66 + // over - A function to be called when an item is over this item 1.67 + // out - A function to be called when an item leaves this item 1.68 + // drop - A function to be called when an item is dropped in this item 1.69 + this.dropOptions = null; 1.70 + 1.71 + // Variable: resizeOptions 1.72 + // Used by <resizable> 1.73 + // 1.74 + // Possible properties: 1.75 + // minWidth - Minimum width allowable during resize 1.76 + // minHeight - Minimum height allowable during resize 1.77 + // aspectRatio - true if we should respect aspect ratio; default false 1.78 + // start - A function to be called when resizing starts 1.79 + // resize - A function to be called each time the mouse moves during resize 1.80 + // stop - A function to be called when the resize is done 1.81 + this.resizeOptions = null; 1.82 + 1.83 + // Variable: isDragging 1.84 + // Boolean for whether the item is currently being dragged or not. 1.85 + this.isDragging = false; 1.86 +}; 1.87 + 1.88 +Item.prototype = { 1.89 + // ---------- 1.90 + // Function: _init 1.91 + // Initializes the object. To be called from the subclass's intialization function. 1.92 + // 1.93 + // Parameters: 1.94 + // container - the outermost DOM element that describes this item onscreen. 1.95 + _init: function Item__init(container) { 1.96 + Utils.assert(typeof this.addSubscriber == 'function' && 1.97 + typeof this.removeSubscriber == 'function' && 1.98 + typeof this._sendToSubscribers == 'function', 1.99 + 'Subclass must implement the Subscribable interface'); 1.100 + Utils.assert(Utils.isDOMElement(container), 'container must be a DOM element'); 1.101 + Utils.assert(typeof this.setBounds == 'function', 'Subclass must provide setBounds'); 1.102 + Utils.assert(typeof this.setZ == 'function', 'Subclass must provide setZ'); 1.103 + Utils.assert(typeof this.close == 'function', 'Subclass must provide close'); 1.104 + Utils.assert(typeof this.save == 'function', 'Subclass must provide save'); 1.105 + Utils.assert(Utils.isRect(this.bounds), 'Subclass must provide bounds'); 1.106 + 1.107 + this.container = container; 1.108 + this.$container = iQ(container); 1.109 + 1.110 + iQ(this.container).data('item', this); 1.111 + 1.112 + // ___ drag 1.113 + this.dragOptions = { 1.114 + cancelClass: 'close stackExpander', 1.115 + start: function(e, ui) { 1.116 + UI.setActive(this); 1.117 + if (this.isAGroupItem) 1.118 + this._unfreezeItemSize(); 1.119 + // if we start dragging a tab within a group, start with dropSpace on. 1.120 + else if (this.parent != null) 1.121 + this.parent._dropSpaceActive = true; 1.122 + drag.info = new Drag(this, e); 1.123 + }, 1.124 + drag: function(e) { 1.125 + drag.info.drag(e); 1.126 + }, 1.127 + stop: function() { 1.128 + drag.info.stop(); 1.129 + 1.130 + if (!this.isAGroupItem && !this.parent) { 1.131 + new GroupItem([drag.info.$el], {focusTitle: true}); 1.132 + gTabView.firstUseExperienced = true; 1.133 + } 1.134 + 1.135 + drag.info = null; 1.136 + }, 1.137 + // The minimum the mouse must move after mouseDown in order to move an 1.138 + // item 1.139 + minDragDistance: 3 1.140 + }; 1.141 + 1.142 + // ___ drop 1.143 + this.dropOptions = { 1.144 + over: function() {}, 1.145 + out: function() { 1.146 + let groupItem = drag.info.item.parent; 1.147 + if (groupItem) 1.148 + groupItem.remove(drag.info.$el, {dontClose: true}); 1.149 + iQ(this.container).removeClass("acceptsDrop"); 1.150 + }, 1.151 + drop: function(event) { 1.152 + iQ(this.container).removeClass("acceptsDrop"); 1.153 + }, 1.154 + // Function: dropAcceptFunction 1.155 + // Given a DOM element, returns true if it should accept tabs being dropped on it. 1.156 + // Private to this file. 1.157 + accept: function dropAcceptFunction(item) { 1.158 + return (item && item.isATabItem && (!item.parent || !item.parent.expanded)); 1.159 + } 1.160 + }; 1.161 + 1.162 + // ___ resize 1.163 + var self = this; 1.164 + this.resizeOptions = { 1.165 + aspectRatio: self.keepProportional, 1.166 + minWidth: 90, 1.167 + minHeight: 90, 1.168 + start: function(e,ui) { 1.169 + UI.setActive(this); 1.170 + resize.info = new Drag(this, e); 1.171 + }, 1.172 + resize: function(e,ui) { 1.173 + resize.info.snap(UI.rtl ? 'topright' : 'topleft', false, self.keepProportional); 1.174 + }, 1.175 + stop: function() { 1.176 + self.setUserSize(); 1.177 + self.pushAway(); 1.178 + resize.info.stop(); 1.179 + resize.info = null; 1.180 + } 1.181 + }; 1.182 + }, 1.183 + 1.184 + // ---------- 1.185 + // Function: getBounds 1.186 + // Returns a copy of the Item's bounds as a <Rect>. 1.187 + getBounds: function Item_getBounds() { 1.188 + Utils.assert(Utils.isRect(this.bounds), 'this.bounds should be a rect'); 1.189 + return new Rect(this.bounds); 1.190 + }, 1.191 + 1.192 + // ---------- 1.193 + // Function: overlapsWithOtherItems 1.194 + // Returns true if this Item overlaps with any other Item on the screen. 1.195 + overlapsWithOtherItems: function Item_overlapsWithOtherItems() { 1.196 + var self = this; 1.197 + var items = Items.getTopLevelItems(); 1.198 + var bounds = this.getBounds(); 1.199 + return items.some(function(item) { 1.200 + if (item == self) // can't overlap with yourself. 1.201 + return false; 1.202 + var myBounds = item.getBounds(); 1.203 + return myBounds.intersects(bounds); 1.204 + } ); 1.205 + }, 1.206 + 1.207 + // ---------- 1.208 + // Function: setPosition 1.209 + // Moves the Item to the specified location. 1.210 + // 1.211 + // Parameters: 1.212 + // left - the new left coordinate relative to the window 1.213 + // top - the new top coordinate relative to the window 1.214 + // immediately - if false or omitted, animates to the new position; 1.215 + // otherwise goes there immediately 1.216 + setPosition: function Item_setPosition(left, top, immediately) { 1.217 + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); 1.218 + this.setBounds(new Rect(left, top, this.bounds.width, this.bounds.height), immediately); 1.219 + }, 1.220 + 1.221 + // ---------- 1.222 + // Function: setSize 1.223 + // Resizes the Item to the specified size. 1.224 + // 1.225 + // Parameters: 1.226 + // width - the new width in pixels 1.227 + // height - the new height in pixels 1.228 + // immediately - if false or omitted, animates to the new size; 1.229 + // otherwise resizes immediately 1.230 + setSize: function Item_setSize(width, height, immediately) { 1.231 + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); 1.232 + this.setBounds(new Rect(this.bounds.left, this.bounds.top, width, height), immediately); 1.233 + }, 1.234 + 1.235 + // ---------- 1.236 + // Function: setUserSize 1.237 + // Remembers the current size as one the user has chosen. 1.238 + setUserSize: function Item_setUserSize() { 1.239 + Utils.assert(Utils.isRect(this.bounds), 'this.bounds'); 1.240 + this.userSize = new Point(this.bounds.width, this.bounds.height); 1.241 + this.save(); 1.242 + }, 1.243 + 1.244 + // ---------- 1.245 + // Function: getZ 1.246 + // Returns the zIndex of the Item. 1.247 + getZ: function Item_getZ() { 1.248 + return this.zIndex; 1.249 + }, 1.250 + 1.251 + // ---------- 1.252 + // Function: setRotation 1.253 + // Rotates the object to the given number of degrees. 1.254 + setRotation: function Item_setRotation(degrees) { 1.255 + var value = degrees ? "rotate(%deg)".replace(/%/, degrees) : null; 1.256 + iQ(this.container).css({"transform": value}); 1.257 + }, 1.258 + 1.259 + // ---------- 1.260 + // Function: setParent 1.261 + // Sets the receiver's parent to the given <Item>. 1.262 + setParent: function Item_setParent(parent) { 1.263 + this.parent = parent; 1.264 + this.removeTrenches(); 1.265 + this.save(); 1.266 + }, 1.267 + 1.268 + // ---------- 1.269 + // Function: pushAway 1.270 + // Pushes all other items away so none overlap this Item. 1.271 + // 1.272 + // Parameters: 1.273 + // immediately - boolean for doing the pushAway without animation 1.274 + pushAway: function Item_pushAway(immediately) { 1.275 + var items = Items.getTopLevelItems(); 1.276 + 1.277 + // we need at least two top-level items to push something away 1.278 + if (items.length < 2) 1.279 + return; 1.280 + 1.281 + var buffer = Math.floor(Items.defaultGutter / 2); 1.282 + 1.283 + // setup each Item's pushAwayData attribute: 1.284 + items.forEach(function pushAway_setupPushAwayData(item) { 1.285 + var data = {}; 1.286 + data.bounds = item.getBounds(); 1.287 + data.startBounds = new Rect(data.bounds); 1.288 + // Infinity = (as yet) unaffected 1.289 + data.generation = Infinity; 1.290 + item.pushAwayData = data; 1.291 + }); 1.292 + 1.293 + // The first item is a 0-generation pushed item. It all starts here. 1.294 + var itemsToPush = [this]; 1.295 + this.pushAwayData.generation = 0; 1.296 + 1.297 + var pushOne = function Item_pushAway_pushOne(baseItem) { 1.298 + // the baseItem is an n-generation pushed item. (n could be 0) 1.299 + var baseData = baseItem.pushAwayData; 1.300 + var bb = new Rect(baseData.bounds); 1.301 + 1.302 + // make the bounds larger, adding a +buffer margin to each side. 1.303 + bb.inset(-buffer, -buffer); 1.304 + // bbc = center of the base's bounds 1.305 + var bbc = bb.center(); 1.306 + 1.307 + items.forEach(function Item_pushAway_pushOne_pushEach(item) { 1.308 + if (item == baseItem) 1.309 + return; 1.310 + 1.311 + var data = item.pushAwayData; 1.312 + // if the item under consideration has already been pushed, or has a lower 1.313 + // "generation" (and thus an implictly greater placement priority) then don't move it. 1.314 + if (data.generation <= baseData.generation) 1.315 + return; 1.316 + 1.317 + // box = this item's current bounds, with a +buffer margin. 1.318 + var bounds = data.bounds; 1.319 + var box = new Rect(bounds); 1.320 + box.inset(-buffer, -buffer); 1.321 + 1.322 + // if the item under consideration overlaps with the base item... 1.323 + if (box.intersects(bb)) { 1.324 + 1.325 + // Let's push it a little. 1.326 + 1.327 + // First, decide in which direction and how far to push. This is the offset. 1.328 + var offset = new Point(); 1.329 + // center = the current item's center. 1.330 + var center = box.center(); 1.331 + 1.332 + // Consider the relationship between the current item (box) + the base item. 1.333 + // If it's more vertically stacked than "side by side"... 1.334 + if (Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) { 1.335 + // push vertically. 1.336 + if (center.y > bbc.y) 1.337 + offset.y = bb.bottom - box.top; 1.338 + else 1.339 + offset.y = bb.top - box.bottom; 1.340 + } else { // if they're more "side by side" than stacked vertically... 1.341 + // push horizontally. 1.342 + if (center.x > bbc.x) 1.343 + offset.x = bb.right - box.left; 1.344 + else 1.345 + offset.x = bb.left - box.right; 1.346 + } 1.347 + 1.348 + // Actually push the Item. 1.349 + bounds.offset(offset); 1.350 + 1.351 + // This item now becomes an (n+1)-generation pushed item. 1.352 + data.generation = baseData.generation + 1; 1.353 + // keep track of who pushed this item. 1.354 + data.pusher = baseItem; 1.355 + // add this item to the queue, so that it, in turn, can push some other things. 1.356 + itemsToPush.push(item); 1.357 + } 1.358 + }); 1.359 + }; 1.360 + 1.361 + // push each of the itemsToPush, one at a time. 1.362 + // itemsToPush starts with just [this], but pushOne can add more items to the stack. 1.363 + // Maximally, this could run through all Items on the screen. 1.364 + while (itemsToPush.length) 1.365 + pushOne(itemsToPush.shift()); 1.366 + 1.367 + // ___ Squish! 1.368 + var pageBounds = Items.getSafeWindowBounds(); 1.369 + items.forEach(function Item_pushAway_squish(item) { 1.370 + var data = item.pushAwayData; 1.371 + if (data.generation == 0) 1.372 + return; 1.373 + 1.374 + let apply = function Item_pushAway_squish_apply(item, posStep, posStep2, sizeStep) { 1.375 + var data = item.pushAwayData; 1.376 + if (data.generation == 0) 1.377 + return; 1.378 + 1.379 + var bounds = data.bounds; 1.380 + bounds.width -= sizeStep.x; 1.381 + bounds.height -= sizeStep.y; 1.382 + bounds.left += posStep.x; 1.383 + bounds.top += posStep.y; 1.384 + 1.385 + let validSize; 1.386 + if (item.isAGroupItem) { 1.387 + validSize = GroupItems.calcValidSize( 1.388 + new Point(bounds.width, bounds.height)); 1.389 + bounds.width = validSize.x; 1.390 + bounds.height = validSize.y; 1.391 + } else { 1.392 + if (sizeStep.y > sizeStep.x) { 1.393 + validSize = TabItems.calcValidSize(new Point(-1, bounds.height)); 1.394 + bounds.left += (bounds.width - validSize.x) / 2; 1.395 + bounds.width = validSize.x; 1.396 + } else { 1.397 + validSize = TabItems.calcValidSize(new Point(bounds.width, -1)); 1.398 + bounds.top += (bounds.height - validSize.y) / 2; 1.399 + bounds.height = validSize.y; 1.400 + } 1.401 + } 1.402 + 1.403 + var pusher = data.pusher; 1.404 + if (pusher) { 1.405 + var newPosStep = new Point(posStep.x + posStep2.x, posStep.y + posStep2.y); 1.406 + apply(pusher, newPosStep, posStep2, sizeStep); 1.407 + } 1.408 + } 1.409 + 1.410 + var bounds = data.bounds; 1.411 + var posStep = new Point(); 1.412 + var posStep2 = new Point(); 1.413 + var sizeStep = new Point(); 1.414 + 1.415 + if (bounds.left < pageBounds.left) { 1.416 + posStep.x = pageBounds.left - bounds.left; 1.417 + sizeStep.x = posStep.x / data.generation; 1.418 + posStep2.x = -sizeStep.x; 1.419 + } else if (bounds.right > pageBounds.right) { // this may be less of a problem post-601534 1.420 + posStep.x = pageBounds.right - bounds.right; 1.421 + sizeStep.x = -posStep.x / data.generation; 1.422 + posStep.x += sizeStep.x; 1.423 + posStep2.x = sizeStep.x; 1.424 + } 1.425 + 1.426 + if (bounds.top < pageBounds.top) { 1.427 + posStep.y = pageBounds.top - bounds.top; 1.428 + sizeStep.y = posStep.y / data.generation; 1.429 + posStep2.y = -sizeStep.y; 1.430 + } else if (bounds.bottom > pageBounds.bottom) { // this may be less of a problem post-601534 1.431 + posStep.y = pageBounds.bottom - bounds.bottom; 1.432 + sizeStep.y = -posStep.y / data.generation; 1.433 + posStep.y += sizeStep.y; 1.434 + posStep2.y = sizeStep.y; 1.435 + } 1.436 + 1.437 + if (posStep.x || posStep.y || sizeStep.x || sizeStep.y) 1.438 + apply(item, posStep, posStep2, sizeStep); 1.439 + }); 1.440 + 1.441 + // ___ Unsquish 1.442 + var pairs = []; 1.443 + items.forEach(function Item_pushAway_setupUnsquish(item) { 1.444 + var data = item.pushAwayData; 1.445 + pairs.push({ 1.446 + item: item, 1.447 + bounds: data.bounds 1.448 + }); 1.449 + }); 1.450 + 1.451 + Items.unsquish(pairs); 1.452 + 1.453 + // ___ Apply changes 1.454 + items.forEach(function Item_pushAway_setBounds(item) { 1.455 + var data = item.pushAwayData; 1.456 + var bounds = data.bounds; 1.457 + if (!bounds.equals(data.startBounds)) { 1.458 + item.setBounds(bounds, immediately); 1.459 + } 1.460 + }); 1.461 + }, 1.462 + 1.463 + // ---------- 1.464 + // Function: setTrenches 1.465 + // Sets up/moves the trenches for snapping to this item. 1.466 + setTrenches: function Item_setTrenches(rect) { 1.467 + if (this.parent !== null) 1.468 + return; 1.469 + 1.470 + if (!this.borderTrenches) 1.471 + this.borderTrenches = Trenches.registerWithItem(this,"border"); 1.472 + 1.473 + var bT = this.borderTrenches; 1.474 + Trenches.getById(bT.left).setWithRect(rect); 1.475 + Trenches.getById(bT.right).setWithRect(rect); 1.476 + Trenches.getById(bT.top).setWithRect(rect); 1.477 + Trenches.getById(bT.bottom).setWithRect(rect); 1.478 + 1.479 + if (!this.guideTrenches) 1.480 + this.guideTrenches = Trenches.registerWithItem(this,"guide"); 1.481 + 1.482 + var gT = this.guideTrenches; 1.483 + Trenches.getById(gT.left).setWithRect(rect); 1.484 + Trenches.getById(gT.right).setWithRect(rect); 1.485 + Trenches.getById(gT.top).setWithRect(rect); 1.486 + Trenches.getById(gT.bottom).setWithRect(rect); 1.487 + 1.488 + }, 1.489 + 1.490 + // ---------- 1.491 + // Function: removeTrenches 1.492 + // Removes the trenches for snapping to this item. 1.493 + removeTrenches: function Item_removeTrenches() { 1.494 + for (var edge in this.borderTrenches) { 1.495 + Trenches.unregister(this.borderTrenches[edge]); // unregister can take an array 1.496 + } 1.497 + this.borderTrenches = null; 1.498 + for (var edge in this.guideTrenches) { 1.499 + Trenches.unregister(this.guideTrenches[edge]); // unregister can take an array 1.500 + } 1.501 + this.guideTrenches = null; 1.502 + }, 1.503 + 1.504 + // ---------- 1.505 + // Function: snap 1.506 + // The snap function used during groupItem creation via drag-out 1.507 + // 1.508 + // Parameters: 1.509 + // immediately - bool for having the drag do the final positioning without animation 1.510 + snap: function Item_snap(immediately) { 1.511 + // make the snapping work with a wider range! 1.512 + var defaultRadius = Trenches.defaultRadius; 1.513 + Trenches.defaultRadius = 2 * defaultRadius; // bump up from 10 to 20! 1.514 + 1.515 + var FauxDragInfo = new Drag(this, {}); 1.516 + FauxDragInfo.snap('none', false); 1.517 + FauxDragInfo.stop(immediately); 1.518 + 1.519 + Trenches.defaultRadius = defaultRadius; 1.520 + }, 1.521 + 1.522 + // ---------- 1.523 + // Function: draggable 1.524 + // Enables dragging on this item. Note: not to be called multiple times on the same item! 1.525 + draggable: function Item_draggable() { 1.526 + try { 1.527 + Utils.assert(this.dragOptions, 'dragOptions'); 1.528 + 1.529 + var cancelClasses = []; 1.530 + if (typeof this.dragOptions.cancelClass == 'string') 1.531 + cancelClasses = this.dragOptions.cancelClass.split(' '); 1.532 + 1.533 + var self = this; 1.534 + var $container = iQ(this.container); 1.535 + var startMouse; 1.536 + var startPos; 1.537 + var startSent; 1.538 + var startEvent; 1.539 + var droppables; 1.540 + var dropTarget; 1.541 + 1.542 + // determine the best drop target based on the current mouse coordinates 1.543 + let determineBestDropTarget = function (e, box) { 1.544 + // drop events 1.545 + var best = { 1.546 + dropTarget: null, 1.547 + score: 0 1.548 + }; 1.549 + 1.550 + droppables.forEach(function(droppable) { 1.551 + var intersection = box.intersection(droppable.bounds); 1.552 + if (intersection && intersection.area() > best.score) { 1.553 + var possibleDropTarget = droppable.item; 1.554 + var accept = true; 1.555 + if (possibleDropTarget != dropTarget) { 1.556 + var dropOptions = possibleDropTarget.dropOptions; 1.557 + if (dropOptions && typeof dropOptions.accept == "function") 1.558 + accept = dropOptions.accept.apply(possibleDropTarget, [self]); 1.559 + } 1.560 + 1.561 + if (accept) { 1.562 + best.dropTarget = possibleDropTarget; 1.563 + best.score = intersection.area(); 1.564 + } 1.565 + } 1.566 + }); 1.567 + 1.568 + return best.dropTarget; 1.569 + } 1.570 + 1.571 + // ___ mousemove 1.572 + var handleMouseMove = function(e) { 1.573 + // global drag tracking 1.574 + drag.lastMoveTime = Date.now(); 1.575 + 1.576 + // positioning 1.577 + var mouse = new Point(e.pageX, e.pageY); 1.578 + if (!startSent) { 1.579 + if(Math.abs(mouse.x - startMouse.x) > self.dragOptions.minDragDistance || 1.580 + Math.abs(mouse.y - startMouse.y) > self.dragOptions.minDragDistance) { 1.581 + if (typeof self.dragOptions.start == "function") 1.582 + self.dragOptions.start.apply(self, 1.583 + [startEvent, {position: {left: startPos.x, top: startPos.y}}]); 1.584 + startSent = true; 1.585 + } 1.586 + } 1.587 + if (startSent) { 1.588 + // drag events 1.589 + var box = self.getBounds(); 1.590 + box.left = startPos.x + (mouse.x - startMouse.x); 1.591 + box.top = startPos.y + (mouse.y - startMouse.y); 1.592 + self.setBounds(box, true); 1.593 + 1.594 + if (typeof self.dragOptions.drag == "function") 1.595 + self.dragOptions.drag.apply(self, [e]); 1.596 + 1.597 + let bestDropTarget = determineBestDropTarget(e, box); 1.598 + 1.599 + if (bestDropTarget != dropTarget) { 1.600 + var dropOptions; 1.601 + if (dropTarget) { 1.602 + dropOptions = dropTarget.dropOptions; 1.603 + if (dropOptions && typeof dropOptions.out == "function") 1.604 + dropOptions.out.apply(dropTarget, [e]); 1.605 + } 1.606 + 1.607 + dropTarget = bestDropTarget; 1.608 + 1.609 + if (dropTarget) { 1.610 + dropOptions = dropTarget.dropOptions; 1.611 + if (dropOptions && typeof dropOptions.over == "function") 1.612 + dropOptions.over.apply(dropTarget, [e]); 1.613 + } 1.614 + } 1.615 + if (dropTarget) { 1.616 + dropOptions = dropTarget.dropOptions; 1.617 + if (dropOptions && typeof dropOptions.move == "function") 1.618 + dropOptions.move.apply(dropTarget, [e]); 1.619 + } 1.620 + } 1.621 + 1.622 + e.preventDefault(); 1.623 + }; 1.624 + 1.625 + // ___ mouseup 1.626 + var handleMouseUp = function(e) { 1.627 + iQ(gWindow) 1.628 + .unbind('mousemove', handleMouseMove) 1.629 + .unbind('mouseup', handleMouseUp); 1.630 + 1.631 + if (startSent && dropTarget) { 1.632 + var dropOptions = dropTarget.dropOptions; 1.633 + if (dropOptions && typeof dropOptions.drop == "function") 1.634 + dropOptions.drop.apply(dropTarget, [e]); 1.635 + } 1.636 + 1.637 + if (startSent && typeof self.dragOptions.stop == "function") 1.638 + self.dragOptions.stop.apply(self, [e]); 1.639 + 1.640 + e.preventDefault(); 1.641 + }; 1.642 + 1.643 + // ___ mousedown 1.644 + $container.mousedown(function(e) { 1.645 + if (!Utils.isLeftClick(e)) 1.646 + return; 1.647 + 1.648 + var cancel = false; 1.649 + var $target = iQ(e.target); 1.650 + cancelClasses.forEach(function(className) { 1.651 + if ($target.hasClass(className)) 1.652 + cancel = true; 1.653 + }); 1.654 + 1.655 + if (cancel) { 1.656 + e.preventDefault(); 1.657 + return; 1.658 + } 1.659 + 1.660 + startMouse = new Point(e.pageX, e.pageY); 1.661 + let bounds = self.getBounds(); 1.662 + startPos = bounds.position(); 1.663 + startEvent = e; 1.664 + startSent = false; 1.665 + 1.666 + droppables = []; 1.667 + iQ('.iq-droppable').each(function(elem) { 1.668 + if (elem != self.container) { 1.669 + var item = Items.item(elem); 1.670 + droppables.push({ 1.671 + item: item, 1.672 + bounds: item.getBounds() 1.673 + }); 1.674 + } 1.675 + }); 1.676 + 1.677 + dropTarget = determineBestDropTarget(e, bounds); 1.678 + 1.679 + iQ(gWindow) 1.680 + .mousemove(handleMouseMove) 1.681 + .mouseup(handleMouseUp); 1.682 + 1.683 + e.preventDefault(); 1.684 + }); 1.685 + } catch(e) { 1.686 + Utils.log(e); 1.687 + } 1.688 + }, 1.689 + 1.690 + // ---------- 1.691 + // Function: droppable 1.692 + // Enables or disables dropping on this item. 1.693 + droppable: function Item_droppable(value) { 1.694 + try { 1.695 + var $container = iQ(this.container); 1.696 + if (value) { 1.697 + Utils.assert(this.dropOptions, 'dropOptions'); 1.698 + $container.addClass('iq-droppable'); 1.699 + } else 1.700 + $container.removeClass('iq-droppable'); 1.701 + } catch(e) { 1.702 + Utils.log(e); 1.703 + } 1.704 + }, 1.705 + 1.706 + // ---------- 1.707 + // Function: resizable 1.708 + // Enables or disables resizing of this item. 1.709 + resizable: function Item_resizable(value) { 1.710 + try { 1.711 + var $container = iQ(this.container); 1.712 + iQ('.iq-resizable-handle', $container).remove(); 1.713 + 1.714 + if (!value) { 1.715 + $container.removeClass('iq-resizable'); 1.716 + } else { 1.717 + Utils.assert(this.resizeOptions, 'resizeOptions'); 1.718 + 1.719 + $container.addClass('iq-resizable'); 1.720 + 1.721 + var self = this; 1.722 + var startMouse; 1.723 + var startSize; 1.724 + var startAspect; 1.725 + 1.726 + // ___ mousemove 1.727 + var handleMouseMove = function(e) { 1.728 + // global resize tracking 1.729 + resize.lastMoveTime = Date.now(); 1.730 + 1.731 + var mouse = new Point(e.pageX, e.pageY); 1.732 + var box = self.getBounds(); 1.733 + if (UI.rtl) { 1.734 + var minWidth = (self.resizeOptions.minWidth || 0); 1.735 + var oldWidth = box.width; 1.736 + if (minWidth != oldWidth || mouse.x < startMouse.x) { 1.737 + box.width = Math.max(minWidth, startSize.x - (mouse.x - startMouse.x)); 1.738 + box.left -= box.width - oldWidth; 1.739 + } 1.740 + } else { 1.741 + box.width = Math.max(self.resizeOptions.minWidth || 0, startSize.x + (mouse.x - startMouse.x)); 1.742 + } 1.743 + box.height = Math.max(self.resizeOptions.minHeight || 0, startSize.y + (mouse.y - startMouse.y)); 1.744 + 1.745 + if (self.resizeOptions.aspectRatio) { 1.746 + if (startAspect < 1) 1.747 + box.height = box.width * startAspect; 1.748 + else 1.749 + box.width = box.height / startAspect; 1.750 + } 1.751 + 1.752 + self.setBounds(box, true); 1.753 + 1.754 + if (typeof self.resizeOptions.resize == "function") 1.755 + self.resizeOptions.resize.apply(self, [e]); 1.756 + 1.757 + e.preventDefault(); 1.758 + e.stopPropagation(); 1.759 + }; 1.760 + 1.761 + // ___ mouseup 1.762 + var handleMouseUp = function(e) { 1.763 + iQ(gWindow) 1.764 + .unbind('mousemove', handleMouseMove) 1.765 + .unbind('mouseup', handleMouseUp); 1.766 + 1.767 + if (typeof self.resizeOptions.stop == "function") 1.768 + self.resizeOptions.stop.apply(self, [e]); 1.769 + 1.770 + e.preventDefault(); 1.771 + e.stopPropagation(); 1.772 + }; 1.773 + 1.774 + // ___ handle + mousedown 1.775 + iQ('<div>') 1.776 + .addClass('iq-resizable-handle iq-resizable-se') 1.777 + .appendTo($container) 1.778 + .mousedown(function(e) { 1.779 + if (!Utils.isLeftClick(e)) 1.780 + return; 1.781 + 1.782 + startMouse = new Point(e.pageX, e.pageY); 1.783 + startSize = self.getBounds().size(); 1.784 + startAspect = startSize.y / startSize.x; 1.785 + 1.786 + if (typeof self.resizeOptions.start == "function") 1.787 + self.resizeOptions.start.apply(self, [e]); 1.788 + 1.789 + iQ(gWindow) 1.790 + .mousemove(handleMouseMove) 1.791 + .mouseup(handleMouseUp); 1.792 + 1.793 + e.preventDefault(); 1.794 + e.stopPropagation(); 1.795 + }); 1.796 + } 1.797 + } catch(e) { 1.798 + Utils.log(e); 1.799 + } 1.800 + } 1.801 +}; 1.802 + 1.803 +// ########## 1.804 +// Class: Items 1.805 +// Keeps track of all Items. 1.806 +let Items = { 1.807 + // ---------- 1.808 + // Function: toString 1.809 + // Prints [Items] for debug use 1.810 + toString: function Items_toString() { 1.811 + return "[Items]"; 1.812 + }, 1.813 + 1.814 + // ---------- 1.815 + // Variable: defaultGutter 1.816 + // How far apart Items should be from each other and from bounds 1.817 + defaultGutter: 15, 1.818 + 1.819 + // ---------- 1.820 + // Function: item 1.821 + // Given a DOM element representing an Item, returns the Item. 1.822 + item: function Items_item(el) { 1.823 + return iQ(el).data('item'); 1.824 + }, 1.825 + 1.826 + // ---------- 1.827 + // Function: getTopLevelItems 1.828 + // Returns an array of all Items not grouped into groupItems. 1.829 + getTopLevelItems: function Items_getTopLevelItems() { 1.830 + var items = []; 1.831 + 1.832 + iQ('.tab, .groupItem').each(function(elem) { 1.833 + var $this = iQ(elem); 1.834 + var item = $this.data('item'); 1.835 + if (item && !item.parent && !$this.hasClass('phantom')) 1.836 + items.push(item); 1.837 + }); 1.838 + 1.839 + return items; 1.840 + }, 1.841 + 1.842 + // ---------- 1.843 + // Function: getPageBounds 1.844 + // Returns a <Rect> defining the area of the page <Item>s should stay within. 1.845 + getPageBounds: function Items_getPageBounds() { 1.846 + var width = Math.max(100, window.innerWidth); 1.847 + var height = Math.max(100, window.innerHeight); 1.848 + return new Rect(0, 0, width, height); 1.849 + }, 1.850 + 1.851 + // ---------- 1.852 + // Function: getSafeWindowBounds 1.853 + // Returns the bounds within which it is safe to place all non-stationary <Item>s. 1.854 + getSafeWindowBounds: function Items_getSafeWindowBounds() { 1.855 + // the safe bounds that would keep it "in the window" 1.856 + var gutter = Items.defaultGutter; 1.857 + // Here, I've set the top gutter separately, as the top of the window has its own 1.858 + // extra chrome which makes a large top gutter unnecessary. 1.859 + // TODO: set top gutter separately, elsewhere. 1.860 + var topGutter = 5; 1.861 + return new Rect(gutter, topGutter, 1.862 + window.innerWidth - 2 * gutter, window.innerHeight - gutter - topGutter); 1.863 + 1.864 + }, 1.865 + 1.866 + // ---------- 1.867 + // Function: arrange 1.868 + // Arranges the given items in a grid within the given bounds, 1.869 + // maximizing item size but maintaining standard tab aspect ratio for each 1.870 + // 1.871 + // Parameters: 1.872 + // items - an array of <Item>s. Can be null, in which case we won't 1.873 + // actually move anything. 1.874 + // bounds - a <Rect> defining the space to arrange within 1.875 + // options - an object with various properites (see below) 1.876 + // 1.877 + // Possible "options" properties: 1.878 + // animate - whether to animate; default: true. 1.879 + // z - the z index to set all the items; default: don't change z. 1.880 + // return - if set to 'widthAndColumns', it'll return an object with the 1.881 + // width of children and the columns. 1.882 + // count - overrides the item count for layout purposes; 1.883 + // default: the actual item count 1.884 + // columns - (int) a preset number of columns to use 1.885 + // dropPos - a <Point> which should have a one-tab space left open, used 1.886 + // when a tab is dragged over. 1.887 + // 1.888 + // Returns: 1.889 + // By default, an object with three properties: `rects`, the list of <Rect>s, 1.890 + // `dropIndex`, the index which a dragged tab should have if dropped 1.891 + // (null if no `dropPos` was specified), and the number of columns (`columns`). 1.892 + // If the `return` option is set to 'widthAndColumns', an object with the 1.893 + // width value of the child items (`childWidth`) and the number of columns 1.894 + // (`columns`) is returned. 1.895 + arrange: function Items_arrange(items, bounds, options) { 1.896 + if (!options) 1.897 + options = {}; 1.898 + var animate = "animate" in options ? options.animate : true; 1.899 + var immediately = !animate; 1.900 + 1.901 + var rects = []; 1.902 + 1.903 + var count = options.count || (items ? items.length : 0); 1.904 + if (options.addTab) 1.905 + count++; 1.906 + if (!count) { 1.907 + let dropIndex = (Utils.isPoint(options.dropPos)) ? 0 : null; 1.908 + return {rects: rects, dropIndex: dropIndex}; 1.909 + } 1.910 + 1.911 + var columns = options.columns || 1; 1.912 + // We'll assume for the time being that all the items have the same styling 1.913 + // and that the margin is the same width around. 1.914 + var itemMargin = items && items.length ? 1.915 + parseInt(iQ(items[0].container).css('margin-left')) : 0; 1.916 + var padding = itemMargin * 2; 1.917 + var rows; 1.918 + var tabWidth; 1.919 + var tabHeight; 1.920 + var totalHeight; 1.921 + 1.922 + function figure() { 1.923 + rows = Math.ceil(count / columns); 1.924 + let validSize = TabItems.calcValidSize( 1.925 + new Point((bounds.width - (padding * columns)) / columns, -1), 1.926 + options); 1.927 + tabWidth = validSize.x; 1.928 + tabHeight = validSize.y; 1.929 + 1.930 + totalHeight = (tabHeight * rows) + (padding * rows); 1.931 + } 1.932 + 1.933 + figure(); 1.934 + 1.935 + while (rows > 1 && totalHeight > bounds.height) { 1.936 + columns++; 1.937 + figure(); 1.938 + } 1.939 + 1.940 + if (rows == 1) { 1.941 + let validSize = TabItems.calcValidSize(new Point(tabWidth, 1.942 + bounds.height - 2 * itemMargin), options); 1.943 + tabWidth = validSize.x; 1.944 + tabHeight = validSize.y; 1.945 + } 1.946 + 1.947 + if (options.return == 'widthAndColumns') 1.948 + return {childWidth: tabWidth, columns: columns}; 1.949 + 1.950 + let initialOffset = 0; 1.951 + if (UI.rtl) { 1.952 + initialOffset = bounds.width - tabWidth - padding; 1.953 + } 1.954 + var box = new Rect(bounds.left + initialOffset, bounds.top, tabWidth, tabHeight); 1.955 + 1.956 + var column = 0; 1.957 + 1.958 + var dropIndex = false; 1.959 + var dropRect = false; 1.960 + if (Utils.isPoint(options.dropPos)) 1.961 + dropRect = new Rect(options.dropPos.x, options.dropPos.y, 1, 1); 1.962 + for (let a = 0; a < count; a++) { 1.963 + // If we had a dropPos, see if this is where we should place it 1.964 + if (dropRect) { 1.965 + let activeBox = new Rect(box); 1.966 + activeBox.inset(-itemMargin - 1, -itemMargin - 1); 1.967 + // if the designated position (dropRect) is within the active box, 1.968 + // this is where, if we drop the tab being dragged, it should land! 1.969 + if (activeBox.contains(dropRect)) 1.970 + dropIndex = a; 1.971 + } 1.972 + 1.973 + // record the box. 1.974 + rects.push(new Rect(box)); 1.975 + 1.976 + box.left += (UI.rtl ? -1 : 1) * (box.width + padding); 1.977 + column++; 1.978 + if (column == columns) { 1.979 + box.left = bounds.left + initialOffset; 1.980 + box.top += box.height + padding; 1.981 + column = 0; 1.982 + } 1.983 + } 1.984 + 1.985 + return {rects: rects, dropIndex: dropIndex, columns: columns}; 1.986 + }, 1.987 + 1.988 + // ---------- 1.989 + // Function: unsquish 1.990 + // Checks to see which items can now be unsquished. 1.991 + // 1.992 + // Parameters: 1.993 + // pairs - an array of objects, each with two properties: item and bounds. The bounds are 1.994 + // modified as appropriate, but the items are not changed. If pairs is null, the 1.995 + // operation is performed directly on all of the top level items. 1.996 + // ignore - an <Item> to not include in calculations (because it's about to be closed, for instance) 1.997 + unsquish: function Items_unsquish(pairs, ignore) { 1.998 + var pairsProvided = (pairs ? true : false); 1.999 + if (!pairsProvided) { 1.1000 + var items = Items.getTopLevelItems(); 1.1001 + pairs = []; 1.1002 + items.forEach(function(item) { 1.1003 + pairs.push({ 1.1004 + item: item, 1.1005 + bounds: item.getBounds() 1.1006 + }); 1.1007 + }); 1.1008 + } 1.1009 + 1.1010 + var pageBounds = Items.getSafeWindowBounds(); 1.1011 + pairs.forEach(function(pair) { 1.1012 + var item = pair.item; 1.1013 + if (item == ignore) 1.1014 + return; 1.1015 + 1.1016 + var bounds = pair.bounds; 1.1017 + var newBounds = new Rect(bounds); 1.1018 + 1.1019 + var newSize; 1.1020 + if (Utils.isPoint(item.userSize)) 1.1021 + newSize = new Point(item.userSize); 1.1022 + else if (item.isAGroupItem) 1.1023 + newSize = GroupItems.calcValidSize( 1.1024 + new Point(GroupItems.minGroupWidth, -1)); 1.1025 + else 1.1026 + newSize = TabItems.calcValidSize( 1.1027 + new Point(TabItems.tabWidth, -1)); 1.1028 + 1.1029 + if (item.isAGroupItem) { 1.1030 + newBounds.width = Math.max(newBounds.width, newSize.x); 1.1031 + newBounds.height = Math.max(newBounds.height, newSize.y); 1.1032 + } else { 1.1033 + if (bounds.width < newSize.x) { 1.1034 + newBounds.width = newSize.x; 1.1035 + newBounds.height = newSize.y; 1.1036 + } 1.1037 + } 1.1038 + 1.1039 + newBounds.left -= (newBounds.width - bounds.width) / 2; 1.1040 + newBounds.top -= (newBounds.height - bounds.height) / 2; 1.1041 + 1.1042 + var offset = new Point(); 1.1043 + if (newBounds.left < pageBounds.left) 1.1044 + offset.x = pageBounds.left - newBounds.left; 1.1045 + else if (newBounds.right > pageBounds.right) 1.1046 + offset.x = pageBounds.right - newBounds.right; 1.1047 + 1.1048 + if (newBounds.top < pageBounds.top) 1.1049 + offset.y = pageBounds.top - newBounds.top; 1.1050 + else if (newBounds.bottom > pageBounds.bottom) 1.1051 + offset.y = pageBounds.bottom - newBounds.bottom; 1.1052 + 1.1053 + newBounds.offset(offset); 1.1054 + 1.1055 + if (!bounds.equals(newBounds)) { 1.1056 + var blocked = false; 1.1057 + pairs.forEach(function(pair2) { 1.1058 + if (pair2 == pair || pair2.item == ignore) 1.1059 + return; 1.1060 + 1.1061 + var bounds2 = pair2.bounds; 1.1062 + if (bounds2.intersects(newBounds)) 1.1063 + blocked = true; 1.1064 + return; 1.1065 + }); 1.1066 + 1.1067 + if (!blocked) { 1.1068 + pair.bounds.copy(newBounds); 1.1069 + } 1.1070 + } 1.1071 + return; 1.1072 + }); 1.1073 + 1.1074 + if (!pairsProvided) { 1.1075 + pairs.forEach(function(pair) { 1.1076 + pair.item.setBounds(pair.bounds); 1.1077 + }); 1.1078 + } 1.1079 + } 1.1080 +};