chromium/chrome/browser/resources/chromeos/arc_support/ui.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// NOTE: ui.js and the autogenerated ui.m.js module version are deprecated.
// These files and files that depend on them should only be used by legacy UIs
// that have not yet been updated to new patterns. Use Web Components in any new
// code.

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.
   * @closurePrimitive {asserts.matchesReturn}
   */
  /* #export */ function decorate(source, constr) {
    let elements;
    if (typeof source === 'string') {
      elements = document.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 = document;
    }
    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() {
   *     ...
   *   },
   *   ...
   * };
   * </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.
   */
  /* #export */ function define(tagNameOrFunction) {
    let createFunction;
    let 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;
      if (el.decorate) {
        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.
   */
  /* #export */ 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'.
   */
  /* #export */ 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.
   */
  /* #export */ 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);
  }

  // #cr_define_end
  console.warn('crbug/1173575, non-JS module files deprecated.');
  return {
    decorate: decorate,
    define: define,
    limitInputWidth: limitInputWidth,
    toCssPx: toCssPx,
    swallowDoubleClick: swallowDoubleClick,
  };
});