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
21cr.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