• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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      this.addEventListener('touchstart', this.handleTouchStart_.bind(this),
73                            true);
74    },
75
76    /**
77     * Starts the dragging of the splitter. Adds listeners for mouse or touch
78     * events and calls splitter drag start handler.
79     * @param {number} clientX X position of the mouse or touch event that
80     *                         started the drag.
81     * @param {boolean} isTouchEvent True if the drag started by touch event.
82     */
83    startDrag: function(clientX, isTouchEvent) {
84      if (this.handlers_) {
85        console.log('Concurent drags');
86        this.endDrag_();
87      }
88      if (isTouchEvent) {
89        var endDragBound = this.endDrag_.bind(this);
90        this.handlers_ = {
91          'touchmove': this.handleTouchMove_.bind(this),
92          'touchend': endDragBound,
93          'touchcancel': endDragBound,
94
95          // Another touch start (we somehow missed touchend or touchcancel).
96          'touchstart': endDragBound,
97        };
98      } else {
99        this.handlers_ = {
100          'mousemove': this.handleMouseMove_.bind(this),
101          'mouseup': this.handleMouseUp_.bind(this),
102        };
103      }
104
105      var doc = this.ownerDocument;
106
107      // Use capturing events on the document to get events when the mouse
108      // leaves the document.
109      for (var eventType in this.handlers_) {
110        doc.addEventListener(eventType, this.handlers_[eventType], true);
111      }
112
113      this.startX_ = clientX;
114      this.handleSplitterDragStart();
115    },
116
117    /**
118     * Ends the dragging of the splitter. Removes listeners set in startDrag
119     * and calls splitter drag end handler.
120     * @private
121     */
122    endDrag_: function() {
123      var doc = this.ownerDocument;
124      for (var eventType in this.handlers_) {
125        doc.removeEventListener(eventType, this.handlers_[eventType], true);
126      }
127      this.handlers_ = null;
128      this.handleSplitterDragEnd();
129    },
130
131    /**
132     * Handles the mousedown event which starts the dragging of the splitter.
133     * @param {!MouseEvent} e The mouse event.
134     * @private
135     */
136    handleMouseDown_: function(e) {
137      this.startDrag(e.clientX, false);
138      // Default action is to start selection and to move focus.
139      e.preventDefault();
140    },
141
142    /**
143     * Handles the touchstart event which starts the dragging of the splitter.
144     * @param {!TouchEvent} e The touch event.
145     * @private
146     */
147    handleTouchStart_: function(e) {
148      if (e.touches.length == 1) {
149        this.startDrag(e.touches[0].clientX, true);
150        e.preventDefault();
151      }
152    },
153
154    /**
155     * Handles the mousemove event which moves the splitter as the user moves
156     * the mouse.
157     * @param {!MouseEvent} e The mouse event.
158     * @private
159     */
160    handleMouseMove_: function(e) {
161      this.handleMove_(e.clientX);
162    },
163
164    /**
165     * Handles the touch move event.
166     * @param {!TouchEvent} e The touch event.
167     */
168    handleTouchMove_: function(e) {
169      if (e.touches.length == 1)
170        this.handleMove_(e.touches[0].clientX);
171    },
172
173    /**
174     * Common part of handling mousemove and touchmove. Calls splitter drag
175     * move handler.
176     * @param {number} clientX X position of the mouse or touch event.
177     * @private
178     */
179    handleMove_: function(clientX) {
180      var rtl = this.ownerDocument.defaultView.getComputedStyle(this).
181          direction == 'rtl';
182      var dirMultiplier = rtl ? -1 : 1;
183      var deltaX = dirMultiplier * (clientX - this.startX_);
184      this.handleSplitterDragMove(deltaX);
185    },
186
187    /**
188     * Handles the mouse up event which ends the dragging of the splitter.
189     * @param {!MouseEvent} e The mouse event.
190     * @private
191     */
192    handleMouseUp_: function(e) {
193      this.endDrag_();
194    },
195
196    /**
197     * Handles start of the splitter dragging. Saves current width of the
198     * element being resized.
199     * @protected
200     */
201    handleSplitterDragStart: function() {
202      // Use the computed width style as the base so that we can ignore what
203      // box sizing the element has.
204      var leftComponent = this.previousElementSibling;
205      var doc = leftComponent.ownerDocument;
206      this.startWidth_ = parseFloat(
207          doc.defaultView.getComputedStyle(leftComponent).width);
208    },
209
210    /**
211     * Handles splitter moves. Updates width of the element being resized.
212     * @param {number} changeX The change of splitter horizontal position.
213     * @protected
214     */
215    handleSplitterDragMove: function(deltaX) {
216      var leftComponent = this.previousElementSibling;
217      leftComponent.style.width = this.startWidth_ + deltaX + 'px';
218    },
219
220    /**
221     * Handles end of the splitter dragging. This fires a 'resize' event if the
222     * size changed.
223     * @protected
224     */
225    handleSplitterDragEnd: function() {
226      // Check if the size changed.
227      var leftComponent = this.previousElementSibling;
228      var doc = leftComponent.ownerDocument;
229      var computedWidth = parseFloat(
230          doc.defaultView.getComputedStyle(leftComponent).width);
231      if (this.startWidth_ != computedWidth)
232        cr.dispatchSimpleEvent(this, 'resize');
233    },
234  };
235
236  return {
237    Splitter: Splitter
238  };
239});
240