Home | History | Annotate | Download | only in cr
      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