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