chromium/third_party/blink/renderer/core/html/forms/resources/picker_common.js

'use strict';
/*
 * Copyright (C) 2012 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

let didOpenPicker = false;

/**
 * @param {!string} id
 */
function $(id) {
  return document.getElementById(id);
}

/**
 * @param {!string} tagName
 * @param {string=} opt_class
 * @param {string=} opt_text
 * @return {!Element}
 */
function createElement(tagName, opt_class, opt_text) {
  const element = document.createElement(tagName);
  if (opt_class)
    element.setAttribute('class', opt_class);
  if (opt_text)
    element.appendChild(document.createTextNode(opt_text));
  return element;
}

// ----------------------------------------------------------------

class Rectangle {
  /**
   * @param {!number|Rectangle|Object} xOrRect
   * @param {!number} y
   * @param {!number} width
   * @param {!number} height
   */
  constructor(xOrRect, y, width, height) {
    if (typeof xOrRect === 'object') {
      y = xOrRect.y;
      width = xOrRect.width;
      height = xOrRect.height;
      xOrRect = xOrRect.x;
    }
    this.x = xOrRect;
    this.y = y;
    this.width = width;
    this.height = height;
  }

  get maxX() {
    return this.x + this.width;
  }

  get maxY() {
    return this.y + this.height;
  }

  toString() {
    return 'Rectangle(' + this.x + ',' + this.y + ',' + this.width + ',' +
        this.height + ')';
  }

  /**
   * @param {!Rectangle} rect1
   * @param {!Rectangle} rect2
   * @return {?Rectangle}
   */
  static intersection(rect1, rect2) {
    const x = Math.max(rect1.x, rect2.x);
    const maxX = Math.min(rect1.maxX, rect2.maxX);
    const y = Math.max(rect1.y, rect2.y);
    const maxY = Math.min(rect1.maxY, rect2.maxY);
    const width = maxX - x;
    const height = maxY - y;
    if (width < 0 || height < 0)
      return null;
    return new Rectangle(x, y, width, height);
  }
}

// ----------------------------------------------------------------

/**
 * @param {!number} width in CSS pixel
 * @param {!number} height in CSS pixel
 */
function resizeWindow(width, height) {
  const zoom = global.params.zoomFactor ? global.params.zoomFactor : 1;
  setWindowRect(adjustWindowRect(
      width * zoom, height * zoom, width * zoom, height * zoom,
      /*allowOverlapWithAnchor=*/ true));
}

/**
 * @param {!number} width in DIP
 * @param {!number} height in DIP
 * @param {!number} minWidth in DIP
 * @param {!number} minHeight in DIP
 * @param {!boolean} allowOverlapWithAnchor Whether the window should be
 *                                          allowed to overlap with the inline
 *                                          anchor rect if there isn't
 *                                          sufficient horizontal/vertical
 *                                          space.
 * @return {!Rectangle} Adjusted rectangle in DIP
 */
function adjustWindowRect(
    width, height, minWidth, minHeight, allowOverlapWithAnchor) {
  const windowRect = new Rectangle(0, 0, Math.ceil(width), Math.ceil(height));

  if (!global.params.anchorRectInScreen)
    return windowRect;

  const anchorRect = new Rectangle(global.params.anchorRectInScreen);
  const availRect = new Rectangle(
      window.screen.availLeft, window.screen.availTop, window.screen.availWidth,
      window.screen.availHeight);

  _adjustWindowRectVertically(
      windowRect, availRect, anchorRect, minHeight, allowOverlapWithAnchor);
  _adjustWindowRectHorizontally(
      windowRect, availRect, anchorRect, minWidth, minHeight,
      allowOverlapWithAnchor);

  return windowRect;
}

/**
 * Arguments are DIPs.
 */
function _getAvailableVerticalSpace(availRect, anchorRect, minHeight) {
  let availableSpaceAbove = anchorRect.y - availRect.y;
  availableSpaceAbove =
      Math.max(0, Math.min(availRect.height, availableSpaceAbove));

  let availableSpaceBelow = availRect.maxY - anchorRect.maxY;
  availableSpaceBelow =
      Math.max(0, Math.min(availRect.height, availableSpaceBelow));

  return {availableSpaceAbove, availableSpaceBelow};
}

/**
 * Arguments are DIPs.
 */
function _isOverlappingAnchorVertically(
    availRect, anchorRect, minHeight, allowOverlapWithAnchor) {
  let {availableSpaceAbove, availableSpaceBelow} =
      _getAvailableVerticalSpace(availRect, anchorRect, minHeight);

  return (
      allowOverlapWithAnchor && minHeight > availableSpaceAbove &&
      minHeight > availableSpaceBelow);
}

/**
 * Arguments are DIPs.
 */
function _adjustWindowRectVertically(
    windowRect, availRect, anchorRect, minHeight, allowOverlapWithAnchor) {
  let {availableSpaceAbove, availableSpaceBelow} =
      _getAvailableVerticalSpace(availRect, anchorRect, minHeight);

  if (_isOverlappingAnchorVertically(
          availRect, anchorRect, minHeight, allowOverlapWithAnchor)) {
    windowRect.height = minHeight;
    // If there isn't room to fit either fully above or fully below the anchor,
    // position the window as far down as possible...
    if (minHeight <= availRect.height) {
      windowRect.y = availRect.maxY - minHeight;
    } else {
      // ...except if the window is asking for more height than the screen
      // has, in which case align the top of the window to the top of the screen
      // and let the bottom overflow.
      windowRect.y = availRect.y;
    }
  } else if (
      windowRect.height > availableSpaceBelow &&
      availableSpaceBelow < availableSpaceAbove) {
    windowRect.height = Math.min(windowRect.height, availableSpaceAbove);
    windowRect.height = Math.max(windowRect.height, minHeight);
    windowRect.y = anchorRect.y - windowRect.height;
  } else {
    windowRect.height = Math.min(windowRect.height, availableSpaceBelow);
    windowRect.height = Math.max(windowRect.height, minHeight);
    windowRect.y = anchorRect.maxY;
  }
}

/**
 * Arguments are DIPs.
 */
function _adjustWindowRectHorizontally(
    windowRect, availRect, anchorRect, minWidth, minHeight,
    allowOverlapWithAnchor) {
  windowRect.width = Math.min(windowRect.width, availRect.width);
  windowRect.width = Math.max(windowRect.width, minWidth);

  const isOverlappingAnchorVertically = _isOverlappingAnchorVertically(
      availRect, anchorRect, minHeight, allowOverlapWithAnchor);
  const isEnoughSpaceRightOfAnchor =
      anchorRect.maxX + windowRect.width < availRect.maxX;
  const isEnoughSpaceLeftOfAnchor =
      availRect.x + windowRect.width < anchorRect.x;

  if (isOverlappingAnchorVertically && isEnoughSpaceRightOfAnchor &&
      !(global.params.isRTL && isEnoughSpaceLeftOfAnchor)) {
    // Show the window to the right of the anchor so that the anchor isn't obscured
    // due to overlapping in the vertical direction.
    windowRect.x = anchorRect.maxX;
  } else if (isOverlappingAnchorVertically && isEnoughSpaceLeftOfAnchor) {
    // Show the window to the left of the anchor so that the anchor isn't obscured
    // due to overlapping in the vertical direction.
    windowRect.x = anchorRect.x - windowRect.width;
  } else {
    // Show the window over/under the anchor, rather than to the side.
    windowRect.x = anchorRect.x;
    // If we are getting clipped, we want to switch alignment to the right side
    // of the anchor rect as long as doing so will make the popup not clipped.
    const rightAlignedX = windowRect.x + anchorRect.width - windowRect.width;
    if (rightAlignedX >= availRect.x &&
        (windowRect.maxX > availRect.maxX || global.params.isRTL))
      windowRect.x = rightAlignedX;
  }
}

/**
 * @param {!Rectangle} rect Window position and size in DIP.
 */
function setWindowRect(rect) {
  if (window.frameElement) {
    window.frameElement.style.width = rect.width + 'px';
    window.frameElement.style.height = rect.height + 'px';
  } else {
    window.pagePopupController.setWindowRect(
        rect.x, rect.y, rect.width, rect.height);
  }
}

function hideWindow() {
  setWindowRect(adjustWindowRect(1, 1, 1, 1));
}

/**
 * @return {!boolean}
 */
function isWindowHidden() {
  // window.innerWidth and innerHeight are zoom-adjusted values.  If we call
  // setWindowRect with width=100 and the zoom-level is 2.0, innerWidth will
  // return 50.
  return window.innerWidth <= 1 && window.innerHeight <= 1;
}

window.addEventListener('resize', function() {
  if (isWindowHidden()) {
    window.dispatchEvent(new CustomEvent('didHide'));
  } else {
    window.dispatchEvent(new CustomEvent('didOpenPicker'));
    window.didOpenPicker = true;
  }
}, false);

/**
 * @return {!number}
 */
function getScrollbarWidth() {
  if (typeof window.scrollbarWidth === 'undefined') {
    const scrollDiv = document.createElement('div');
    scrollDiv.style.opacity = '0';
    scrollDiv.style.overflow = 'scroll';
    scrollDiv.style.width = '50px';
    scrollDiv.style.height = '50px';
    document.body.appendChild(scrollDiv);
    window.scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
    scrollDiv.parentNode.removeChild(scrollDiv);
  }
  return window.scrollbarWidth;
}

/**
 * @param {!string} className
 * @return {?Element}
 */
function enclosingNodeOrSelfWithClass(selfNode, className) {
  for (let node = selfNode; node && node !== selfNode.ownerDocument;
       node = node.parentNode) {
    if (node.nodeType === Node.ELEMENT_NODE &&
        node.classList.contains(className))
      return node;
  }
  return null;
}

// ----------------------------------------------------------------

class EventEmitter {
  constructor() {
  }

  /**
   * @param {!string} type
   * @param {!function({...*})} callback
   */
  on(type, callback) {
    console.assert(callback instanceof Function);
    if (!this._callbacks)
      this._callbacks = {};
    if (!this._callbacks[type])
      this._callbacks[type] = [];
    this._callbacks[type].push(callback);
  }

  hasListener(type) {
    if (!this._callbacks)
      return false;
    const callbacksForType = this._callbacks[type];
    if (!callbacksForType)
      return false;
    return callbacksForType.length > 0;
  }

  /**
   * @param {!string} type
   * @param {!function(Object)} callback
   */
  removeListener(type, callback) {
    if (!this._callbacks)
      return;
    const callbacksForType = this._callbacks[type];
    if (!callbacksForType)
      return;
    callbacksForType.splice(callbacksForType.indexOf(callback), 1);
    if (callbacksForType.length === 0)
      delete this._callbacks[type];
  }

  /**
   * @param {!string} type
   * @param {...*} var_args
   */
  dispatchEvent(type) {
    if (!this._callbacks)
      return;
    let callbacksForType = this._callbacks[type];
    if (!callbacksForType)
      return;
    callbacksForType = callbacksForType.slice(0);
    for (let i = 0; i < callbacksForType.length; ++i) {
      callbacksForType[i].apply(this, Array.prototype.slice.call(arguments, 1));
    }
  }
}

// ----------------------------------------------------------------

class Picker extends EventEmitter {
  /**
   * @param {!Element} element
   * @param {!Object} config
   */
  constructor(element, config) {
    super();
    this.element_ = element;
    this.config_ = config;
  }

  /**
   * @enum {number}
   */
  static Actions = {
    SET_VALUE: 0,
    CANCEL: -1,
  };

  /**
   * @param {!string} value
   */
  submitValue(value) {
    window.pagePopupController.setValue(value);
    window.pagePopupController.closePopup();
  }

  handleCancel() {
    window.pagePopupController.closePopup();
  }

  cleanup() {
  }
}

// ----------------------------------------------------------------

window.addEventListener('keyup', function(event) {
  // JAWS dispatches extra Alt events and unless we handle them they move the
  // focus and close the popup.
  if (event.key === 'Alt')
    event.preventDefault();
}, true);