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