chromium/third_party/google-closure-library/closure/goog/ui/dimensionpickerrenderer.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview The default renderer for a goog.dom.DimensionPicker.  A
 * dimension picker allows the user to visually select a row and column count.
 * It looks like a palette but in order to minimize DOM load it is rendered.
 * using CSS background tiling instead of as a grid of nodes.
 */

goog.provide('goog.ui.DimensionPickerRenderer');

goog.forwardDeclare('goog.ui.DimensionPicker');
goog.require('goog.a11y.aria.Announcer');
goog.require('goog.a11y.aria.LivePriority');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.i18n.bidi');
goog.require('goog.style');
goog.require('goog.ui.ControlRenderer');
goog.require('goog.userAgent');



/**
 * Default renderer for {@link goog.ui.DimensionPicker}s.  Renders the
 * palette as two divs, one with the un-highlighted background, and one with the
 * highlighted background.
 *
 * @constructor
 * @extends {goog.ui.ControlRenderer}
 */
goog.ui.DimensionPickerRenderer = function() {
  'use strict';
  goog.ui.ControlRenderer.call(this);

  /** @private {goog.a11y.aria.Announcer} */
  this.announcer_ = new goog.a11y.aria.Announcer();
};
goog.inherits(goog.ui.DimensionPickerRenderer, goog.ui.ControlRenderer);
goog.addSingletonGetter(goog.ui.DimensionPickerRenderer);


/**
 * Default CSS class to be applied to the root element of components rendered
 * by this renderer.
 * @type {string}
 */
goog.ui.DimensionPickerRenderer.CSS_CLASS =
    goog.getCssName('goog-dimension-picker');


/**
 * Return the underlying div for the given outer element.
 * @param {Element} element The root element.
 * @return {Element} The underlying div.
 * @private
 */
goog.ui.DimensionPickerRenderer.prototype.getUnderlyingDiv_ = function(
    element) {
  'use strict';
  return /** @type {Element} */ (element.firstChild.childNodes[1]);
};


/**
 * Return the highlight div for the given outer element.
 * @param {Element} element The root element.
 * @return {Element} The highlight div.
 * @private
 */
goog.ui.DimensionPickerRenderer.prototype.getHighlightDiv_ = function(element) {
  'use strict';
  return /** @type {Element} */ (element.firstChild.lastChild);
};


/**
 * Return the status message div for the given outer element.
 * @param {Element} element The root element.
 * @return {Element} The status message div.
 * @private
 */
goog.ui.DimensionPickerRenderer.prototype.getStatusDiv_ = function(element) {
  'use strict';
  return /** @type {Element} */ (element.lastChild);
};


/**
 * Return the invisible mouse catching div for the given outer element.
 * @param {Element} element The root element.
 * @return {Element} The invisible mouse catching div.
 * @private
 */
goog.ui.DimensionPickerRenderer.prototype.getMouseCatcher_ = function(element) {
  'use strict';
  return /** @type {Element} */ (element.firstChild.firstChild);
};


/**
 * Overrides {@link goog.ui.ControlRenderer#canDecorate} to allow decorating
 * empty DIVs only.
 * @param {Element} element The element to check.
 * @return {boolean} Whether if the element is an empty div.
 * @override
 */
goog.ui.DimensionPickerRenderer.prototype.canDecorate = function(element) {
  'use strict';
  return element.tagName == goog.dom.TagName.DIV && !element.firstChild;
};


/**
 * Overrides {@link goog.ui.ControlRenderer#decorate} to decorate empty DIVs.
 * @param {goog.ui.Control} control goog.ui.DimensionPicker to decorate.
 * @param {Element} element The element to decorate.
 * @return {Element} The decorated element.
 * @override
 */
goog.ui.DimensionPickerRenderer.prototype.decorate = function(
    control, element) {
  'use strict';
  var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  goog.ui.DimensionPickerRenderer.superClass_.decorate.call(
      this, palette, element);

  this.addElementContents_(palette, element);
  this.updateSize(palette, element);

  return element;
};


/**
 * Scales various elements in order to update the palette's size.
 * @param {goog.ui.DimensionPicker} palette The palette object.
 * @param {Element} element The element to set the style of.
 */
goog.ui.DimensionPickerRenderer.prototype.updateSize = function(
    palette, element) {
  'use strict';
  var size = palette.getSize();

  element.style.width = size.width + 'em';

  var underlyingDiv = this.getUnderlyingDiv_(element);
  underlyingDiv.style.width = size.width + 'em';
  underlyingDiv.style.height = size.height + 'em';

  if (palette.isRightToLeft()) {
    this.adjustParentDirection_(palette, element);
  }
};


/**
 * Adds the appropriate content elements to the given outer DIV.
 * @param {goog.ui.DimensionPicker} palette The palette object.
 * @param {Element} element The element to decorate.
 * @private
 */
goog.ui.DimensionPickerRenderer.prototype.addElementContents_ = function(
    palette, element) {
  'use strict';
  // First we create a single div containing three stacked divs.  The bottom div
  // catches mouse events.  We can't use document level mouse move detection as
  // we could lose events to iframes.  This is especially important in Firefox 2
  // in which TrogEdit creates iframes. The middle div uses a css tiled
  // background image to represent deselected tiles.  The top div uses a
  // different css tiled background image to represent selected tiles.
  var mouseCatcherDiv = palette.getDomHelper().createDom(
      goog.dom.TagName.DIV,
      goog.getCssName(this.getCssClass(), 'mousecatcher'));
  var unhighlightedDiv =
      palette.getDomHelper().createDom(goog.dom.TagName.DIV, {
        'class': goog.getCssName(this.getCssClass(), 'unhighlighted'),
        'style': 'width:100%;height:100%'
      });
  var highlightedDiv = palette.getDomHelper().createDom(
      goog.dom.TagName.DIV, goog.getCssName(this.getCssClass(), 'highlighted'));
  element.appendChild(
      palette.getDomHelper().createDom(
          goog.dom.TagName.DIV, {
            'style': 'width:100%;height:100%;touch-action:none;'
          },
          mouseCatcherDiv, unhighlightedDiv, highlightedDiv));

  // Lastly we add a div to store the text version of the current state.
  element.appendChild(
      palette.getDomHelper().createDom(
          goog.dom.TagName.DIV, goog.getCssName(this.getCssClass(), 'status')));
};


/**
 * Creates a div and adds the appropriate contents to it.
 * @param {goog.ui.Control} control Picker to render.
 * @return {!Element} Root element for the palette.
 * @override
 */
goog.ui.DimensionPickerRenderer.prototype.createDom = function(control) {
  'use strict';
  var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  var classNames = this.getClassNames(palette);
  // Hide the element from screen readers so they don't announce "1 of 1" for
  // the perceived number of items in the palette.
  var element = palette.getDomHelper().createDom(
      goog.dom.TagName.DIV,
      {'class': classNames ? classNames.join(' ') : '', 'aria-hidden': 'true'});
  this.addElementContents_(palette, element);
  this.updateSize(palette, element);
  return element;
};


/**
 * Initializes the control's DOM when the control enters the document.  Called
 * from {@link goog.ui.Control#enterDocument}.
 * @param {goog.ui.Control} control Palette whose DOM is to be
 *     initialized as it enters the document.
 * @override
 */
goog.ui.DimensionPickerRenderer.prototype.initializeDom = function(control) {
  'use strict';
  var palette = /** @type {goog.ui.DimensionPicker} */ (control);
  goog.ui.DimensionPickerRenderer.superClass_.initializeDom.call(this, palette);

  // Make the displayed highlighted size match the dimension picker's value.
  var highlightedSize = palette.getValue();
  this.setHighlightedSize(
      palette, highlightedSize.width, highlightedSize.height);

  this.positionMouseCatcher(palette);
};


/**
 * Get the element to listen for mouse move events on.
 * @param {goog.ui.DimensionPicker} palette The palette to listen on.
 * @return {Element} The element to listen for mouse move events on.
 */
goog.ui.DimensionPickerRenderer.prototype.getMouseMoveElement = function(
    palette) {
  'use strict';
  return /** @type {Element} */ (palette.getElement().firstChild);
};


/**
 * Returns the x offset in to the grid for the given mouse x position.
 * @param {goog.ui.DimensionPicker} palette The table size palette.
 * @param {number} x The mouse event x position.
 * @return {number} The x offset in to the grid.
 */
goog.ui.DimensionPickerRenderer.prototype.getGridOffsetX = function(
    palette, x) {
  'use strict';
  // TODO(robbyw): Don't rely on magic 18 - measure each palette's em size.
  return Math.min(palette.maxColumns, Math.ceil(x / 18));
};


/**
 * Returns the y offset in to the grid for the given mouse y position.
 * @param {goog.ui.DimensionPicker} palette The table size palette.
 * @param {number} y The mouse event y position.
 * @return {number} The y offset in to the grid.
 */
goog.ui.DimensionPickerRenderer.prototype.getGridOffsetY = function(
    palette, y) {
  'use strict';
  return Math.min(palette.maxRows, Math.ceil(y / 18));
};


/**
 * Sets the highlighted size. Does nothing if the palette hasn't been rendered.
 * @param {goog.ui.DimensionPicker} palette The table size palette.
 * @param {number} columns The number of columns to highlight.
 * @param {number} rows The number of rows to highlight.
 */
goog.ui.DimensionPickerRenderer.prototype.setHighlightedSize = function(
    palette, columns, rows) {
  'use strict';
  var element = palette.getElement();
  // Can't update anything if DimensionPicker hasn't been rendered.
  if (!element) {
    return;
  }

  // Style the highlight div.
  var style = this.getHighlightDiv_(element).style;
  style.width = columns + 'em';
  style.height = rows + 'em';

  // Explicitly set style.right so the element grows to the left when increase
  // in width.
  if (palette.isRightToLeft()) {
    style.right = '0';
  }

  /**
   * @desc The dimension of the columns and rows currently selected in the
   * dimension picker, as text that can be spoken by a screen reader.
   */
  var MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS = goog.getMsg(
      '{$numCols} by {$numRows}',
      {'numCols': String(columns), 'numRows': String(rows)});
  this.announcer_.say(
      MSG_DIMENSION_PICKER_HIGHLIGHTED_DIMENSIONS,
      goog.a11y.aria.LivePriority.ASSERTIVE);

  // Update the size text.
  goog.dom.setTextContent(
      this.getStatusDiv_(element),
      goog.i18n.bidi.enforceLtrInText(columns + ' x ' + rows));
};


/**
 * Position the mouse catcher such that it receives mouse events past the
 * selectedsize up to the maximum size.  Takes care to not introduce scrollbars.
 * Should be called on enter document and when the window changes size.
 * @param {goog.ui.DimensionPicker} palette The table size palette.
 */
goog.ui.DimensionPickerRenderer.prototype.positionMouseCatcher = function(
    palette) {
  'use strict';
  var mouseCatcher = this.getMouseCatcher_(palette.getElement());
  var doc = goog.dom.getOwnerDocument(mouseCatcher);
  var body = doc.body;

  var position = goog.style.getRelativePosition(mouseCatcher, body);

  // Hide the mouse catcher so it doesn't affect the body's scroll size.
  mouseCatcher.style.display = 'none';

  // Compute the maximum size the catcher can be without introducing scrolling.
  var xAvailableEm = (palette.isRightToLeft() && position.x > 0) ?
      Math.floor(position.x / 18) :
      Math.floor((body.scrollWidth - position.x) / 18);

  // Computing available height is more complicated - we need to check the
  // window's inner height.
  var height;
  if (goog.userAgent.IE) {
    // Offset 20px to make up for scrollbar size.
    height = goog.style.getClientViewportElement(body).scrollHeight - 20;
  } else {
    var win = goog.dom.getWindow(doc);
    // Offset 20px to make up for scrollbar size.
    height = Math.max(win.innerHeight, body.scrollHeight) - 20;
  }
  var yAvailableEm = Math.floor((height - position.y) / 18);

  // Resize and display the mouse catcher.
  mouseCatcher.style.width = Math.min(palette.maxColumns, xAvailableEm) + 'em';
  mouseCatcher.style.height = Math.min(palette.maxRows, yAvailableEm) + 'em';
  mouseCatcher.style.display = '';

  // Explicitly set style.right so the mouse catcher is positioned on the left
  // side instead of right.
  if (palette.isRightToLeft()) {
    mouseCatcher.style.right = '0';
  }
};


/**
 * Returns the CSS class to be applied to the root element of components
 * rendered using this renderer.
 * @return {string} Renderer-specific CSS class.
 * @override
 */
goog.ui.DimensionPickerRenderer.prototype.getCssClass = function() {
  'use strict';
  return goog.ui.DimensionPickerRenderer.CSS_CLASS;
};


/**
 * This function adjusts the positioning from 'left' and 'top' to 'right' and
 * 'top' as appropriate for RTL control.  This is so when the dimensionpicker
 * grow in width, the containing element grow to the left instead of right.
 * This won't be necessary if goog.ui.SubMenu rendering code would position RTL
 * control with 'right' and 'top'.
 * @private
 *
 * @param {goog.ui.DimensionPicker} palette The palette object.
 * @param {Element} element The palette's element.
 */
goog.ui.DimensionPickerRenderer.prototype.adjustParentDirection_ = function(
    palette, element) {
  'use strict';
  var parent = palette.getParent();
  if (parent) {
    var parentElement = parent.getElement();

    // Anchors the containing element to the right so it grows to the left
    // when it increase in width.
    var right = goog.style.getStyle(parentElement, 'right');
    if (right == '') {
      var parentPos = goog.style.getPosition(parentElement);
      var parentSize = goog.style.getSize(parentElement);
      if (parentSize.width != 0 && parentPos.x != 0) {
        var visibleRect =
            goog.style.getBounds(goog.style.getClientViewportElement());
        var visibleWidth = visibleRect.width;
        right = visibleWidth - parentPos.x - parentSize.width;
        goog.style.setStyle(parentElement, 'right', right + 'px');
      }
    }

    // When a table is inserted, the containing elemet's position is
    // recalculated the next time it shows, set left back to '' to prevent
    // extra white space on the left.
    var left = goog.style.getStyle(parentElement, 'left');
    if (left != '') {
      goog.style.setStyle(parentElement, 'left', '');
    }
  } else {
    goog.style.setStyle(element, 'right', '0px');
  }
};