| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| cr.define('cr.ui', function() { |
| |
| /** |
| * Decorates elements as an instance of a class. |
| * @param {string|!Element} source The way to find the element(s) to decorate. |
| * If this is a string then {@code querySeletorAll} is used to find the |
| * elements to decorate. |
| * @param {!Function} constr The constructor to decorate with. The constr |
| * needs to have a {@code decorate} function. |
| */ |
| function decorate(source, constr) { |
| let elements; |
| if (typeof source == 'string') { |
| elements = cr.doc.querySelectorAll(source); |
| } else { |
| elements = [source]; |
| } |
| |
| for (let i = 0, el; el = elements[i]; i++) { |
| if (!(el instanceof constr)) { |
| constr.decorate(el); |
| } |
| } |
| } |
| |
| /** |
| * Helper function for creating new element for define. |
| */ |
| function createElementHelper(tagName, opt_bag) { |
| // Allow passing in ownerDocument to create in a different document. |
| let doc; |
| if (opt_bag && opt_bag.ownerDocument) { |
| doc = opt_bag.ownerDocument; |
| } else { |
| doc = cr.doc; |
| } |
| return doc.createElement(tagName); |
| } |
| |
| /** |
| * Creates the constructor for a UI element class. |
| * |
| * Usage: |
| * <pre> |
| * var List = cr.ui.define('list'); |
| * List.prototype = { |
| * __proto__: HTMLUListElement.prototype, |
| * decorate: function() { |
| * ... |
| * }, |
| * ... |
| * }; |
| * </pre> |
| * |
| * @param {string|Function} tagNameOrFunction The tagName or |
| * function to use for newly created elements. If this is a function it |
| * needs to return a new element when called. |
| * @return {function(Object=):Element} The constructor function which takes |
| * an optional property bag. The function also has a static |
| * {@code decorate} method added to it. |
| */ |
| function define(tagNameOrFunction) { |
| let createFunction, tagName; |
| if (typeof tagNameOrFunction == 'function') { |
| createFunction = tagNameOrFunction; |
| tagName = ''; |
| } else { |
| createFunction = createElementHelper; |
| tagName = tagNameOrFunction; |
| } |
| |
| /** |
| * Creates a new UI element constructor. |
| * @param {Object=} opt_propertyBag Optional bag of properties to set on the |
| * object after created. The property {@code ownerDocument} is special |
| * cased and it allows you to create the element in a different |
| * document than the default. |
| * @constructor |
| */ |
| function f(opt_propertyBag) { |
| const el = createFunction(tagName, opt_propertyBag); |
| f.decorate(el); |
| for (const propertyName in opt_propertyBag) { |
| el[propertyName] = opt_propertyBag[propertyName]; |
| } |
| return el; |
| } |
| |
| /** |
| * Decorates an element as a UI element class. |
| * @param {!Element} el The element to decorate. |
| */ |
| f.decorate = function(el) { |
| el.__proto__ = f.prototype; |
| el.decorate(); |
| }; |
| |
| return f; |
| } |
| |
| /** |
| * Input elements do not grow and shrink with their content. This is a simple |
| * (and not very efficient) way of handling shrinking to content with support |
| * for min width and limited by the width of the parent element. |
| * @param {!HTMLElement} el The element to limit the width for. |
| * @param {!HTMLElement} parentEl The parent element that should limit the |
| * size. |
| * @param {number} min The minimum width. |
| * @param {number=} opt_scale Optional scale factor to apply to the width. |
| */ |
| function limitInputWidth(el, parentEl, min, opt_scale) { |
| // Needs a size larger than borders |
| el.style.width = '10px'; |
| const doc = el.ownerDocument; |
| const win = doc.defaultView; |
| const computedStyle = win.getComputedStyle(el); |
| const parentComputedStyle = win.getComputedStyle(parentEl); |
| const rtl = computedStyle.direction == 'rtl'; |
| |
| // To get the max width we get the width of the treeItem minus the position |
| // of the input. |
| const inputRect = el.getBoundingClientRect(); // box-sizing |
| const parentRect = parentEl.getBoundingClientRect(); |
| const startPos = rtl ? parentRect.right - inputRect.right : |
| inputRect.left - parentRect.left; |
| |
| // Add up border and padding of the input. |
| const inner = parseInt(computedStyle.borderLeftWidth, 10) + |
| parseInt(computedStyle.paddingLeft, 10) + |
| parseInt(computedStyle.paddingRight, 10) + |
| parseInt(computedStyle.borderRightWidth, 10); |
| |
| // We also need to subtract the padding of parent to prevent it to overflow. |
| const parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : |
| parseInt(parentComputedStyle.paddingRight, 10); |
| |
| let max = parentEl.clientWidth - startPos - inner - parentPadding; |
| if (opt_scale) { |
| max *= opt_scale; |
| } |
| |
| function limit() { |
| if (el.scrollWidth > max) { |
| el.style.width = max + 'px'; |
| } else { |
| el.style.width = 0; |
| const sw = el.scrollWidth; |
| if (sw < min) { |
| el.style.width = min + 'px'; |
| } else { |
| el.style.width = sw + 'px'; |
| } |
| } |
| } |
| |
| el.addEventListener('input', limit); |
| limit(); |
| } |
| |
| /** |
| * Takes a number and spits out a value CSS will be happy with. To avoid |
| * subpixel layout issues, the value is rounded to the nearest integral value. |
| * @param {number} pixels The number of pixels. |
| * @return {string} e.g. '16px'. |
| */ |
| function toCssPx(pixels) { |
| if (!window.isFinite(pixels)) { |
| console.error('Pixel value is not a number: ' + pixels); |
| } |
| return Math.round(pixels) + 'px'; |
| } |
| |
| /** |
| * Users complain they occasionaly use doubleclicks instead of clicks |
| * (http://crbug.com/140364). To fix it we freeze click handling for |
| * the doubleclick time interval. |
| * @param {MouseEvent} e Initial click event. |
| */ |
| function swallowDoubleClick(e) { |
| const doc = e.target.ownerDocument; |
| let counter = Math.min(1, e.detail); |
| function swallow(e) { |
| e.stopPropagation(); |
| e.preventDefault(); |
| } |
| function onclick(e) { |
| if (e.detail > counter) { |
| counter = e.detail; |
| // Swallow the click since it's a click inside the doubleclick timeout. |
| swallow(e); |
| } else { |
| // Stop tracking clicks and let regular handling. |
| doc.removeEventListener('dblclick', swallow, true); |
| doc.removeEventListener('click', onclick, true); |
| } |
| } |
| // The following 'click' event (if e.type == 'mouseup') mustn't be taken |
| // into account (it mustn't stop tracking clicks). Start event listening |
| // after zero timeout. |
| setTimeout(function() { |
| doc.addEventListener('click', onclick, true); |
| doc.addEventListener('dblclick', swallow, true); |
| }, 0); |
| } |
| |
| return { |
| decorate: decorate, |
| define: define, |
| limitInputWidth: limitInputWidth, |
| toCssPx: toCssPx, |
| swallowDoubleClick: swallowDoubleClick |
| }; |
| }); |