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