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.

     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       });
  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;
  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);
  1068       return;
  1069     });
  1071     if (!pairsProvided) {
  1072       pairs.forEach(function(pair) {
  1073         pair.item.setBounds(pair.bounds);
  1074       });
  1077 };

mercurial