// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Renderer for {@link goog.ui.Palette}s. * * @author attila@google.com (Attila Bodis) */ goog.provide('goog.ui.PaletteRenderer'); goog.require('goog.a11y.aria'); goog.require('goog.a11y.aria.Role'); goog.require('goog.a11y.aria.State'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.NodeIterator'); goog.require('goog.dom.NodeType'); goog.require('goog.dom.TagName'); goog.require('goog.dom.classlist'); goog.require('goog.iter'); goog.require('goog.style'); goog.require('goog.ui.ControlRenderer'); goog.require('goog.userAgent'); /** * Default renderer for {@link goog.ui.Palette}s. Renders the palette as an * HTML table wrapped in a DIV, with one palette item per cell: * *
* * * * * * ... * * * ... * * *
...Item 0......Item 1...
*
* * @constructor * @extends {goog.ui.ControlRenderer} */ goog.ui.PaletteRenderer = function() { goog.ui.ControlRenderer.call(this); }; goog.inherits(goog.ui.PaletteRenderer, goog.ui.ControlRenderer); goog.addSingletonGetter(goog.ui.PaletteRenderer); /** * Globally unique ID sequence for cells rendered by this renderer class. * @type {number} * @private */ goog.ui.PaletteRenderer.cellId_ = 0; /** * Default CSS class to be applied to the root element of components rendered * by this renderer. * @type {string} */ goog.ui.PaletteRenderer.CSS_CLASS = goog.getCssName('goog-palette'); /** * Returns the palette items arranged in a table wrapped in a DIV, with the * renderer's own CSS class and additional state-specific classes applied to * it. * @param {goog.ui.Control} palette goog.ui.Palette to render. * @return {!Element} Root element for the palette. * @override */ goog.ui.PaletteRenderer.prototype.createDom = function(palette) { var classNames = this.getClassNames(palette); var element = palette.getDomHelper().createDom( goog.dom.TagName.DIV, classNames, this.createGrid( /** @type {Array} */ (palette.getContent()), palette.getSize(), palette.getDomHelper())); goog.a11y.aria.setRole(element, goog.a11y.aria.Role.GRID); return element; }; /** * Returns the given items in a table with {@code size.width} columns and * {@code size.height} rows. If the table is too big, empty cells will be * created as needed. If the table is too small, the items that don't fit * will not be rendered. * @param {Array} items Palette items. * @param {goog.math.Size} size Palette size (columns x rows); both dimensions * must be specified as numbers. * @param {goog.dom.DomHelper} dom DOM helper for document interaction. * @return {!Element} Palette table element. */ goog.ui.PaletteRenderer.prototype.createGrid = function(items, size, dom) { var rows = []; for (var row = 0, index = 0; row < size.height; row++) { var cells = []; for (var column = 0; column < size.width; column++) { var item = items && items[index++]; cells.push(this.createCell(item, dom)); } rows.push(this.createRow(cells, dom)); } return this.createTable(rows, dom); }; /** * Returns a table element (or equivalent) that wraps the given rows. * @param {Array} rows Array of row elements. * @param {goog.dom.DomHelper} dom DOM helper for document interaction. * @return {!Element} Palette table element. */ goog.ui.PaletteRenderer.prototype.createTable = function(rows, dom) { var table = dom.createDom( goog.dom.TagName.TABLE, goog.getCssName(this.getCssClass(), 'table'), dom.createDom( goog.dom.TagName.TBODY, goog.getCssName(this.getCssClass(), 'body'), rows)); table.cellSpacing = '0'; table.cellPadding = '0'; return table; }; /** * Returns a table row element (or equivalent) that wraps the given cells. * @param {Array} cells Array of cell elements. * @param {goog.dom.DomHelper} dom DOM helper for document interaction. * @return {!Element} Row element. */ goog.ui.PaletteRenderer.prototype.createRow = function(cells, dom) { var row = dom.createDom( goog.dom.TagName.TR, goog.getCssName(this.getCssClass(), 'row'), cells); goog.a11y.aria.setRole(row, goog.a11y.aria.Role.ROW); return row; }; /** * Returns a table cell element (or equivalent) that wraps the given palette * item (which must be a DOM node). * @param {Node|string} node Palette item. * @param {goog.dom.DomHelper} dom DOM helper for document interaction. * @return {!Element} Cell element. */ goog.ui.PaletteRenderer.prototype.createCell = function(node, dom) { var cell = dom.createDom( goog.dom.TagName.TD, { 'class': goog.getCssName(this.getCssClass(), 'cell'), // Cells must have an ID, for accessibility, so we generate one here. 'id': goog.getCssName(this.getCssClass(), 'cell-') + goog.ui.PaletteRenderer.cellId_++ }, node); goog.a11y.aria.setRole(cell, goog.a11y.aria.Role.GRIDCELL); // Initialize to an unselected state. goog.a11y.aria.setState(cell, goog.a11y.aria.State.SELECTED, false); if (!goog.dom.getTextContent(cell) && !goog.a11y.aria.getLabel(cell)) { var ariaLabelForCell = this.findAriaLabelForCell_(cell); if (ariaLabelForCell) { goog.a11y.aria.setLabel(cell, ariaLabelForCell); } } return cell; }; /** * Descends the DOM and tries to find an aria label for a grid cell * from the first child with a label or title. * @param {!Element} cell The cell. * @return {string} The label to use. * @private */ goog.ui.PaletteRenderer.prototype.findAriaLabelForCell_ = function(cell) { var iter = new goog.dom.NodeIterator(cell); var label = ''; var node; while (!label && (node = goog.iter.nextOrValue(iter, null))) { if (node.nodeType == goog.dom.NodeType.ELEMENT) { label = goog.a11y.aria.getLabel(/** @type {!Element} */ (node)) || node.title; } } return label; }; /** * Overrides {@link goog.ui.ControlRenderer#canDecorate} to always return false. * @param {Element} element Ignored. * @return {boolean} False, since palettes don't support the decorate flow (for * now). * @override */ goog.ui.PaletteRenderer.prototype.canDecorate = function(element) { return false; }; /** * Overrides {@link goog.ui.ControlRenderer#decorate} to be a no-op, since * palettes don't support the decorate flow (for now). * @param {goog.ui.Control} palette Ignored. * @param {Element} element Ignored. * @return {null} Always null. * @override */ goog.ui.PaletteRenderer.prototype.decorate = function(palette, element) { return null; }; /** * Overrides {@link goog.ui.ControlRenderer#setContent} for palettes. Locates * the HTML table representing the palette grid, and replaces the contents of * each cell with a new element from the array of nodes passed as the second * argument. If the new content has too many items the table will have more * rows added to fit, if there are less items than the table has cells, then the * left over cells will be empty. * @param {Element} element Root element of the palette control. * @param {goog.ui.ControlContent} content Array of items to replace existing * palette items. * @override */ goog.ui.PaletteRenderer.prototype.setContent = function(element, content) { var items = /** @type {Array} */ (content); if (element) { var tbody = goog.dom.getElementsByTagNameAndClass( goog.dom.TagName.TBODY, goog.getCssName(this.getCssClass(), 'body'), element)[0]; if (tbody) { var index = 0; goog.array.forEach(tbody.rows, function(row) { goog.array.forEach(row.cells, function(cell) { goog.dom.removeChildren(cell); if (items) { var item = items[index++]; if (item) { goog.dom.appendChild(cell, item); } } }); }); // Make space for any additional items. if (index < items.length) { var cells = []; var dom = goog.dom.getDomHelper(element); var width = tbody.rows[0].cells.length; while (index < items.length) { var item = items[index++]; cells.push(this.createCell(item, dom)); if (cells.length == width) { var row = this.createRow(cells, dom); goog.dom.appendChild(tbody, row); cells.length = 0; } } if (cells.length > 0) { while (cells.length < width) { cells.push(this.createCell('', dom)); } var row = this.createRow(cells, dom); goog.dom.appendChild(tbody, row); } } } // Make sure the new contents are still unselectable. goog.style.setUnselectable(element, true, goog.userAgent.GECKO); } }; /** * Returns the item corresponding to the given node, or null if the node is * neither a palette cell nor part of a palette item. * @param {goog.ui.Palette} palette Palette in which to look for the item. * @param {Node} node Node to look for. * @return {Node} The corresponding palette item (null if not found). */ goog.ui.PaletteRenderer.prototype.getContainingItem = function(palette, node) { var root = palette.getElement(); while (node && node.nodeType == goog.dom.NodeType.ELEMENT && node != root) { if (node.tagName == goog.dom.TagName.TD && goog.dom.classlist.contains( /** @type {!Element} */ (node), goog.getCssName(this.getCssClass(), 'cell'))) { return node.firstChild; } node = node.parentNode; } return null; }; /** * Updates the highlight styling of the palette cell containing the given node * based on the value of the Boolean argument. * @param {goog.ui.Palette} palette Palette containing the item. * @param {Node} node Item whose cell is to be highlighted or un-highlighted. * @param {boolean} highlight If true, the cell is highlighted; otherwise it is * un-highlighted. */ goog.ui.PaletteRenderer.prototype.highlightCell = function( palette, node, highlight) { if (node) { var cell = this.getCellForItem(node); goog.asserts.assert(cell); goog.dom.classlist.enable( cell, goog.getCssName(this.getCssClass(), 'cell-hover'), highlight); // See http://www.w3.org/TR/2006/WD-aria-state-20061220/#activedescendent // for an explanation of the activedescendent. if (highlight) { goog.a11y.aria.setState( palette.getElementStrict(), goog.a11y.aria.State.ACTIVEDESCENDANT, cell.id); } else if ( cell.id == goog.a11y.aria.getState( palette.getElementStrict(), goog.a11y.aria.State.ACTIVEDESCENDANT)) { goog.a11y.aria.removeState( palette.getElementStrict(), goog.a11y.aria.State.ACTIVEDESCENDANT); } } }; /** * @param {Node} node Item whose cell is to be returned. * @return {Element} The grid cell for the palette item. */ goog.ui.PaletteRenderer.prototype.getCellForItem = function(node) { return /** @type {Element} */ (node ? node.parentNode : null); }; /** * Updates the selection styling of the palette cell containing the given node * based on the value of the Boolean argument. * @param {goog.ui.Palette} palette Palette containing the item. * @param {Node} node Item whose cell is to be selected or deselected. * @param {boolean} select If true, the cell is selected; otherwise it is * deselected. */ goog.ui.PaletteRenderer.prototype.selectCell = function(palette, node, select) { if (node) { var cell = /** @type {!Element} */ (node.parentNode); goog.dom.classlist.enable( cell, goog.getCssName(this.getCssClass(), 'cell-selected'), select); goog.a11y.aria.setState(cell, goog.a11y.aria.State.SELECTED, select); } }; /** * 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.PaletteRenderer.prototype.getCssClass = function() { return goog.ui.PaletteRenderer.CSS_CLASS; };