| // 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: |
| * |
| * <div class="goog-palette"> |
| * <table class="goog-palette-table"> |
| * <tbody class="goog-palette-body"> |
| * <tr class="goog-palette-row"> |
| * <td class="goog-palette-cell">...Item 0...</td> |
| * <td class="goog-palette-cell">...Item 1...</td> |
| * ... |
| * </tr> |
| * <tr class="goog-palette-row"> |
| * ... |
| * </tr> |
| * </tbody> |
| * </table> |
| * </div> |
| * |
| * @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 ? classNames.join(' ') : null, |
| this.createGrid(/** @type {Array<Node>} */(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<Node>} 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<Element>} 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<Element>} 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<Node>} */ (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; |
| }; |