Added a new style of interaction called a zoom group. It is rather hot. I believe this is how we will be doing all future designs.
authoraza@More-Better-Internet.local
Wed, 17 Mar 2010 01:07:00 -0700
changeset 49744 16ada45683aceba15d688297ecd76f8ca1a5172c
parent 49743 181e7b1beccb7b7d49c72b0d4fcbd063d0cfa3f7
child 49745 49fbfed1253116a22f17889baf9c3f66e577bab0
child 49746 3b031e9024ccc5ce2580a0ba9998ee70403e04e7
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
Added a new style of interaction called a zoom group. It is rather hot. I believe this is how we will be doing all future designs.
browser/base/content/tabcandy/app/groups.js
content/candies/original/index.html
content/candies/zoomgroups/index.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/tabcandy/app/groups.js
@@ -0,0 +1,260 @@
+(function(){
+
+var numCmp = function(a,b){ return a-b; }
+
+function min(list){ return list.slice().sort(numCmp)[0]; }
+function max(list){ return list.slice().sort(numCmp).reverse()[0]; }
+
+function Group(){}
+Group.prototype = {
+  _children: [],
+  _container: null,
+  _padding: 30,
+  
+  _getBoundingBox: function(){
+    var els = this._children;
+    var el;
+    var boundingBox = {
+      top:    min( [$(el).position().top  for([,el] in Iterator(els))] ),
+      left:   min( [$(el).position().left for([,el] in Iterator(els))] ),
+      bottom: max( [$(el).position().top  for([,el] in Iterator(els))] )  + $(els[0]).height(),
+      right:  max( [$(el).position().left for([,el] in Iterator(els))] ) + $(els[0]).width(),
+    };
+    boundingBox.height = boundingBox.bottom - boundingBox.top;
+    boundingBox.width  = boundingBox.right - boundingBox.left;
+    return boundingBox;
+  },
+  
+  _getContainerBox: function(){
+    var pos = $(this._container).position();
+    var w = $(this._container).width();
+    var h = $(this._container).height();
+    return {
+      top: pos.top,
+      left: pos.left,
+      bottom: pos.top + h,
+      right: pos.left + w,
+      height: h,
+      width: w
+    }
+  },
+  
+  create: function(listOfEls){
+    this._children = $(listOfEls).toArray();
+
+    var boundingBox = this._getBoundingBox();
+    var padding = 30;
+    var container = $("<div class='group'/>")
+      .css({
+        position: "absolute",
+        top: boundingBox.top-padding,
+        left: boundingBox.left-padding,
+        width: boundingBox.width+padding*2,
+        height: boundingBox.height+padding*2,
+        zIndex: -100,
+        opacity: 0,
+      })
+      .appendTo("body")
+      .animate({opacity:1.0});
+
+    this._container = container;
+    
+    this._addHandlers(container);
+    this._updateGroup();
+
+    var els = this._children;
+    this._children = [];
+    for(var i in els){
+      this.add( els[i] );
+    }
+  },
+  
+  add: function(el){
+    this._children.push( el );
+    $(el).droppable("disable");
+    
+    this._updateGroup();
+    this.arrange();
+  },
+  
+  remove: function(el){
+    $(el).data("toRemove", true);
+    this._children = this._children.filter(function(child){
+      if( $(child).data("toRemove") == true ){
+        $(child).data("group", null);
+        $(child).animate({width:160, height:120}, 250);
+        $(child).droppable("enable");        
+        return false;
+      }
+      else return true;
+    })
+    $(el).data("toRemove", false);
+    
+    if( this._children.length == 0 ){
+      this._container.fadeOut(function() $(this).remove());
+    } else {
+      this.arrange();
+    }
+    
+  },
+  
+  _updateGroup: function(){
+    var self = this;
+    this._children.forEach(function(el){
+      $(el).data("group", self);
+    });    
+  },
+  
+  arrange: function(){
+    if( this._children.length < 2 ) return;
+    var bb = this._getContainerBox();
+    var tab;
+
+    // TODO: This is an hacky heuristic. Should probably fix this.
+    var pad = 10;
+    var w = parseInt(Math.sqrt(((bb.height+pad) * (bb.width+pad))/(this._children.length+4)));
+    var h = w * (2/3);
+
+    var x=pad;
+    var y=pad;
+    for([,tab] in Iterator(this._children)){
+      // This is for actual tabs. Need a better solution.
+      // One that doesn't require special casing to find the linked info.
+      // If I just animate the tab, the rest should happen automatically!
+      if( $("canvas", tab)[0] != null ){
+        $("canvas", tab).data('link').animate({height:h}, 250);
+      }
+            
+      $(tab).animate({
+        height:h, width: w,
+        top:y+bb.top, left:x+bb.left,
+      }, 250);
+      
+      x += w+pad;
+      if( x+w+pad > $(this._container).width()){x = pad;y += h+pad;}
+    }
+    
+  },
+  
+  _addHandlers: function(container){
+    var self = this;
+    
+    $(container).draggable({
+      start: function(){
+        $(container).data("origPosition", $(container).position());
+        self._children.forEach(function(el){
+          $(el).data("origPosition", $(el).position());
+        });
+      },
+      drag: function(e, ui){
+        var origPos = $(container).data("origPosition");
+        dX = ui.offset.left - origPos.left;
+        dY = ui.offset.top - origPos.top;
+        $(self._children).each(function(){
+          $(this).css({
+            left: $(this).data("origPosition").left + dX,
+            top:  $(this).data("origPosition").top + dY
+          })
+        })
+      }
+    });
+    
+    
+    $(container).droppable({
+      over: function(){
+        $dragged.addClass("willGroup");
+      },
+      out: function(){
+        $dragged.data("group").remove($dragged);
+        $dragged.removeClass("willGroup");
+      },
+      drop: function(e){
+        $dragged.removeClass("willGroup");
+        self.add( $dragged )
+      },
+      accept: ".tab",
+      
+    });
+    
+    }
+}
+
+var zIndex = 100;
+var $dragged = null;
+var timeout = null;
+
+window.Groups = {
+  dragOptions: {
+    start:function(){
+      $dragged = $(this);
+      $dragged.data('isDragging', true);
+    },
+    stop: function(){
+      $dragged.data('isDragging', false);
+      $(this).css({zIndex: zIndex});
+      $dragged = null;          
+      zIndex += 1;
+    },
+    zIndex: 999,
+  },
+  
+  dropOptions: {
+    accept: ".tab",
+    tolerance: "pointer",
+    greedy: true,
+    drop: function(e){
+      $target = $(e.target);
+  
+      // Only drop onto the top z-index
+      if( $target.css("zIndex") < $dragged.data("topDropZIndex") ) return;
+      $dragged.data("topDropZIndex", $target.css("zIndex") );
+      $dragged.data("topDrop", $target);
+      
+      // This strange timeout thing solves the problem of when
+      // something is dropped onto multiple potential drop targets.
+      // We wait a little bit to see get all drops, and then we have saved
+      // the top-most one and drop onto that.
+      clearTimeout( timeout );
+      var dragged = $dragged;
+      var target = $target;
+      timeout = setTimeout( function(){
+        dragged.removeClass("willGroup")   
+  
+        dragged.animate({
+          top: target.position().top+15,
+          left: target.position().left+15,      
+        }, 100);
+        
+        setTimeout( function(){
+          var group = $(target).data("group");
+          if( group == null ){
+            var group = new Group();
+            group.create( [target, dragged] );            
+          } else {
+            group.add( dragged )
+          }
+          
+        }, 100);
+        
+        
+      }, 10 );
+      
+      
+    },
+    over: function(e){
+      $dragged.addClass("willGroup");
+      $dragged.data("topDropZIndex", 0);    
+    },
+    out: function(){      
+      $dragged.removeClass("willGroup");
+    }
+  }  
+};
+
+$(".tab").data('isDragging', false)
+  .draggable(window.Groups.dragOptions)
+  .droppable(window.Groups.dropOptions);
+
+
+
+})();
\ No newline at end of file
--- a/content/candies/original/index.html
+++ b/content/candies/original/index.html
@@ -102,19 +102,18 @@
     border: 1px solid #DDD;
     -moz-box-shadow: 2px 2px 4px rgba(0,0,0,.5);
     line-height: 100px;
     text-align: center;
     cursor: pointer;
   }
   
   .group{
-    background-color: #DDD;
-    border: 1px solid #CCC;
     cursor: move;
+    -moz-box-shadow: 0px 0px 5px rgba(0,0,0,.2);
   }    
   
   </style>
   
 </head>
 
 <body>
   <div class="search">
@@ -126,21 +125,18 @@
   
   <script type="text/javascript;version=1.8" src="../../js/core/jquery.js"></script>  
   <script type="text/javascript;version=1.8" src="../../js/optional/jquery-ui.js"></script>  
   <script type="text/javascript;version=1.8" src="../../js/core/utils.js"></script>  
   <script type="text/javascript;version=1.8" src="../../js/core/tabs.js"></script>
   <script type="text/javascript;version=1.8" src="../../js/core/mirror.js"></script>
   <script type="text/javascript;version=1.8" src="js/ui.js"></script>  
 
-  <!--
-  <script type="text/javascript;version=1.8" src="../../js/shared/jquery.lasso.js"></script>
-  <script type="text/javascript;version=1.8" src="../../js/shared/jquery.select.js"></script>  -->
   <script type="text/javascript;version=1.8"
-  src="../groups/js/groups.js"></script>
+  src="../zoomgroups/js/groups.js"></script>
   
   <!-- BEGIN Switch Control -->
   <script type="text/javascript;version=1.8" src="../../js/optional/switch.js"></script>  
   <script type="text/javascript;version=1.8">
     Switch.insert('body', '');
   </script>
   <!-- END Switch Control -->
 
new file mode 100644
--- /dev/null
+++ b/content/candies/zoomgroups/index.html
@@ -0,0 +1,77 @@
+<html>
+  <head>
+    <title>Stacks!</title>
+    <script src="../../js/core/jquery.js"></script>
+    <script src="../../js/core/utils.js"></script>  
+    <style>
+      body{ margin: 0px, padding: 0px; }
+      .tab{ width: 160px; height: 120px; background-color: #222; -moz-box-shadow: 1px 1px 8px rgba(0,0,0,.8); border:1px solid white; z-index: 0;}
+      .tab{
+        color: rgba(255,255,255,0.3);
+        font-size:250px;
+        overflow:hidden; line-height:.5em;
+        font-family: "Helvetica";
+        cursor: move;}
+      
+      .active{
+        -moz-box-shadow: 5px 5px 4px rgba(0,0,0,.5);
+      }
+      
+      .willGroup{
+        -moz-box-shadow: 0px 0px 10px rgba(255,0,0,1);
+      }
+      
+      .group{
+        cursor: move;
+        -moz-box-shadow: 0px 0px 5px rgba(0,0,0,.2);        
+      }    
+
+
+      
+    </style>
+  </head>
+  
+  <body>
+    <div id="content">
+      <div class="tab">A<div id="aza">hello</div></div>
+      <div class="tab">B</div>
+      <div class="tab">C</div>
+      <div class="tab">D</div>
+      <div class="tab">E</div>
+      <div class="tab">F</div>
+      <div class="tab">G</div>
+      <div class="tab">H</div>
+      <div class="tab">I</div>
+      <div class="tab">j</div>
+      <div class="tab">k</div>
+      <div class="tab">m</div>
+      <div class="tab">n</div>      
+      <div class="tab">z</div>
+    </div>
+  <script>
+    function randInt(num){
+      return parseInt(Math.random()*num);
+    }
+  
+    $(".tab").each(function(){
+      $(this).css({
+        top: randInt(window.innerHeight-150),
+        left:randInt(window.innerWidth-150),
+        position: "absolute"
+      })
+    })
+  </script>
+
+    <script type="text/javascript;version=1.8" src="../../js/optional/jquery-ui.js"></script>  
+
+    <script src="js/groups.js"></script>
+  
+    <!-- BEGIN Switch Control -->
+    <script type="text/javascript;version=1.8" src="../../js/optional/switch.js"></script>  
+    <script type="text/javascript;version=1.8">
+      Switch.insert('body', '');
+    </script>
+    <!-- END Switch Control -->
+
+  </body>
+</html>
\ No newline at end of file