paletterenderer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Renderer for {@link goog.ui.Palette}s.
  16. *
  17. * @author attila@google.com (Attila Bodis)
  18. */
  19. goog.provide('goog.ui.PaletteRenderer');
  20. goog.require('goog.a11y.aria');
  21. goog.require('goog.a11y.aria.Role');
  22. goog.require('goog.a11y.aria.State');
  23. goog.require('goog.array');
  24. goog.require('goog.asserts');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.NodeIterator');
  27. goog.require('goog.dom.NodeType');
  28. goog.require('goog.dom.TagName');
  29. goog.require('goog.dom.classlist');
  30. goog.require('goog.iter');
  31. goog.require('goog.style');
  32. goog.require('goog.ui.ControlRenderer');
  33. goog.require('goog.userAgent');
  34. /**
  35. * Default renderer for {@link goog.ui.Palette}s. Renders the palette as an
  36. * HTML table wrapped in a DIV, with one palette item per cell:
  37. *
  38. * <div class="goog-palette">
  39. * <table class="goog-palette-table">
  40. * <tbody class="goog-palette-body">
  41. * <tr class="goog-palette-row">
  42. * <td class="goog-palette-cell">...Item 0...</td>
  43. * <td class="goog-palette-cell">...Item 1...</td>
  44. * ...
  45. * </tr>
  46. * <tr class="goog-palette-row">
  47. * ...
  48. * </tr>
  49. * </tbody>
  50. * </table>
  51. * </div>
  52. *
  53. * @constructor
  54. * @extends {goog.ui.ControlRenderer}
  55. */
  56. goog.ui.PaletteRenderer = function() {
  57. goog.ui.ControlRenderer.call(this);
  58. };
  59. goog.inherits(goog.ui.PaletteRenderer, goog.ui.ControlRenderer);
  60. goog.addSingletonGetter(goog.ui.PaletteRenderer);
  61. /**
  62. * Globally unique ID sequence for cells rendered by this renderer class.
  63. * @type {number}
  64. * @private
  65. */
  66. goog.ui.PaletteRenderer.cellId_ = 0;
  67. /**
  68. * Default CSS class to be applied to the root element of components rendered
  69. * by this renderer.
  70. * @type {string}
  71. */
  72. goog.ui.PaletteRenderer.CSS_CLASS = goog.getCssName('goog-palette');
  73. /**
  74. * Returns the palette items arranged in a table wrapped in a DIV, with the
  75. * renderer's own CSS class and additional state-specific classes applied to
  76. * it.
  77. * @param {goog.ui.Control} palette goog.ui.Palette to render.
  78. * @return {!Element} Root element for the palette.
  79. * @override
  80. */
  81. goog.ui.PaletteRenderer.prototype.createDom = function(palette) {
  82. var classNames = this.getClassNames(palette);
  83. var element = palette.getDomHelper().createDom(
  84. goog.dom.TagName.DIV, classNames,
  85. this.createGrid(
  86. /** @type {Array<Node>} */ (palette.getContent()), palette.getSize(),
  87. palette.getDomHelper()));
  88. goog.a11y.aria.setRole(element, goog.a11y.aria.Role.GRID);
  89. return element;
  90. };
  91. /**
  92. * Returns the given items in a table with {@code size.width} columns and
  93. * {@code size.height} rows. If the table is too big, empty cells will be
  94. * created as needed. If the table is too small, the items that don't fit
  95. * will not be rendered.
  96. * @param {Array<Node>} items Palette items.
  97. * @param {goog.math.Size} size Palette size (columns x rows); both dimensions
  98. * must be specified as numbers.
  99. * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
  100. * @return {!Element} Palette table element.
  101. */
  102. goog.ui.PaletteRenderer.prototype.createGrid = function(items, size, dom) {
  103. var rows = [];
  104. for (var row = 0, index = 0; row < size.height; row++) {
  105. var cells = [];
  106. for (var column = 0; column < size.width; column++) {
  107. var item = items && items[index++];
  108. cells.push(this.createCell(item, dom));
  109. }
  110. rows.push(this.createRow(cells, dom));
  111. }
  112. return this.createTable(rows, dom);
  113. };
  114. /**
  115. * Returns a table element (or equivalent) that wraps the given rows.
  116. * @param {Array<Element>} rows Array of row elements.
  117. * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
  118. * @return {!Element} Palette table element.
  119. */
  120. goog.ui.PaletteRenderer.prototype.createTable = function(rows, dom) {
  121. var table = dom.createDom(
  122. goog.dom.TagName.TABLE, goog.getCssName(this.getCssClass(), 'table'),
  123. dom.createDom(
  124. goog.dom.TagName.TBODY, goog.getCssName(this.getCssClass(), 'body'),
  125. rows));
  126. table.cellSpacing = '0';
  127. table.cellPadding = '0';
  128. return table;
  129. };
  130. /**
  131. * Returns a table row element (or equivalent) that wraps the given cells.
  132. * @param {Array<Element>} cells Array of cell elements.
  133. * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
  134. * @return {!Element} Row element.
  135. */
  136. goog.ui.PaletteRenderer.prototype.createRow = function(cells, dom) {
  137. var row = dom.createDom(
  138. goog.dom.TagName.TR, goog.getCssName(this.getCssClass(), 'row'), cells);
  139. goog.a11y.aria.setRole(row, goog.a11y.aria.Role.ROW);
  140. return row;
  141. };
  142. /**
  143. * Returns a table cell element (or equivalent) that wraps the given palette
  144. * item (which must be a DOM node).
  145. * @param {Node|string} node Palette item.
  146. * @param {goog.dom.DomHelper} dom DOM helper for document interaction.
  147. * @return {!Element} Cell element.
  148. */
  149. goog.ui.PaletteRenderer.prototype.createCell = function(node, dom) {
  150. var cell = dom.createDom(
  151. goog.dom.TagName.TD, {
  152. 'class': goog.getCssName(this.getCssClass(), 'cell'),
  153. // Cells must have an ID, for accessibility, so we generate one here.
  154. 'id': goog.getCssName(this.getCssClass(), 'cell-') +
  155. goog.ui.PaletteRenderer.cellId_++
  156. },
  157. node);
  158. goog.a11y.aria.setRole(cell, goog.a11y.aria.Role.GRIDCELL);
  159. // Initialize to an unselected state.
  160. goog.a11y.aria.setState(cell, goog.a11y.aria.State.SELECTED, false);
  161. if (!goog.dom.getTextContent(cell) && !goog.a11y.aria.getLabel(cell)) {
  162. var ariaLabelForCell = this.findAriaLabelForCell_(cell);
  163. if (ariaLabelForCell) {
  164. goog.a11y.aria.setLabel(cell, ariaLabelForCell);
  165. }
  166. }
  167. return cell;
  168. };
  169. /**
  170. * Descends the DOM and tries to find an aria label for a grid cell
  171. * from the first child with a label or title.
  172. * @param {!Element} cell The cell.
  173. * @return {string} The label to use.
  174. * @private
  175. */
  176. goog.ui.PaletteRenderer.prototype.findAriaLabelForCell_ = function(cell) {
  177. var iter = new goog.dom.NodeIterator(cell);
  178. var label = '';
  179. var node;
  180. while (!label && (node = goog.iter.nextOrValue(iter, null))) {
  181. if (node.nodeType == goog.dom.NodeType.ELEMENT) {
  182. label =
  183. goog.a11y.aria.getLabel(/** @type {!Element} */ (node)) || node.title;
  184. }
  185. }
  186. return label;
  187. };
  188. /**
  189. * Overrides {@link goog.ui.ControlRenderer#canDecorate} to always return false.
  190. * @param {Element} element Ignored.
  191. * @return {boolean} False, since palettes don't support the decorate flow (for
  192. * now).
  193. * @override
  194. */
  195. goog.ui.PaletteRenderer.prototype.canDecorate = function(element) {
  196. return false;
  197. };
  198. /**
  199. * Overrides {@link goog.ui.ControlRenderer#decorate} to be a no-op, since
  200. * palettes don't support the decorate flow (for now).
  201. * @param {goog.ui.Control} palette Ignored.
  202. * @param {Element} element Ignored.
  203. * @return {null} Always null.
  204. * @override
  205. */
  206. goog.ui.PaletteRenderer.prototype.decorate = function(palette, element) {
  207. return null;
  208. };
  209. /**
  210. * Overrides {@link goog.ui.ControlRenderer#setContent} for palettes. Locates
  211. * the HTML table representing the palette grid, and replaces the contents of
  212. * each cell with a new element from the array of nodes passed as the second
  213. * argument. If the new content has too many items the table will have more
  214. * rows added to fit, if there are less items than the table has cells, then the
  215. * left over cells will be empty.
  216. * @param {Element} element Root element of the palette control.
  217. * @param {goog.ui.ControlContent} content Array of items to replace existing
  218. * palette items.
  219. * @override
  220. */
  221. goog.ui.PaletteRenderer.prototype.setContent = function(element, content) {
  222. var items = /** @type {Array<Node>} */ (content);
  223. if (element) {
  224. var tbody = goog.dom.getElementsByTagNameAndClass(
  225. goog.dom.TagName.TBODY, goog.getCssName(this.getCssClass(), 'body'),
  226. element)[0];
  227. if (tbody) {
  228. var index = 0;
  229. goog.array.forEach(tbody.rows, function(row) {
  230. goog.array.forEach(row.cells, function(cell) {
  231. goog.dom.removeChildren(cell);
  232. if (items) {
  233. var item = items[index++];
  234. if (item) {
  235. goog.dom.appendChild(cell, item);
  236. }
  237. }
  238. });
  239. });
  240. // Make space for any additional items.
  241. if (index < items.length) {
  242. var cells = [];
  243. var dom = goog.dom.getDomHelper(element);
  244. var width = tbody.rows[0].cells.length;
  245. while (index < items.length) {
  246. var item = items[index++];
  247. cells.push(this.createCell(item, dom));
  248. if (cells.length == width) {
  249. var row = this.createRow(cells, dom);
  250. goog.dom.appendChild(tbody, row);
  251. cells.length = 0;
  252. }
  253. }
  254. if (cells.length > 0) {
  255. while (cells.length < width) {
  256. cells.push(this.createCell('', dom));
  257. }
  258. var row = this.createRow(cells, dom);
  259. goog.dom.appendChild(tbody, row);
  260. }
  261. }
  262. }
  263. // Make sure the new contents are still unselectable.
  264. goog.style.setUnselectable(element, true, goog.userAgent.GECKO);
  265. }
  266. };
  267. /**
  268. * Returns the item corresponding to the given node, or null if the node is
  269. * neither a palette cell nor part of a palette item.
  270. * @param {goog.ui.Palette} palette Palette in which to look for the item.
  271. * @param {Node} node Node to look for.
  272. * @return {Node} The corresponding palette item (null if not found).
  273. */
  274. goog.ui.PaletteRenderer.prototype.getContainingItem = function(palette, node) {
  275. var root = palette.getElement();
  276. while (node && node.nodeType == goog.dom.NodeType.ELEMENT && node != root) {
  277. if (node.tagName == goog.dom.TagName.TD &&
  278. goog.dom.classlist.contains(
  279. /** @type {!Element} */ (node),
  280. goog.getCssName(this.getCssClass(), 'cell'))) {
  281. return node.firstChild;
  282. }
  283. node = node.parentNode;
  284. }
  285. return null;
  286. };
  287. /**
  288. * Updates the highlight styling of the palette cell containing the given node
  289. * based on the value of the Boolean argument.
  290. * @param {goog.ui.Palette} palette Palette containing the item.
  291. * @param {Node} node Item whose cell is to be highlighted or un-highlighted.
  292. * @param {boolean} highlight If true, the cell is highlighted; otherwise it is
  293. * un-highlighted.
  294. */
  295. goog.ui.PaletteRenderer.prototype.highlightCell = function(
  296. palette, node, highlight) {
  297. if (node) {
  298. var cell = this.getCellForItem(node);
  299. goog.asserts.assert(cell);
  300. goog.dom.classlist.enable(
  301. cell, goog.getCssName(this.getCssClass(), 'cell-hover'), highlight);
  302. // See http://www.w3.org/TR/2006/WD-aria-state-20061220/#activedescendent
  303. // for an explanation of the activedescendent.
  304. if (highlight) {
  305. goog.a11y.aria.setState(
  306. palette.getElementStrict(), goog.a11y.aria.State.ACTIVEDESCENDANT,
  307. cell.id);
  308. } else if (
  309. cell.id ==
  310. goog.a11y.aria.getState(
  311. palette.getElementStrict(),
  312. goog.a11y.aria.State.ACTIVEDESCENDANT)) {
  313. goog.a11y.aria.removeState(
  314. palette.getElementStrict(), goog.a11y.aria.State.ACTIVEDESCENDANT);
  315. }
  316. }
  317. };
  318. /**
  319. * @param {Node} node Item whose cell is to be returned.
  320. * @return {Element} The grid cell for the palette item.
  321. */
  322. goog.ui.PaletteRenderer.prototype.getCellForItem = function(node) {
  323. return /** @type {Element} */ (node ? node.parentNode : null);
  324. };
  325. /**
  326. * Updates the selection styling of the palette cell containing the given node
  327. * based on the value of the Boolean argument.
  328. * @param {goog.ui.Palette} palette Palette containing the item.
  329. * @param {Node} node Item whose cell is to be selected or deselected.
  330. * @param {boolean} select If true, the cell is selected; otherwise it is
  331. * deselected.
  332. */
  333. goog.ui.PaletteRenderer.prototype.selectCell = function(palette, node, select) {
  334. if (node) {
  335. var cell = /** @type {!Element} */ (node.parentNode);
  336. goog.dom.classlist.enable(
  337. cell, goog.getCssName(this.getCssClass(), 'cell-selected'), select);
  338. goog.a11y.aria.setState(cell, goog.a11y.aria.State.SELECTED, select);
  339. }
  340. };
  341. /**
  342. * Returns the CSS class to be applied to the root element of components
  343. * rendered using this renderer.
  344. * @return {string} Renderer-specific CSS class.
  345. * @override
  346. */
  347. goog.ui.PaletteRenderer.prototype.getCssClass = function() {
  348. return goog.ui.PaletteRenderer.CSS_CLASS;
  349. };