• 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
5cr.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 {number} parentEl The parent element that should limit the size.
108   * @param {number} min The minimum width.
109   * @param {number} opt_scale Optional scale factor to apply to the width.
110   */
111  function limitInputWidth(el, parentEl, min, opt_scale) {
112    // Needs a size larger than borders
113    el.style.width = '10px';
114    var doc = el.ownerDocument;
115    var win = doc.defaultView;
116    var computedStyle = win.getComputedStyle(el);
117    var parentComputedStyle = win.getComputedStyle(parentEl);
118    var rtl = computedStyle.direction == 'rtl';
119
120    // To get the max width we get the width of the treeItem minus the position
121    // of the input.
122    var inputRect = el.getBoundingClientRect();  // box-sizing
123    var parentRect = parentEl.getBoundingClientRect();
124    var startPos = rtl ? parentRect.right - inputRect.right :
125        inputRect.left - parentRect.left;
126
127    // Add up border and padding of the input.
128    var inner = parseInt(computedStyle.borderLeftWidth, 10) +
129        parseInt(computedStyle.paddingLeft, 10) +
130        parseInt(computedStyle.paddingRight, 10) +
131        parseInt(computedStyle.borderRightWidth, 10);
132
133    // We also need to subtract the padding of parent to prevent it to overflow.
134    var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
135        parseInt(parentComputedStyle.paddingRight, 10);
136
137    var max = parentEl.clientWidth - startPos - inner - parentPadding;
138    if (opt_scale)
139      max *= opt_scale;
140
141    function limit() {
142      if (el.scrollWidth > max) {
143        el.style.width = max + 'px';
144      } else {
145        el.style.width = 0;
146        var sw = el.scrollWidth;
147        if (sw < min) {
148          el.style.width = min + 'px';
149        } else {
150          el.style.width = sw + 'px';
151        }
152      }
153    }
154
155    el.addEventListener('input', limit);
156    limit();
157  }
158
159  /**
160   * Takes a number and spits out a value CSS will be happy with. To avoid
161   * subpixel layout issues, the value is rounded to the nearest integral value.
162   * @param {number} pixels The number of pixels.
163   * @return {string} e.g. '16px'.
164   */
165  function toCssPx(pixels) {
166    if (!window.isFinite(pixels))
167      console.error('Pixel value is not a number: ' + pixels);
168    return Math.round(pixels) + 'px';
169  }
170
171  /**
172   * Users complain they occasionaly use doubleclicks instead of clicks
173   * (http://crbug.com/140364). To fix it we freeze click handling for
174   * the doubleclick time interval.
175   * @param {MouseEvent} e Initial click event.
176   */
177  function swallowDoubleClick(e) {
178    var doc = e.target.ownerDocument;
179    var counter = Math.min(1, e.detail);
180    function swallow(e) {
181      e.stopPropagation();
182      e.preventDefault();
183    }
184    function onclick(e) {
185      if (e.detail > counter) {
186        counter = e.detail;
187        // Swallow the click since it's a click inside the doubleclick timeout.
188        swallow(e);
189      } else {
190        // Stop tracking clicks and let regular handling.
191        doc.removeEventListener('dblclick', swallow, true);
192        doc.removeEventListener('click', onclick, true);
193      }
194    }
195    // The following 'click' event (if e.type == 'mouseup') mustn't be taken
196    // into account (it mustn't stop tracking clicks). Start event listening
197    // after zero timeout.
198    setTimeout(function() {
199      doc.addEventListener('click', onclick, true);
200      doc.addEventListener('dblclick', swallow, true);
201    }, 0);
202  }
203
204  return {
205    decorate: decorate,
206    define: define,
207    limitInputWidth: limitInputWidth,
208    toCssPx: toCssPx,
209    swallowDoubleClick: swallowDoubleClick
210  };
211});
212