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 cr.define('cr.ui', function() { 6 7 /** 8 * Decorates elements as an instance of a class. 9 * @param {string|!Element} source The way to find the element(s) to decorate. 10 * If this is a string then {@code querySeletorAll} is used to find the 11 * elements to decorate. 12 * @param {!Function} constr The constructor to decorate with. The constr 13 * needs to have a {@code decorate} function. 14 */ 15 function decorate(source, constr) { 16 var elements; 17 if (typeof source == 'string') 18 elements = cr.doc.querySelectorAll(source); 19 else 20 elements = [source]; 21 22 for (var i = 0, el; el = elements[i]; i++) { 23 if (!(el instanceof constr)) 24 constr.decorate(el); 25 } 26 } 27 28 /** 29 * Helper function for creating new element for define. 30 */ 31 function createElementHelper(tagName, opt_bag) { 32 // Allow passing in ownerDocument to create in a different document. 33 var doc; 34 if (opt_bag && opt_bag.ownerDocument) 35 doc = opt_bag.ownerDocument; 36 else 37 doc = cr.doc; 38 return doc.createElement(tagName); 39 } 40 41 /** 42 * Creates the constructor for a UI element class. 43 * 44 * Usage: 45 * <pre> 46 * var List = cr.ui.define('list'); 47 * List.prototype = { 48 * __proto__: HTMLUListElement.prototype, 49 * decorate: function() { 50 * ... 51 * }, 52 * ... 53 * }; 54 * </pre> 55 * 56 * @param {string|Function} tagNameOrFunction The tagName or 57 * function to use for newly created elements. If this is a function it 58 * needs to return a new element when called. 59 * @return {function(Object=):Element} The constructor function which takes 60 * an optional property bag. The function also has a static 61 * {@code decorate} method added to it. 62 */ 63 function define(tagNameOrFunction) { 64 var createFunction, tagName; 65 if (typeof tagNameOrFunction == 'function') { 66 createFunction = tagNameOrFunction; 67 tagName = ''; 68 } else { 69 createFunction = createElementHelper; 70 tagName = tagNameOrFunction; 71 } 72 73 /** 74 * Creates a new UI element constructor. 75 * @param {Object=} opt_propertyBag Optional bag of properties to set on the 76 * object after created. The property {@code ownerDocument} is special 77 * cased and it allows you to create the element in a different 78 * document than the default. 79 * @constructor 80 */ 81 function f(opt_propertyBag) { 82 var el = createFunction(tagName, opt_propertyBag); 83 f.decorate(el); 84 for (var propertyName in opt_propertyBag) { 85 el[propertyName] = opt_propertyBag[propertyName]; 86 } 87 return el; 88 } 89 90 /** 91 * Decorates an element as a UI element class. 92 * @param {!Element} el The element to decorate. 93 */ 94 f.decorate = function(el) { 95 el.__proto__ = f.prototype; 96 el.decorate(); 97 }; 98 99 return f; 100 } 101 102 /** 103 * Input elements do not grow and shrink with their content. This is a simple 104 * (and not very efficient) way of handling shrinking to content with support 105 * for min width and limited by the width of the parent element. 106 * @param {!HTMLElement} el The element to limit the width for. 107 * @param {!HTMLElement} parentEl The parent element that should limit the 108 * size. 109 * @param {number} min The minimum width. 110 * @param {number=} opt_scale Optional scale factor to apply to the width. 111 */ 112 function limitInputWidth(el, parentEl, min, opt_scale) { 113 // Needs a size larger than borders 114 el.style.width = '10px'; 115 var doc = el.ownerDocument; 116 var win = doc.defaultView; 117 var computedStyle = win.getComputedStyle(el); 118 var parentComputedStyle = win.getComputedStyle(parentEl); 119 var rtl = computedStyle.direction == 'rtl'; 120 121 // To get the max width we get the width of the treeItem minus the position 122 // of the input. 123 var inputRect = el.getBoundingClientRect(); // box-sizing 124 var parentRect = parentEl.getBoundingClientRect(); 125 var startPos = rtl ? parentRect.right - inputRect.right : 126 inputRect.left - parentRect.left; 127 128 // Add up border and padding of the input. 129 var inner = parseInt(computedStyle.borderLeftWidth, 10) + 130 parseInt(computedStyle.paddingLeft, 10) + 131 parseInt(computedStyle.paddingRight, 10) + 132 parseInt(computedStyle.borderRightWidth, 10); 133 134 // We also need to subtract the padding of parent to prevent it to overflow. 135 var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : 136 parseInt(parentComputedStyle.paddingRight, 10); 137 138 var max = parentEl.clientWidth - startPos - inner - parentPadding; 139 if (opt_scale) 140 max *= opt_scale; 141 142 function limit() { 143 if (el.scrollWidth > max) { 144 el.style.width = max + 'px'; 145 } else { 146 el.style.width = 0; 147 var sw = el.scrollWidth; 148 if (sw < min) { 149 el.style.width = min + 'px'; 150 } else { 151 el.style.width = sw + 'px'; 152 } 153 } 154 } 155 156 el.addEventListener('input', limit); 157 limit(); 158 } 159 160 /** 161 * Takes a number and spits out a value CSS will be happy with. To avoid 162 * subpixel layout issues, the value is rounded to the nearest integral value. 163 * @param {number} pixels The number of pixels. 164 * @return {string} e.g. '16px'. 165 */ 166 function toCssPx(pixels) { 167 if (!window.isFinite(pixels)) 168 console.error('Pixel value is not a number: ' + pixels); 169 return Math.round(pixels) + 'px'; 170 } 171 172 /** 173 * Users complain they occasionaly use doubleclicks instead of clicks 174 * (http://crbug.com/140364). To fix it we freeze click handling for 175 * the doubleclick time interval. 176 * @param {MouseEvent} e Initial click event. 177 */ 178 function swallowDoubleClick(e) { 179 var doc = e.target.ownerDocument; 180 var counter = Math.min(1, e.detail); 181 function swallow(e) { 182 e.stopPropagation(); 183 e.preventDefault(); 184 } 185 function onclick(e) { 186 if (e.detail > counter) { 187 counter = e.detail; 188 // Swallow the click since it's a click inside the doubleclick timeout. 189 swallow(e); 190 } else { 191 // Stop tracking clicks and let regular handling. 192 doc.removeEventListener('dblclick', swallow, true); 193 doc.removeEventListener('click', onclick, true); 194 } 195 } 196 // The following 'click' event (if e.type == 'mouseup') mustn't be taken 197 // into account (it mustn't stop tracking clicks). Start event listening 198 // after zero timeout. 199 setTimeout(function() { 200 doc.addEventListener('click', onclick, true); 201 doc.addEventListener('dblclick', swallow, true); 202 }, 0); 203 } 204 205 return { 206 decorate: decorate, 207 define: define, 208 limitInputWidth: limitInputWidth, 209 toCssPx: toCssPx, 210 swallowDoubleClick: swallowDoubleClick 211 }; 212 }); 213