browser/components/tabview/items.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 // **********
michael@0 6 // Title: items.js
michael@0 7
michael@0 8 // ##########
michael@0 9 // Class: Item
michael@0 10 // Superclass for all visible objects (<TabItem>s and <GroupItem>s).
michael@0 11 //
michael@0 12 // If you subclass, in addition to the things Item provides, you need to also provide these methods:
michael@0 13 // setBounds - function(rect, immediately, options)
michael@0 14 // setZ - function(value)
michael@0 15 // close - function()
michael@0 16 // save - function()
michael@0 17 //
michael@0 18 // Subclasses of Item must also provide the <Subscribable> interface.
michael@0 19 //
michael@0 20 // Make sure to call _init() from your subclass's constructor.
michael@0 21 function Item() {
michael@0 22 // Variable: isAnItem
michael@0 23 // Always true for Items
michael@0 24 this.isAnItem = true;
michael@0 25
michael@0 26 // Variable: bounds
michael@0 27 // The position and size of this Item, represented as a <Rect>.
michael@0 28 // This should never be modified without using setBounds()
michael@0 29 this.bounds = null;
michael@0 30
michael@0 31 // Variable: zIndex
michael@0 32 // The z-index for this item.
michael@0 33 this.zIndex = 0;
michael@0 34
michael@0 35 // Variable: container
michael@0 36 // The outermost DOM element that describes this item on screen.
michael@0 37 this.container = null;
michael@0 38
michael@0 39 // Variable: parent
michael@0 40 // The groupItem that this item is a child of
michael@0 41 this.parent = null;
michael@0 42
michael@0 43 // Variable: userSize
michael@0 44 // A <Point> that describes the last size specifically chosen by the user.
michael@0 45 // Used by unsquish.
michael@0 46 this.userSize = null;
michael@0 47
michael@0 48 // Variable: dragOptions
michael@0 49 // Used by <draggable>
michael@0 50 //
michael@0 51 // Possible properties:
michael@0 52 // cancelClass - A space-delimited list of classes that should cancel a drag
michael@0 53 // start - A function to be called when a drag starts
michael@0 54 // drag - A function to be called each time the mouse moves during drag
michael@0 55 // stop - A function to be called when the drag is done
michael@0 56 this.dragOptions = null;
michael@0 57
michael@0 58 // Variable: dropOptions
michael@0 59 // Used by <draggable> if the item is set to droppable.
michael@0 60 //
michael@0 61 // Possible properties:
michael@0 62 // accept - A function to determine if a particular item should be accepted for dropping
michael@0 63 // over - A function to be called when an item is over this item
michael@0 64 // out - A function to be called when an item leaves this item
michael@0 65 // drop - A function to be called when an item is dropped in this item
michael@0 66 this.dropOptions = null;
michael@0 67
michael@0 68 // Variable: resizeOptions
michael@0 69 // Used by <resizable>
michael@0 70 //
michael@0 71 // Possible properties:
michael@0 72 // minWidth - Minimum width allowable during resize
michael@0 73 // minHeight - Minimum height allowable during resize
michael@0 74 // aspectRatio - true if we should respect aspect ratio; default false
michael@0 75 // start - A function to be called when resizing starts
michael@0 76 // resize - A function to be called each time the mouse moves during resize
michael@0 77 // stop - A function to be called when the resize is done
michael@0 78 this.resizeOptions = null;
michael@0 79
michael@0 80 // Variable: isDragging
michael@0 81 // Boolean for whether the item is currently being dragged or not.
michael@0 82 this.isDragging = false;
michael@0 83 };
michael@0 84
michael@0 85 Item.prototype = {
michael@0 86 // ----------
michael@0 87 // Function: _init
michael@0 88 // Initializes the object. To be called from the subclass's intialization function.
michael@0 89 //
michael@0 90 // Parameters:
michael@0 91 // container - the outermost DOM element that describes this item onscreen.
michael@0 92 _init: function Item__init(container) {
michael@0 93 Utils.assert(typeof this.addSubscriber == 'function' &&
michael@0 94 typeof this.removeSubscriber == 'function' &&
michael@0 95 typeof this._sendToSubscribers == 'function',
michael@0 96 'Subclass must implement the Subscribable interface');
michael@0 97 Utils.assert(Utils.isDOMElement(container), 'container must be a DOM element');
michael@0 98 Utils.assert(typeof this.setBounds == 'function', 'Subclass must provide setBounds');
michael@0 99 Utils.assert(typeof this.setZ == 'function', 'Subclass must provide setZ');
michael@0 100 Utils.assert(typeof this.close == 'function', 'Subclass must provide close');
michael@0 101 Utils.assert(typeof this.save == 'function', 'Subclass must provide save');
michael@0 102 Utils.assert(Utils.isRect(this.bounds), 'Subclass must provide bounds');
michael@0 103
michael@0 104 this.container = container;
michael@0 105 this.$container = iQ(container);
michael@0 106
michael@0 107 iQ(this.container).data('item', this);
michael@0 108
michael@0 109 // ___ drag
michael@0 110 this.dragOptions = {
michael@0 111 cancelClass: 'close stackExpander',
michael@0 112 start: function(e, ui) {
michael@0 113 UI.setActive(this);
michael@0 114 if (this.isAGroupItem)
michael@0 115 this._unfreezeItemSize();
michael@0 116 // if we start dragging a tab within a group, start with dropSpace on.
michael@0 117 else if (this.parent != null)
michael@0 118 this.parent._dropSpaceActive = true;
michael@0 119 drag.info = new Drag(this, e);
michael@0 120 },
michael@0 121 drag: function(e) {
michael@0 122 drag.info.drag(e);
michael@0 123 },
michael@0 124 stop: function() {
michael@0 125 drag.info.stop();
michael@0 126
michael@0 127 if (!this.isAGroupItem && !this.parent) {
michael@0 128 new GroupItem([drag.info.$el], {focusTitle: true});
michael@0 129 gTabView.firstUseExperienced = true;
michael@0 130 }
michael@0 131
michael@0 132 drag.info = null;
michael@0 133 },
michael@0 134 // The minimum the mouse must move after mouseDown in order to move an
michael@0 135 // item
michael@0 136 minDragDistance: 3
michael@0 137 };
michael@0 138
michael@0 139 // ___ drop
michael@0 140 this.dropOptions = {
michael@0 141 over: function() {},
michael@0 142 out: function() {
michael@0 143 let groupItem = drag.info.item.parent;
michael@0 144 if (groupItem)
michael@0 145 groupItem.remove(drag.info.$el, {dontClose: true});
michael@0 146 iQ(this.container).removeClass("acceptsDrop");
michael@0 147 },
michael@0 148 drop: function(event) {
michael@0 149 iQ(this.container).removeClass("acceptsDrop");
michael@0 150 },
michael@0 151 // Function: dropAcceptFunction
michael@0 152 // Given a DOM element, returns true if it should accept tabs being dropped on it.
michael@0 153 // Private to this file.
michael@0 154 accept: function dropAcceptFunction(item) {
michael@0 155 return (item && item.isATabItem && (!item.parent || !item.parent.expanded));
michael@0 156 }
michael@0 157 };
michael@0 158
michael@0 159 // ___ resize
michael@0 160 var self = this;
michael@0 161 this.resizeOptions = {
michael@0 162 aspectRatio: self.keepProportional,
michael@0 163 minWidth: 90,
michael@0 164 minHeight: 90,
michael@0 165 start: function(e,ui) {
michael@0 166 UI.setActive(this);
michael@0 167 resize.info = new Drag(this, e);
michael@0 168 },
michael@0 169 resize: function(e,ui) {
michael@0 170 resize.info.snap(UI.rtl ? 'topright' : 'topleft', false, self.keepProportional);
michael@0 171 },
michael@0 172 stop: function() {
michael@0 173 self.setUserSize();
michael@0 174 self.pushAway();
michael@0 175 resize.info.stop();
michael@0 176 resize.info = null;
michael@0 177 }
michael@0 178 };
michael@0 179 },
michael@0 180
michael@0 181 // ----------
michael@0 182 // Function: getBounds
michael@0 183 // Returns a copy of the Item's bounds as a <Rect>.
michael@0 184 getBounds: function Item_getBounds() {
michael@0 185 Utils.assert(Utils.isRect(this.bounds), 'this.bounds should be a rect');
michael@0 186 return new Rect(this.bounds);
michael@0 187 },
michael@0 188
michael@0 189 // ----------
michael@0 190 // Function: overlapsWithOtherItems
michael@0 191 // Returns true if this Item overlaps with any other Item on the screen.
michael@0 192 overlapsWithOtherItems: function Item_overlapsWithOtherItems() {
michael@0 193 var self = this;
michael@0 194 var items = Items.getTopLevelItems();
michael@0 195 var bounds = this.getBounds();
michael@0 196 return items.some(function(item) {
michael@0 197 if (item == self) // can't overlap with yourself.
michael@0 198 return false;
michael@0 199 var myBounds = item.getBounds();
michael@0 200 return myBounds.intersects(bounds);
michael@0 201 } );
michael@0 202 },
michael@0 203
michael@0 204 // ----------
michael@0 205 // Function: setPosition
michael@0 206 // Moves the Item to the specified location.
michael@0 207 //
michael@0 208 // Parameters:
michael@0 209 // left - the new left coordinate relative to the window
michael@0 210 // top - the new top coordinate relative to the window
michael@0 211 // immediately - if false or omitted, animates to the new position;
michael@0 212 // otherwise goes there immediately
michael@0 213 setPosition: function Item_setPosition(left, top, immediately) {
michael@0 214 Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
michael@0 215 this.setBounds(new Rect(left, top, this.bounds.width, this.bounds.height), immediately);
michael@0 216 },
michael@0 217
michael@0 218 // ----------
michael@0 219 // Function: setSize
michael@0 220 // Resizes the Item to the specified size.
michael@0 221 //
michael@0 222 // Parameters:
michael@0 223 // width - the new width in pixels
michael@0 224 // height - the new height in pixels
michael@0 225 // immediately - if false or omitted, animates to the new size;
michael@0 226 // otherwise resizes immediately
michael@0 227 setSize: function Item_setSize(width, height, immediately) {
michael@0 228 Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
michael@0 229 this.setBounds(new Rect(this.bounds.left, this.bounds.top, width, height), immediately);
michael@0 230 },
michael@0 231
michael@0 232 // ----------
michael@0 233 // Function: setUserSize
michael@0 234 // Remembers the current size as one the user has chosen.
michael@0 235 setUserSize: function Item_setUserSize() {
michael@0 236 Utils.assert(Utils.isRect(this.bounds), 'this.bounds');
michael@0 237 this.userSize = new Point(this.bounds.width, this.bounds.height);
michael@0 238 this.save();
michael@0 239 },
michael@0 240
michael@0 241 // ----------
michael@0 242 // Function: getZ
michael@0 243 // Returns the zIndex of the Item.
michael@0 244 getZ: function Item_getZ() {
michael@0 245 return this.zIndex;
michael@0 246 },
michael@0 247
michael@0 248 // ----------
michael@0 249 // Function: setRotation
michael@0 250 // Rotates the object to the given number of degrees.
michael@0 251 setRotation: function Item_setRotation(degrees) {
michael@0 252 var value = degrees ? "rotate(%deg)".replace(/%/, degrees) : null;
michael@0 253 iQ(this.container).css({"transform": value});
michael@0 254 },
michael@0 255
michael@0 256 // ----------
michael@0 257 // Function: setParent
michael@0 258 // Sets the receiver's parent to the given <Item>.
michael@0 259 setParent: function Item_setParent(parent) {
michael@0 260 this.parent = parent;
michael@0 261 this.removeTrenches();
michael@0 262 this.save();
michael@0 263 },
michael@0 264
michael@0 265 // ----------
michael@0 266 // Function: pushAway
michael@0 267 // Pushes all other items away so none overlap this Item.
michael@0 268 //
michael@0 269 // Parameters:
michael@0 270 // immediately - boolean for doing the pushAway without animation
michael@0 271 pushAway: function Item_pushAway(immediately) {
michael@0 272 var items = Items.getTopLevelItems();
michael@0 273
michael@0 274 // we need at least two top-level items to push something away
michael@0 275 if (items.length < 2)
michael@0 276 return;
michael@0 277
michael@0 278 var buffer = Math.floor(Items.defaultGutter / 2);
michael@0 279
michael@0 280 // setup each Item's pushAwayData attribute:
michael@0 281 items.forEach(function pushAway_setupPushAwayData(item) {
michael@0 282 var data = {};
michael@0 283 data.bounds = item.getBounds();
michael@0 284 data.startBounds = new Rect(data.bounds);
michael@0 285 // Infinity = (as yet) unaffected
michael@0 286 data.generation = Infinity;
michael@0 287 item.pushAwayData = data;
michael@0 288 });
michael@0 289
michael@0 290 // The first item is a 0-generation pushed item. It all starts here.
michael@0 291 var itemsToPush = [this];
michael@0 292 this.pushAwayData.generation = 0;
michael@0 293
michael@0 294 var pushOne = function Item_pushAway_pushOne(baseItem) {
michael@0 295 // the baseItem is an n-generation pushed item. (n could be 0)
michael@0 296 var baseData = baseItem.pushAwayData;
michael@0 297 var bb = new Rect(baseData.bounds);
michael@0 298
michael@0 299 // make the bounds larger, adding a +buffer margin to each side.
michael@0 300 bb.inset(-buffer, -buffer);
michael@0 301 // bbc = center of the base's bounds
michael@0 302 var bbc = bb.center();
michael@0 303
michael@0 304 items.forEach(function Item_pushAway_pushOne_pushEach(item) {
michael@0 305 if (item == baseItem)
michael@0 306 return;
michael@0 307
michael@0 308 var data = item.pushAwayData;
michael@0 309 // if the item under consideration has already been pushed, or has a lower
michael@0 310 // "generation" (and thus an implictly greater placement priority) then don't move it.
michael@0 311 if (data.generation <= baseData.generation)
michael@0 312 return;
michael@0 313
michael@0 314 // box = this item's current bounds, with a +buffer margin.
michael@0 315 var bounds = data.bounds;
michael@0 316 var box = new Rect(bounds);
michael@0 317 box.inset(-buffer, -buffer);
michael@0 318
michael@0 319 // if the item under consideration overlaps with the base item...
michael@0 320 if (box.intersects(bb)) {
michael@0 321
michael@0 322 // Let's push it a little.
michael@0 323
michael@0 324 // First, decide in which direction and how far to push. This is the offset.
michael@0 325 var offset = new Point();
michael@0 326 // center = the current item's center.
michael@0 327 var center = box.center();
michael@0 328
michael@0 329 // Consider the relationship between the current item (box) + the base item.
michael@0 330 // If it's more vertically stacked than "side by side"...
michael@0 331 if (Math.abs(center.x - bbc.x) < Math.abs(center.y - bbc.y)) {
michael@0 332 // push vertically.
michael@0 333 if (center.y > bbc.y)
michael@0 334 offset.y = bb.bottom - box.top;
michael@0 335 else
michael@0 336 offset.y = bb.top - box.bottom;
michael@0 337 } else { // if they're more "side by side" than stacked vertically...
michael@0 338 // push horizontally.
michael@0 339 if (center.x > bbc.x)
michael@0 340 offset.x = bb.right - box.left;
michael@0 341 else
michael@0 342 offset.x = bb.left - box.right;
michael@0 343 }
michael@0 344
michael@0 345 // Actually push the Item.
michael@0 346 bounds.offset(offset);
michael@0 347
michael@0 348 // This item now becomes an (n+1)-generation pushed item.
michael@0 349 data.generation = baseData.generation + 1;
michael@0 350 // keep track of who pushed this item.
michael@0 351 data.pusher = baseItem;
michael@0 352 // add this item to the queue, so that it, in turn, can push some other things.
michael@0 353 itemsToPush.push(item);
michael@0 354 }
michael@0 355 });
michael@0 356 };
michael@0 357
michael@0 358 // push each of the itemsToPush, one at a time.
michael@0 359 // itemsToPush starts with just [this], but pushOne can add more items to the stack.
michael@0 360 // Maximally, this could run through all Items on the screen.
michael@0 361 while (itemsToPush.length)
michael@0 362 pushOne(itemsToPush.shift());
michael@0 363
michael@0 364 // ___ Squish!
michael@0 365 var pageBounds = Items.getSafeWindowBounds();
michael@0 366 items.forEach(function Item_pushAway_squish(item) {
michael@0 367 var data = item.pushAwayData;
michael@0 368 if (data.generation == 0)
michael@0 369 return;
michael@0 370
michael@0 371 let apply = function Item_pushAway_squish_apply(item, posStep, posStep2, sizeStep) {
michael@0 372 var data = item.pushAwayData;
michael@0 373 if (data.generation == 0)
michael@0 374 return;
michael@0 375
michael@0 376 var bounds = data.bounds;
michael@0 377 bounds.width -= sizeStep.x;
michael@0 378 bounds.height -= sizeStep.y;
michael@0 379 bounds.left += posStep.x;
michael@0 380 bounds.top += posStep.y;
michael@0 381
michael@0 382 let validSize;
michael@0 383 if (item.isAGroupItem) {
michael@0 384 validSize = GroupItems.calcValidSize(
michael@0 385 new Point(bounds.width, bounds.height));
michael@0 386 bounds.width = validSize.x;
michael@0 387 bounds.height = validSize.y;
michael@0 388 } else {
michael@0 389 if (sizeStep.y > sizeStep.x) {
michael@0 390 validSize = TabItems.calcValidSize(new Point(-1, bounds.height));
michael@0 391 bounds.left += (bounds.width - validSize.x) / 2;
michael@0 392 bounds.width = validSize.x;
michael@0 393 } else {
michael@0 394 validSize = TabItems.calcValidSize(new Point(bounds.width, -1));
michael@0 395 bounds.top += (bounds.height - validSize.y) / 2;
michael@0 396 bounds.height = validSize.y;
michael@0 397 }
michael@0 398 }
michael@0 399
michael@0 400 var pusher = data.pusher;
michael@0 401 if (pusher) {
michael@0 402 var newPosStep = new Point(posStep.x + posStep2.x, posStep.y + posStep2.y);
michael@0 403 apply(pusher, newPosStep, posStep2, sizeStep);
michael@0 404 }
michael@0 405 }
michael@0 406
michael@0 407 var bounds = data.bounds;
michael@0 408 var posStep = new Point();
michael@0 409 var posStep2 = new Point();
michael@0 410 var sizeStep = new Point();
michael@0 411
michael@0 412 if (bounds.left < pageBounds.left) {
michael@0 413 posStep.x = pageBounds.left - bounds.left;
michael@0 414 sizeStep.x = posStep.x / data.generation;
michael@0 415 posStep2.x = -sizeStep.x;
michael@0 416 } else if (bounds.right > pageBounds.right) { // this may be less of a problem post-601534
michael@0 417 posStep.x = pageBounds.right - bounds.right;
michael@0 418 sizeStep.x = -posStep.x / data.generation;
michael@0 419 posStep.x += sizeStep.x;
michael@0 420 posStep2.x = sizeStep.x;
michael@0 421 }
michael@0 422
michael@0 423 if (bounds.top < pageBounds.top) {
michael@0 424 posStep.y = pageBounds.top - bounds.top;
michael@0 425 sizeStep.y = posStep.y / data.generation;
michael@0 426 posStep2.y = -sizeStep.y;
michael@0 427 } else if (bounds.bottom > pageBounds.bottom) { // this may be less of a problem post-601534
michael@0 428 posStep.y = pageBounds.bottom - bounds.bottom;
michael@0 429 sizeStep.y = -posStep.y / data.generation;
michael@0 430 posStep.y += sizeStep.y;
michael@0 431 posStep2.y = sizeStep.y;
michael@0 432 }
michael@0 433
michael@0 434 if (posStep.x || posStep.y || sizeStep.x || sizeStep.y)
michael@0 435 apply(item, posStep, posStep2, sizeStep);
michael@0 436 });
michael@0 437
michael@0 438 // ___ Unsquish
michael@0 439 var pairs = [];
michael@0 440 items.forEach(function Item_pushAway_setupUnsquish(item) {
michael@0 441 var data = item.pushAwayData;
michael@0 442 pairs.push({
michael@0 443 item: item,
michael@0 444 bounds: data.bounds
michael@0 445 });
michael@0 446 });
michael@0 447
michael@0 448 Items.unsquish(pairs);
michael@0 449
michael@0 450 // ___ Apply changes
michael@0 451 items.forEach(function Item_pushAway_setBounds(item) {
michael@0 452 var data = item.pushAwayData;
michael@0 453 var bounds = data.bounds;
michael@0 454 if (!bounds.equals(data.startBounds)) {
michael@0 455 item.setBounds(bounds, immediately);
michael@0 456 }
michael@0 457 });
michael@0 458 },
michael@0 459
michael@0 460 // ----------
michael@0 461 // Function: setTrenches
michael@0 462 // Sets up/moves the trenches for snapping to this item.
michael@0 463 setTrenches: function Item_setTrenches(rect) {
michael@0 464 if (this.parent !== null)
michael@0 465 return;
michael@0 466
michael@0 467 if (!this.borderTrenches)
michael@0 468 this.borderTrenches = Trenches.registerWithItem(this,"border");
michael@0 469
michael@0 470 var bT = this.borderTrenches;
michael@0 471 Trenches.getById(bT.left).setWithRect(rect);
michael@0 472 Trenches.getById(bT.right).setWithRect(rect);
michael@0 473 Trenches.getById(bT.top).setWithRect(rect);
michael@0 474 Trenches.getById(bT.bottom).setWithRect(rect);
michael@0 475
michael@0 476 if (!this.guideTrenches)
michael@0 477 this.guideTrenches = Trenches.registerWithItem(this,"guide");
michael@0 478
michael@0 479 var gT = this.guideTrenches;
michael@0 480 Trenches.getById(gT.left).setWithRect(rect);
michael@0 481 Trenches.getById(gT.right).setWithRect(rect);
michael@0 482 Trenches.getById(gT.top).setWithRect(rect);
michael@0 483 Trenches.getById(gT.bottom).setWithRect(rect);
michael@0 484
michael@0 485 },
michael@0 486
michael@0 487 // ----------
michael@0 488 // Function: removeTrenches
michael@0 489 // Removes the trenches for snapping to this item.
michael@0 490 removeTrenches: function Item_removeTrenches() {
michael@0 491 for (var edge in this.borderTrenches) {
michael@0 492 Trenches.unregister(this.borderTrenches[edge]); // unregister can take an array
michael@0 493 }
michael@0 494 this.borderTrenches = null;
michael@0 495 for (var edge in this.guideTrenches) {
michael@0 496 Trenches.unregister(this.guideTrenches[edge]); // unregister can take an array
michael@0 497 }
michael@0 498 this.guideTrenches = null;
michael@0 499 },
michael@0 500
michael@0 501 // ----------
michael@0 502 // Function: snap
michael@0 503 // The snap function used during groupItem creation via drag-out
michael@0 504 //
michael@0 505 // Parameters:
michael@0 506 // immediately - bool for having the drag do the final positioning without animation
michael@0 507 snap: function Item_snap(immediately) {
michael@0 508 // make the snapping work with a wider range!
michael@0 509 var defaultRadius = Trenches.defaultRadius;
michael@0 510 Trenches.defaultRadius = 2 * defaultRadius; // bump up from 10 to 20!
michael@0 511
michael@0 512 var FauxDragInfo = new Drag(this, {});
michael@0 513 FauxDragInfo.snap('none', false);
michael@0 514 FauxDragInfo.stop(immediately);
michael@0 515
michael@0 516 Trenches.defaultRadius = defaultRadius;
michael@0 517 },
michael@0 518
michael@0 519 // ----------
michael@0 520 // Function: draggable
michael@0 521 // Enables dragging on this item. Note: not to be called multiple times on the same item!
michael@0 522 draggable: function Item_draggable() {
michael@0 523 try {
michael@0 524 Utils.assert(this.dragOptions, 'dragOptions');
michael@0 525
michael@0 526 var cancelClasses = [];
michael@0 527 if (typeof this.dragOptions.cancelClass == 'string')
michael@0 528 cancelClasses = this.dragOptions.cancelClass.split(' ');
michael@0 529
michael@0 530 var self = this;
michael@0 531 var $container = iQ(this.container);
michael@0 532 var startMouse;
michael@0 533 var startPos;
michael@0 534 var startSent;
michael@0 535 var startEvent;
michael@0 536 var droppables;
michael@0 537 var dropTarget;
michael@0 538
michael@0 539 // determine the best drop target based on the current mouse coordinates
michael@0 540 let determineBestDropTarget = function (e, box) {
michael@0 541 // drop events
michael@0 542 var best = {
michael@0 543 dropTarget: null,
michael@0 544 score: 0
michael@0 545 };
michael@0 546
michael@0 547 droppables.forEach(function(droppable) {
michael@0 548 var intersection = box.intersection(droppable.bounds);
michael@0 549 if (intersection && intersection.area() > best.score) {
michael@0 550 var possibleDropTarget = droppable.item;
michael@0 551 var accept = true;
michael@0 552 if (possibleDropTarget != dropTarget) {
michael@0 553 var dropOptions = possibleDropTarget.dropOptions;
michael@0 554 if (dropOptions && typeof dropOptions.accept == "function")
michael@0 555 accept = dropOptions.accept.apply(possibleDropTarget, [self]);
michael@0 556 }
michael@0 557
michael@0 558 if (accept) {
michael@0 559 best.dropTarget = possibleDropTarget;
michael@0 560 best.score = intersection.area();
michael@0 561 }
michael@0 562 }
michael@0 563 });
michael@0 564
michael@0 565 return best.dropTarget;
michael@0 566 }
michael@0 567
michael@0 568 // ___ mousemove
michael@0 569 var handleMouseMove = function(e) {
michael@0 570 // global drag tracking
michael@0 571 drag.lastMoveTime = Date.now();
michael@0 572
michael@0 573 // positioning
michael@0 574 var mouse = new Point(e.pageX, e.pageY);
michael@0 575 if (!startSent) {
michael@0 576 if(Math.abs(mouse.x - startMouse.x) > self.dragOptions.minDragDistance ||
michael@0 577 Math.abs(mouse.y - startMouse.y) > self.dragOptions.minDragDistance) {
michael@0 578 if (typeof self.dragOptions.start == "function")
michael@0 579 self.dragOptions.start.apply(self,
michael@0 580 [startEvent, {position: {left: startPos.x, top: startPos.y}}]);
michael@0 581 startSent = true;
michael@0 582 }
michael@0 583 }
michael@0 584 if (startSent) {
michael@0 585 // drag events
michael@0 586 var box = self.getBounds();
michael@0 587 box.left = startPos.x + (mouse.x - startMouse.x);
michael@0 588 box.top = startPos.y + (mouse.y - startMouse.y);
michael@0 589 self.setBounds(box, true);
michael@0 590
michael@0 591 if (typeof self.dragOptions.drag == "function")
michael@0 592 self.dragOptions.drag.apply(self, [e]);
michael@0 593
michael@0 594 let bestDropTarget = determineBestDropTarget(e, box);
michael@0 595
michael@0 596 if (bestDropTarget != dropTarget) {
michael@0 597 var dropOptions;
michael@0 598 if (dropTarget) {
michael@0 599 dropOptions = dropTarget.dropOptions;
michael@0 600 if (dropOptions && typeof dropOptions.out == "function")
michael@0 601 dropOptions.out.apply(dropTarget, [e]);
michael@0 602 }
michael@0 603
michael@0 604 dropTarget = bestDropTarget;
michael@0 605
michael@0 606 if (dropTarget) {
michael@0 607 dropOptions = dropTarget.dropOptions;
michael@0 608 if (dropOptions && typeof dropOptions.over == "function")
michael@0 609 dropOptions.over.apply(dropTarget, [e]);
michael@0 610 }
michael@0 611 }
michael@0 612 if (dropTarget) {
michael@0 613 dropOptions = dropTarget.dropOptions;
michael@0 614 if (dropOptions && typeof dropOptions.move == "function")
michael@0 615 dropOptions.move.apply(dropTarget, [e]);
michael@0 616 }
michael@0 617 }
michael@0 618
michael@0 619 e.preventDefault();
michael@0 620 };
michael@0 621
michael@0 622 // ___ mouseup
michael@0 623 var handleMouseUp = function(e) {
michael@0 624 iQ(gWindow)
michael@0 625 .unbind('mousemove', handleMouseMove)
michael@0 626 .unbind('mouseup', handleMouseUp);
michael@0 627
michael@0 628 if (startSent && dropTarget) {
michael@0 629 var dropOptions = dropTarget.dropOptions;
michael@0 630 if (dropOptions && typeof dropOptions.drop == "function")
michael@0 631 dropOptions.drop.apply(dropTarget, [e]);
michael@0 632 }
michael@0 633
michael@0 634 if (startSent && typeof self.dragOptions.stop == "function")
michael@0 635 self.dragOptions.stop.apply(self, [e]);
michael@0 636
michael@0 637 e.preventDefault();
michael@0 638 };
michael@0 639
michael@0 640 // ___ mousedown
michael@0 641 $container.mousedown(function(e) {
michael@0 642 if (!Utils.isLeftClick(e))
michael@0 643 return;
michael@0 644
michael@0 645 var cancel = false;
michael@0 646 var $target = iQ(e.target);
michael@0 647 cancelClasses.forEach(function(className) {
michael@0 648 if ($target.hasClass(className))
michael@0 649 cancel = true;
michael@0 650 });
michael@0 651
michael@0 652 if (cancel) {
michael@0 653 e.preventDefault();
michael@0 654 return;
michael@0 655 }
michael@0 656
michael@0 657 startMouse = new Point(e.pageX, e.pageY);
michael@0 658 let bounds = self.getBounds();
michael@0 659 startPos = bounds.position();
michael@0 660 startEvent = e;
michael@0 661 startSent = false;
michael@0 662
michael@0 663 droppables = [];
michael@0 664 iQ('.iq-droppable').each(function(elem) {
michael@0 665 if (elem != self.container) {
michael@0 666 var item = Items.item(elem);
michael@0 667 droppables.push({
michael@0 668 item: item,
michael@0 669 bounds: item.getBounds()
michael@0 670 });
michael@0 671 }
michael@0 672 });
michael@0 673
michael@0 674 dropTarget = determineBestDropTarget(e, bounds);
michael@0 675
michael@0 676 iQ(gWindow)
michael@0 677 .mousemove(handleMouseMove)
michael@0 678 .mouseup(handleMouseUp);
michael@0 679
michael@0 680 e.preventDefault();
michael@0 681 });
michael@0 682 } catch(e) {
michael@0 683 Utils.log(e);
michael@0 684 }
michael@0 685 },
michael@0 686
michael@0 687 // ----------
michael@0 688 // Function: droppable
michael@0 689 // Enables or disables dropping on this item.
michael@0 690 droppable: function Item_droppable(value) {
michael@0 691 try {
michael@0 692 var $container = iQ(this.container);
michael@0 693 if (value) {
michael@0 694 Utils.assert(this.dropOptions, 'dropOptions');
michael@0 695 $container.addClass('iq-droppable');
michael@0 696 } else
michael@0 697 $container.removeClass('iq-droppable');
michael@0 698 } catch(e) {
michael@0 699 Utils.log(e);
michael@0 700 }
michael@0 701 },
michael@0 702
michael@0 703 // ----------
michael@0 704 // Function: resizable
michael@0 705 // Enables or disables resizing of this item.
michael@0 706 resizable: function Item_resizable(value) {
michael@0 707 try {
michael@0 708 var $container = iQ(this.container);
michael@0 709 iQ('.iq-resizable-handle', $container).remove();
michael@0 710
michael@0 711 if (!value) {
michael@0 712 $container.removeClass('iq-resizable');
michael@0 713 } else {
michael@0 714 Utils.assert(this.resizeOptions, 'resizeOptions');
michael@0 715
michael@0 716 $container.addClass('iq-resizable');
michael@0 717
michael@0 718 var self = this;
michael@0 719 var startMouse;
michael@0 720 var startSize;
michael@0 721 var startAspect;
michael@0 722
michael@0 723 // ___ mousemove
michael@0 724 var handleMouseMove = function(e) {
michael@0 725 // global resize tracking
michael@0 726 resize.lastMoveTime = Date.now();
michael@0 727
michael@0 728 var mouse = new Point(e.pageX, e.pageY);
michael@0 729 var box = self.getBounds();
michael@0 730 if (UI.rtl) {
michael@0 731 var minWidth = (self.resizeOptions.minWidth || 0);
michael@0 732 var oldWidth = box.width;
michael@0 733 if (minWidth != oldWidth || mouse.x < startMouse.x) {
michael@0 734 box.width = Math.max(minWidth, startSize.x - (mouse.x - startMouse.x));
michael@0 735 box.left -= box.width - oldWidth;
michael@0 736 }
michael@0 737 } else {
michael@0 738 box.width = Math.max(self.resizeOptions.minWidth || 0, startSize.x + (mouse.x - startMouse.x));
michael@0 739 }
michael@0 740 box.height = Math.max(self.resizeOptions.minHeight || 0, startSize.y + (mouse.y - startMouse.y));
michael@0 741
michael@0 742 if (self.resizeOptions.aspectRatio) {
michael@0 743 if (startAspect < 1)
michael@0 744 box.height = box.width * startAspect;
michael@0 745 else
michael@0 746 box.width = box.height / startAspect;
michael@0 747 }
michael@0 748
michael@0 749 self.setBounds(box, true);
michael@0 750
michael@0 751 if (typeof self.resizeOptions.resize == "function")
michael@0 752 self.resizeOptions.resize.apply(self, [e]);
michael@0 753
michael@0 754 e.preventDefault();
michael@0 755 e.stopPropagation();
michael@0 756 };
michael@0 757
michael@0 758 // ___ mouseup
michael@0 759 var handleMouseUp = function(e) {
michael@0 760 iQ(gWindow)
michael@0 761 .unbind('mousemove', handleMouseMove)
michael@0 762 .unbind('mouseup', handleMouseUp);
michael@0 763
michael@0 764 if (typeof self.resizeOptions.stop == "function")
michael@0 765 self.resizeOptions.stop.apply(self, [e]);
michael@0 766
michael@0 767 e.preventDefault();
michael@0 768 e.stopPropagation();
michael@0 769 };
michael@0 770
michael@0 771 // ___ handle + mousedown
michael@0 772 iQ('<div>')
michael@0 773 .addClass('iq-resizable-handle iq-resizable-se')
michael@0 774 .appendTo($container)
michael@0 775 .mousedown(function(e) {
michael@0 776 if (!Utils.isLeftClick(e))
michael@0 777 return;
michael@0 778
michael@0 779 startMouse = new Point(e.pageX, e.pageY);
michael@0 780 startSize = self.getBounds().size();
michael@0 781 startAspect = startSize.y / startSize.x;
michael@0 782
michael@0 783 if (typeof self.resizeOptions.start == "function")
michael@0 784 self.resizeOptions.start.apply(self, [e]);
michael@0 785
michael@0 786 iQ(gWindow)
michael@0 787 .mousemove(handleMouseMove)
michael@0 788 .mouseup(handleMouseUp);
michael@0 789
michael@0 790 e.preventDefault();
michael@0 791 e.stopPropagation();
michael@0 792 });
michael@0 793 }
michael@0 794 } catch(e) {
michael@0 795 Utils.log(e);
michael@0 796 }
michael@0 797 }
michael@0 798 };
michael@0 799
michael@0 800 // ##########
michael@0 801 // Class: Items
michael@0 802 // Keeps track of all Items.
michael@0 803 let Items = {
michael@0 804 // ----------
michael@0 805 // Function: toString
michael@0 806 // Prints [Items] for debug use
michael@0 807 toString: function Items_toString() {
michael@0 808 return "[Items]";
michael@0 809 },
michael@0 810
michael@0 811 // ----------
michael@0 812 // Variable: defaultGutter
michael@0 813 // How far apart Items should be from each other and from bounds
michael@0 814 defaultGutter: 15,
michael@0 815
michael@0 816 // ----------
michael@0 817 // Function: item
michael@0 818 // Given a DOM element representing an Item, returns the Item.
michael@0 819 item: function Items_item(el) {
michael@0 820 return iQ(el).data('item');
michael@0 821 },
michael@0 822
michael@0 823 // ----------
michael@0 824 // Function: getTopLevelItems
michael@0 825 // Returns an array of all Items not grouped into groupItems.
michael@0 826 getTopLevelItems: function Items_getTopLevelItems() {
michael@0 827 var items = [];
michael@0 828
michael@0 829 iQ('.tab, .groupItem').each(function(elem) {
michael@0 830 var $this = iQ(elem);
michael@0 831 var item = $this.data('item');
michael@0 832 if (item && !item.parent && !$this.hasClass('phantom'))
michael@0 833 items.push(item);
michael@0 834 });
michael@0 835
michael@0 836 return items;
michael@0 837 },
michael@0 838
michael@0 839 // ----------
michael@0 840 // Function: getPageBounds
michael@0 841 // Returns a <Rect> defining the area of the page <Item>s should stay within.
michael@0 842 getPageBounds: function Items_getPageBounds() {
michael@0 843 var width = Math.max(100, window.innerWidth);
michael@0 844 var height = Math.max(100, window.innerHeight);
michael@0 845 return new Rect(0, 0, width, height);
michael@0 846 },
michael@0 847
michael@0 848 // ----------
michael@0 849 // Function: getSafeWindowBounds
michael@0 850 // Returns the bounds within which it is safe to place all non-stationary <Item>s.
michael@0 851 getSafeWindowBounds: function Items_getSafeWindowBounds() {
michael@0 852 // the safe bounds that would keep it "in the window"
michael@0 853 var gutter = Items.defaultGutter;
michael@0 854 // Here, I've set the top gutter separately, as the top of the window has its own
michael@0 855 // extra chrome which makes a large top gutter unnecessary.
michael@0 856 // TODO: set top gutter separately, elsewhere.
michael@0 857 var topGutter = 5;
michael@0 858 return new Rect(gutter, topGutter,
michael@0 859 window.innerWidth - 2 * gutter, window.innerHeight - gutter - topGutter);
michael@0 860
michael@0 861 },
michael@0 862
michael@0 863 // ----------
michael@0 864 // Function: arrange
michael@0 865 // Arranges the given items in a grid within the given bounds,
michael@0 866 // maximizing item size but maintaining standard tab aspect ratio for each
michael@0 867 //
michael@0 868 // Parameters:
michael@0 869 // items - an array of <Item>s. Can be null, in which case we won't
michael@0 870 // actually move anything.
michael@0 871 // bounds - a <Rect> defining the space to arrange within
michael@0 872 // options - an object with various properites (see below)
michael@0 873 //
michael@0 874 // Possible "options" properties:
michael@0 875 // animate - whether to animate; default: true.
michael@0 876 // z - the z index to set all the items; default: don't change z.
michael@0 877 // return - if set to 'widthAndColumns', it'll return an object with the
michael@0 878 // width of children and the columns.
michael@0 879 // count - overrides the item count for layout purposes;
michael@0 880 // default: the actual item count
michael@0 881 // columns - (int) a preset number of columns to use
michael@0 882 // dropPos - a <Point> which should have a one-tab space left open, used
michael@0 883 // when a tab is dragged over.
michael@0 884 //
michael@0 885 // Returns:
michael@0 886 // By default, an object with three properties: `rects`, the list of <Rect>s,
michael@0 887 // `dropIndex`, the index which a dragged tab should have if dropped
michael@0 888 // (null if no `dropPos` was specified), and the number of columns (`columns`).
michael@0 889 // If the `return` option is set to 'widthAndColumns', an object with the
michael@0 890 // width value of the child items (`childWidth`) and the number of columns
michael@0 891 // (`columns`) is returned.
michael@0 892 arrange: function Items_arrange(items, bounds, options) {
michael@0 893 if (!options)
michael@0 894 options = {};
michael@0 895 var animate = "animate" in options ? options.animate : true;
michael@0 896 var immediately = !animate;
michael@0 897
michael@0 898 var rects = [];
michael@0 899
michael@0 900 var count = options.count || (items ? items.length : 0);
michael@0 901 if (options.addTab)
michael@0 902 count++;
michael@0 903 if (!count) {
michael@0 904 let dropIndex = (Utils.isPoint(options.dropPos)) ? 0 : null;
michael@0 905 return {rects: rects, dropIndex: dropIndex};
michael@0 906 }
michael@0 907
michael@0 908 var columns = options.columns || 1;
michael@0 909 // We'll assume for the time being that all the items have the same styling
michael@0 910 // and that the margin is the same width around.
michael@0 911 var itemMargin = items && items.length ?
michael@0 912 parseInt(iQ(items[0].container).css('margin-left')) : 0;
michael@0 913 var padding = itemMargin * 2;
michael@0 914 var rows;
michael@0 915 var tabWidth;
michael@0 916 var tabHeight;
michael@0 917 var totalHeight;
michael@0 918
michael@0 919 function figure() {
michael@0 920 rows = Math.ceil(count / columns);
michael@0 921 let validSize = TabItems.calcValidSize(
michael@0 922 new Point((bounds.width - (padding * columns)) / columns, -1),
michael@0 923 options);
michael@0 924 tabWidth = validSize.x;
michael@0 925 tabHeight = validSize.y;
michael@0 926
michael@0 927 totalHeight = (tabHeight * rows) + (padding * rows);
michael@0 928 }
michael@0 929
michael@0 930 figure();
michael@0 931
michael@0 932 while (rows > 1 && totalHeight > bounds.height) {
michael@0 933 columns++;
michael@0 934 figure();
michael@0 935 }
michael@0 936
michael@0 937 if (rows == 1) {
michael@0 938 let validSize = TabItems.calcValidSize(new Point(tabWidth,
michael@0 939 bounds.height - 2 * itemMargin), options);
michael@0 940 tabWidth = validSize.x;
michael@0 941 tabHeight = validSize.y;
michael@0 942 }
michael@0 943
michael@0 944 if (options.return == 'widthAndColumns')
michael@0 945 return {childWidth: tabWidth, columns: columns};
michael@0 946
michael@0 947 let initialOffset = 0;
michael@0 948 if (UI.rtl) {
michael@0 949 initialOffset = bounds.width - tabWidth - padding;
michael@0 950 }
michael@0 951 var box = new Rect(bounds.left + initialOffset, bounds.top, tabWidth, tabHeight);
michael@0 952
michael@0 953 var column = 0;
michael@0 954
michael@0 955 var dropIndex = false;
michael@0 956 var dropRect = false;
michael@0 957 if (Utils.isPoint(options.dropPos))
michael@0 958 dropRect = new Rect(options.dropPos.x, options.dropPos.y, 1, 1);
michael@0 959 for (let a = 0; a < count; a++) {
michael@0 960 // If we had a dropPos, see if this is where we should place it
michael@0 961 if (dropRect) {
michael@0 962 let activeBox = new Rect(box);
michael@0 963 activeBox.inset(-itemMargin - 1, -itemMargin - 1);
michael@0 964 // if the designated position (dropRect) is within the active box,
michael@0 965 // this is where, if we drop the tab being dragged, it should land!
michael@0 966 if (activeBox.contains(dropRect))
michael@0 967 dropIndex = a;
michael@0 968 }
michael@0 969
michael@0 970 // record the box.
michael@0 971 rects.push(new Rect(box));
michael@0 972
michael@0 973 box.left += (UI.rtl ? -1 : 1) * (box.width + padding);
michael@0 974 column++;
michael@0 975 if (column == columns) {
michael@0 976 box.left = bounds.left + initialOffset;
michael@0 977 box.top += box.height + padding;
michael@0 978 column = 0;
michael@0 979 }
michael@0 980 }
michael@0 981
michael@0 982 return {rects: rects, dropIndex: dropIndex, columns: columns};
michael@0 983 },
michael@0 984
michael@0 985 // ----------
michael@0 986 // Function: unsquish
michael@0 987 // Checks to see which items can now be unsquished.
michael@0 988 //
michael@0 989 // Parameters:
michael@0 990 // pairs - an array of objects, each with two properties: item and bounds. The bounds are
michael@0 991 // modified as appropriate, but the items are not changed. If pairs is null, the
michael@0 992 // operation is performed directly on all of the top level items.
michael@0 993 // ignore - an <Item> to not include in calculations (because it's about to be closed, for instance)
michael@0 994 unsquish: function Items_unsquish(pairs, ignore) {
michael@0 995 var pairsProvided = (pairs ? true : false);
michael@0 996 if (!pairsProvided) {
michael@0 997 var items = Items.getTopLevelItems();
michael@0 998 pairs = [];
michael@0 999 items.forEach(function(item) {
michael@0 1000 pairs.push({
michael@0 1001 item: item,
michael@0 1002 bounds: item.getBounds()
michael@0 1003 });
michael@0 1004 });
michael@0 1005 }
michael@0 1006
michael@0 1007 var pageBounds = Items.getSafeWindowBounds();
michael@0 1008 pairs.forEach(function(pair) {
michael@0 1009 var item = pair.item;
michael@0 1010 if (item == ignore)
michael@0 1011 return;
michael@0 1012
michael@0 1013 var bounds = pair.bounds;
michael@0 1014 var newBounds = new Rect(bounds);
michael@0 1015
michael@0 1016 var newSize;
michael@0 1017 if (Utils.isPoint(item.userSize))
michael@0 1018 newSize = new Point(item.userSize);
michael@0 1019 else if (item.isAGroupItem)
michael@0 1020 newSize = GroupItems.calcValidSize(
michael@0 1021 new Point(GroupItems.minGroupWidth, -1));
michael@0 1022 else
michael@0 1023 newSize = TabItems.calcValidSize(
michael@0 1024 new Point(TabItems.tabWidth, -1));
michael@0 1025
michael@0 1026 if (item.isAGroupItem) {
michael@0 1027 newBounds.width = Math.max(newBounds.width, newSize.x);
michael@0 1028 newBounds.height = Math.max(newBounds.height, newSize.y);
michael@0 1029 } else {
michael@0 1030 if (bounds.width < newSize.x) {
michael@0 1031 newBounds.width = newSize.x;
michael@0 1032 newBounds.height = newSize.y;
michael@0 1033 }
michael@0 1034 }
michael@0 1035
michael@0 1036 newBounds.left -= (newBounds.width - bounds.width) / 2;
michael@0 1037 newBounds.top -= (newBounds.height - bounds.height) / 2;
michael@0 1038
michael@0 1039 var offset = new Point();
michael@0 1040 if (newBounds.left < pageBounds.left)
michael@0 1041 offset.x = pageBounds.left - newBounds.left;
michael@0 1042 else if (newBounds.right > pageBounds.right)
michael@0 1043 offset.x = pageBounds.right - newBounds.right;
michael@0 1044
michael@0 1045 if (newBounds.top < pageBounds.top)
michael@0 1046 offset.y = pageBounds.top - newBounds.top;
michael@0 1047 else if (newBounds.bottom > pageBounds.bottom)
michael@0 1048 offset.y = pageBounds.bottom - newBounds.bottom;
michael@0 1049
michael@0 1050 newBounds.offset(offset);
michael@0 1051
michael@0 1052 if (!bounds.equals(newBounds)) {
michael@0 1053 var blocked = false;
michael@0 1054 pairs.forEach(function(pair2) {
michael@0 1055 if (pair2 == pair || pair2.item == ignore)
michael@0 1056 return;
michael@0 1057
michael@0 1058 var bounds2 = pair2.bounds;
michael@0 1059 if (bounds2.intersects(newBounds))
michael@0 1060 blocked = true;
michael@0 1061 return;
michael@0 1062 });
michael@0 1063
michael@0 1064 if (!blocked) {
michael@0 1065 pair.bounds.copy(newBounds);
michael@0 1066 }
michael@0 1067 }
michael@0 1068 return;
michael@0 1069 });
michael@0 1070
michael@0 1071 if (!pairsProvided) {
michael@0 1072 pairs.forEach(function(pair) {
michael@0 1073 pair.item.setBounds(pair.bounds);
michael@0 1074 });
michael@0 1075 }
michael@0 1076 }
michael@0 1077 };

mercurial