Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview This implements a splitter element which can be used to resize
      7  * elements in split panes.
      8  *
      9  * The parent of the splitter should be an hbox (display: -webkit-box) with at
     10  * least one previous element sibling. The splitter controls the width of the
     11  * element before it.
     12  *
     13  * <div class=split-pane>
     14  *   <div class=left>...</div>
     15  *   <div class=splitter></div>
     16  *   ...
     17  * </div>
     18  *
     19  */
     20 
     21 cr.define('cr.ui', function() {
     22   // TODO(arv): Currently this only supports horizontal layout.
     23   // TODO(arv): This ignores min-width and max-width of the elements to the
     24   // right of the splitter.
     25 
     26   /**
     27    * Returns the computed style width of an element.
     28    * @param {!Element} el The element to get the width of.
     29    * @return {number} The width in pixels.
     30    */
     31   function getComputedWidth(el) {
     32     return parseFloat(el.ownerDocument.defaultView.getComputedStyle(el).width) /
     33         getZoomFactor(el.ownerDocument);
     34   }
     35 
     36   /**
     37    * This uses a WebKit bug to work around the same bug. getComputedStyle does
     38    * not take the page zoom into account so it returns the physical pixels
     39    * instead of the logical pixel size.
     40    * @param {!Document} doc The document to get the page zoom factor for.
     41    * @param {number} The zoom factor of the document.
     42    */
     43   function getZoomFactor(doc) {
     44     var dummyElement = doc.createElement('div');
     45     dummyElement.style.cssText =
     46     'position:absolute;width:100px;height:100px;top:-1000px;overflow:hidden';
     47     doc.body.appendChild(dummyElement);
     48     var cs = doc.defaultView.getComputedStyle(dummyElement);
     49     var rect = dummyElement.getBoundingClientRect();
     50     var zoomFactor = parseFloat(cs.width) / 100;
     51     doc.body.removeChild(dummyElement);
     52     return zoomFactor;
     53   }
     54 
     55   /**
     56    * Creates a new splitter element.
     57    * @param {Object=} opt_propertyBag Optional properties.
     58    * @constructor
     59    * @extends {HTMLDivElement}
     60    */
     61   var Splitter = cr.ui.define('div');
     62 
     63   Splitter.prototype = {
     64     __proto__: HTMLDivElement.prototype,
     65 
     66     /**
     67      * Initializes the element.
     68      */
     69     decorate: function() {
     70       this.addEventListener('mousedown', this.handleMouseDown_.bind(this),
     71                             true);
     72     },
     73 
     74     /**
     75      * Starts the dragging of the splitter. Adds listeners for mouse move and
     76      * mouse up events and calls splitter drag start handler.
     77      * @param {!Event} e The mouse event that started the drag.
     78      */
     79     startDrag: function(e) {
     80       if (!this.boundHandleMouseMove_) {
     81         this.boundHandleMouseMove_ = this.handleMouseMove_.bind(this);
     82         this.boundHandleMouseUp_ = this.handleMouseUp_.bind(this);
     83       }
     84 
     85       var doc = this.ownerDocument;
     86 
     87       // Use capturing events on the document to get events when the mouse
     88       // leaves the document.
     89       doc.addEventListener('mousemove',this.boundHandleMouseMove_, true);
     90       doc.addEventListener('mouseup', this.boundHandleMouseUp_, true);
     91 
     92       this.startX_ = e.clientX;
     93       this.handleSplitterDragStart();
     94     },
     95 
     96     /**
     97      * Ends the dragging of the splitter. Removes listeners set in startDrag
     98      * and calls splitter drag end handler.
     99      */
    100     endDrag: function() {
    101       var doc = this.ownerDocument;
    102       doc.removeEventListener('mousemove', this.boundHandleMouseMove_, true);
    103       doc.removeEventListener('mouseup', this.boundHandleMouseUp_, true);
    104       this.handleSplitterDragEnd();
    105     },
    106 
    107     /**
    108      * Handles the mousedown event which starts the dragging of the splitter.
    109      * @param {!Event} e The mouse event.
    110      * @private
    111      */
    112     handleMouseDown_: function(e) {
    113       this.startDrag(e);
    114       // Default action is to start selection and to move focus.
    115       e.preventDefault();
    116     },
    117 
    118     /**
    119      * Handles the mousemove event which moves the splitter as the user moves
    120      * the mouse. Calls splitter drag move handler.
    121      * @param {!Event} e The mouse event.
    122      * @private
    123      */
    124     handleMouseMove_: function(e) {
    125       var rtl = this.ownerDocument.defaultView.getComputedStyle(this).
    126           direction == 'rtl';
    127       var dirMultiplier = rtl ? -1 : 1;
    128       var deltaX = dirMultiplier * (e.clientX - this.startX_);
    129       this.handleSplitterDragMove(deltaX);
    130     },
    131 
    132     /**
    133      * Handles the mouse up event which ends the dragging of the splitter.
    134      * @param {!Event} e The mouse event.
    135      * @private
    136      */
    137     handleMouseUp_: function(e) {
    138       this.endDrag();
    139     },
    140 
    141     /**
    142      * Handles start of the splitter dragging. Saves current width of the
    143      * element being resized.
    144      * @protected
    145      */
    146     handleSplitterDragStart: function() {
    147       // Use the computed width style as the base so that we can ignore what
    148       // box sizing the element has.
    149       var leftComponent = this.previousElementSibling;
    150       var doc = leftComponent.ownerDocument;
    151       this.startWidth_ = parseFloat(
    152           doc.defaultView.getComputedStyle(leftComponent).width);
    153     },
    154 
    155     /**
    156      * Handles splitter moves. Updates width of the element being resized.
    157      * @param {number} changeX The change of splitter horizontal position.
    158      * @protected
    159      */
    160     handleSplitterDragMove: function(deltaX) {
    161       var leftComponent = this.previousElementSibling;
    162       leftComponent.style.width = this.startWidth_ + deltaX + 'px';
    163     },
    164 
    165     /**
    166      * Handles end of the splitter dragging. This fires a 'resize' event if the
    167      * size changed.
    168      * @protected
    169      */
    170     handleSplitterDragEnd: function() {
    171       // Check if the size changed.
    172       var leftComponent = this.previousElementSibling;
    173       var doc = leftComponent.ownerDocument;
    174       var computedWidth = parseFloat(
    175           doc.defaultView.getComputedStyle(leftComponent).width);
    176       if (this.startWidth_ != computedWidth)
    177         cr.dispatchSimpleEvent(this, 'resize');
    178     },
    179   };
    180 
    181   return {
    182     Splitter: Splitter
    183   }
    184 });
    185