blob: 1b36844e65ffcb9d90c7aac7e89d6b8b54a80ea4 [file] [log] [blame]
// 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
};
});