palette.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. // Copyright 2007 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 A palette control. A palette is a grid that the user can
  16. * highlight or select via the keyboard or the mouse.
  17. *
  18. * @author attila@google.com (Attila Bodis)
  19. * @see ../demos/palette.html
  20. */
  21. goog.provide('goog.ui.Palette');
  22. goog.require('goog.array');
  23. goog.require('goog.dom');
  24. goog.require('goog.events');
  25. goog.require('goog.events.EventType');
  26. goog.require('goog.events.KeyCodes');
  27. goog.require('goog.math.Size');
  28. goog.require('goog.ui.Component');
  29. goog.require('goog.ui.Control');
  30. goog.require('goog.ui.PaletteRenderer');
  31. goog.require('goog.ui.SelectionModel');
  32. /**
  33. * A palette is a grid of DOM nodes that the user can highlight or select via
  34. * the keyboard or the mouse. The selection state of the palette is controlled
  35. * an ACTION event. Event listeners may retrieve the selected item using the
  36. * {@link #getSelectedItem} or {@link #getSelectedIndex} method.
  37. *
  38. * Use this class as the base for components like color palettes or emoticon
  39. * pickers. Use {@link #setContent} to set/change the items in the palette
  40. * after construction. See palette.html demo for example usage.
  41. *
  42. * @param {Array<Node>} items Array of DOM nodes to be displayed as items
  43. * in the palette grid (limited to one per cell).
  44. * @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
  45. * decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
  46. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
  47. * document interaction.
  48. * @constructor
  49. * @extends {goog.ui.Control}
  50. */
  51. goog.ui.Palette = function(items, opt_renderer, opt_domHelper) {
  52. goog.ui.Palette.base(
  53. this, 'constructor', items,
  54. opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);
  55. this.setAutoStates(
  56. goog.ui.Component.State.CHECKED | goog.ui.Component.State.SELECTED |
  57. goog.ui.Component.State.OPENED,
  58. false);
  59. /**
  60. * A fake component for dispatching events on palette cell changes.
  61. * @type {!goog.ui.Palette.CurrentCell_}
  62. * @private
  63. */
  64. this.currentCellControl_ = new goog.ui.Palette.CurrentCell_();
  65. this.currentCellControl_.setParentEventTarget(this);
  66. /**
  67. * @private {number} The last highlighted index, or -1 if it never had one.
  68. */
  69. this.lastHighlightedIndex_ = -1;
  70. };
  71. goog.inherits(goog.ui.Palette, goog.ui.Control);
  72. goog.tagUnsealableClass(goog.ui.Palette);
  73. /**
  74. * Events fired by the palette object
  75. * @enum {string}
  76. */
  77. goog.ui.Palette.EventType = {
  78. AFTER_HIGHLIGHT: goog.events.getUniqueId('afterhighlight')
  79. };
  80. /**
  81. * Palette dimensions (columns x rows). If the number of rows is undefined,
  82. * it is calculated on first use.
  83. * @type {goog.math.Size}
  84. * @private
  85. */
  86. goog.ui.Palette.prototype.size_ = null;
  87. /**
  88. * Index of the currently highlighted item (-1 if none).
  89. * @type {number}
  90. * @private
  91. */
  92. goog.ui.Palette.prototype.highlightedIndex_ = -1;
  93. /**
  94. * Selection model controlling the palette's selection state.
  95. * @type {goog.ui.SelectionModel}
  96. * @private
  97. */
  98. goog.ui.Palette.prototype.selectionModel_ = null;
  99. // goog.ui.Component / goog.ui.Control implementation.
  100. /** @override */
  101. goog.ui.Palette.prototype.disposeInternal = function() {
  102. goog.ui.Palette.superClass_.disposeInternal.call(this);
  103. if (this.selectionModel_) {
  104. this.selectionModel_.dispose();
  105. this.selectionModel_ = null;
  106. }
  107. this.size_ = null;
  108. this.currentCellControl_.dispose();
  109. };
  110. /**
  111. * Overrides {@link goog.ui.Control#setContentInternal} by also updating the
  112. * grid size and the selection model. Considered protected.
  113. * @param {goog.ui.ControlContent} content Array of DOM nodes to be displayed
  114. * as items in the palette grid (one item per cell).
  115. * @protected
  116. * @override
  117. */
  118. goog.ui.Palette.prototype.setContentInternal = function(content) {
  119. var items = /** @type {Array<Node>} */ (content);
  120. goog.ui.Palette.superClass_.setContentInternal.call(this, items);
  121. // Adjust the palette size.
  122. this.adjustSize_();
  123. // Add the items to the selection model, replacing previous items (if any).
  124. if (this.selectionModel_) {
  125. // We already have a selection model; just replace the items.
  126. this.selectionModel_.clear();
  127. this.selectionModel_.addItems(items);
  128. } else {
  129. // Create a selection model, initialize the items, and hook up handlers.
  130. this.selectionModel_ = new goog.ui.SelectionModel(items);
  131. this.selectionModel_.setSelectionHandler(goog.bind(this.selectItem_, this));
  132. this.getHandler().listen(
  133. this.selectionModel_, goog.events.EventType.SELECT,
  134. this.handleSelectionChange);
  135. }
  136. // In all cases, clear the highlight.
  137. this.highlightedIndex_ = -1;
  138. };
  139. /**
  140. * Overrides {@link goog.ui.Control#getCaption} to return the empty string,
  141. * since palettes don't have text captions.
  142. * @return {string} The empty string.
  143. * @override
  144. */
  145. goog.ui.Palette.prototype.getCaption = function() {
  146. return '';
  147. };
  148. /**
  149. * Overrides {@link goog.ui.Control#setCaption} to be a no-op, since palettes
  150. * don't have text captions.
  151. * @param {string} caption Ignored.
  152. * @override
  153. */
  154. goog.ui.Palette.prototype.setCaption = function(caption) {
  155. // Do nothing.
  156. };
  157. // Palette event handling.
  158. /**
  159. * Handles mouseover events. Overrides {@link goog.ui.Control#handleMouseOver}
  160. * by determining which palette item (if any) was moused over, highlighting it,
  161. * and un-highlighting any previously-highlighted item.
  162. * @param {goog.events.BrowserEvent} e Mouse event to handle.
  163. * @override
  164. */
  165. goog.ui.Palette.prototype.handleMouseOver = function(e) {
  166. goog.ui.Palette.superClass_.handleMouseOver.call(this, e);
  167. var item = this.getRenderer().getContainingItem(this, e.target);
  168. if (item && e.relatedTarget && goog.dom.contains(item, e.relatedTarget)) {
  169. // Ignore internal mouse moves.
  170. return;
  171. }
  172. if (item != this.getHighlightedItem()) {
  173. this.setHighlightedItem(item);
  174. }
  175. };
  176. /**
  177. * Handles mousedown events. Overrides {@link goog.ui.Control#handleMouseDown}
  178. * by ensuring that the item on which the user moused down is highlighted.
  179. * @param {goog.events.Event} e Mouse event to handle.
  180. * @override
  181. */
  182. goog.ui.Palette.prototype.handleMouseDown = function(e) {
  183. goog.ui.Palette.superClass_.handleMouseDown.call(this, e);
  184. if (this.isActive()) {
  185. // Make sure we move the highlight to the cell on which the user moused
  186. // down.
  187. var item = this.getRenderer().getContainingItem(this, e.target);
  188. if (item != this.getHighlightedItem()) {
  189. this.setHighlightedItem(item);
  190. }
  191. }
  192. };
  193. /**
  194. * Selects the currently highlighted palette item (triggered by mouseup or by
  195. * keyboard action). Overrides {@link goog.ui.Control#performActionInternal}
  196. * by selecting the highlighted item and dispatching an ACTION event.
  197. * @param {goog.events.Event} e Mouse or key event that triggered the action.
  198. * @return {boolean} True if the action was allowed to proceed, false otherwise.
  199. * @override
  200. */
  201. goog.ui.Palette.prototype.performActionInternal = function(e) {
  202. var highlightedItem = this.getHighlightedItem();
  203. if (highlightedItem) {
  204. if (e && this.shouldSelectHighlightedItem_(e)) {
  205. this.setSelectedItem(highlightedItem);
  206. }
  207. return goog.ui.Palette.base(this, 'performActionInternal', e);
  208. }
  209. return false;
  210. };
  211. /**
  212. * Determines whether to select the highlighted item while handling an internal
  213. * action. The highlighted item should not be selected if the action is a mouse
  214. * event occurring outside the palette or in an "empty" cell.
  215. * @param {!goog.events.Event} e Mouseup or key event being handled.
  216. * @return {boolean} True if the highlighted item should be selected.
  217. * @private
  218. */
  219. goog.ui.Palette.prototype.shouldSelectHighlightedItem_ = function(e) {
  220. if (!this.getSelectedItem()) {
  221. // It's always ok to select when nothing is selected yet.
  222. return true;
  223. } else if (e.type != 'mouseup') {
  224. // Keyboard can only act on valid cells.
  225. return true;
  226. } else {
  227. // Return whether or not the mouse action was in the palette.
  228. return !!this.getRenderer().getContainingItem(this, e.target);
  229. }
  230. };
  231. /**
  232. * Handles keyboard events dispatched while the palette has focus. Moves the
  233. * highlight on arrow keys, and selects the highlighted item on Enter or Space.
  234. * Returns true if the event was handled, false otherwise. In particular, if
  235. * the user attempts to navigate out of the grid, the highlight isn't changed,
  236. * and this method returns false; it is then up to the parent component to
  237. * handle the event (e.g. by wrapping the highlight around). Overrides {@link
  238. * goog.ui.Control#handleKeyEvent}.
  239. * @param {goog.events.KeyEvent} e Key event to handle.
  240. * @return {boolean} True iff the key event was handled by the component.
  241. * @override
  242. */
  243. goog.ui.Palette.prototype.handleKeyEvent = function(e) {
  244. var items = this.getContent();
  245. var numItems = items ? items.length : 0;
  246. var numColumns = this.size_.width;
  247. // If the component is disabled or the palette is empty, bail.
  248. if (numItems == 0 || !this.isEnabled()) {
  249. return false;
  250. }
  251. // User hit ENTER or SPACE; trigger action.
  252. if (e.keyCode == goog.events.KeyCodes.ENTER ||
  253. e.keyCode == goog.events.KeyCodes.SPACE) {
  254. return this.performActionInternal(e);
  255. }
  256. // User hit HOME or END; move highlight.
  257. if (e.keyCode == goog.events.KeyCodes.HOME) {
  258. this.setHighlightedIndex(0);
  259. return true;
  260. } else if (e.keyCode == goog.events.KeyCodes.END) {
  261. this.setHighlightedIndex(numItems - 1);
  262. return true;
  263. }
  264. // If nothing is highlighted, start from the selected index. If nothing is
  265. // selected either, highlightedIndex is -1.
  266. var highlightedIndex = this.highlightedIndex_ < 0 ? this.getSelectedIndex() :
  267. this.highlightedIndex_;
  268. switch (e.keyCode) {
  269. case goog.events.KeyCodes.LEFT:
  270. // If the highlighted index is uninitialized, or is at the beginning, move
  271. // it to the end.
  272. if (highlightedIndex == -1 || highlightedIndex == 0) {
  273. highlightedIndex = numItems;
  274. }
  275. this.setHighlightedIndex(highlightedIndex - 1);
  276. e.preventDefault();
  277. return true;
  278. break;
  279. case goog.events.KeyCodes.RIGHT:
  280. // If the highlighted index at the end, move it to the beginning.
  281. if (highlightedIndex == numItems - 1) {
  282. highlightedIndex = -1;
  283. }
  284. this.setHighlightedIndex(highlightedIndex + 1);
  285. e.preventDefault();
  286. return true;
  287. break;
  288. case goog.events.KeyCodes.UP:
  289. if (highlightedIndex == -1) {
  290. highlightedIndex = numItems + numColumns - 1;
  291. }
  292. if (highlightedIndex >= numColumns) {
  293. this.setHighlightedIndex(highlightedIndex - numColumns);
  294. e.preventDefault();
  295. return true;
  296. }
  297. break;
  298. case goog.events.KeyCodes.DOWN:
  299. if (highlightedIndex == -1) {
  300. highlightedIndex = -numColumns;
  301. }
  302. if (highlightedIndex < numItems - numColumns) {
  303. this.setHighlightedIndex(highlightedIndex + numColumns);
  304. e.preventDefault();
  305. return true;
  306. }
  307. break;
  308. }
  309. return false;
  310. };
  311. /**
  312. * Handles selection change events dispatched by the selection model.
  313. * @param {goog.events.Event} e Selection event to handle.
  314. */
  315. goog.ui.Palette.prototype.handleSelectionChange = function(e) {
  316. // No-op in the base class.
  317. };
  318. // Palette management.
  319. /**
  320. * Returns the size of the palette grid.
  321. * @return {goog.math.Size} Palette size (columns x rows).
  322. */
  323. goog.ui.Palette.prototype.getSize = function() {
  324. return this.size_;
  325. };
  326. /**
  327. * Sets the size of the palette grid to the given size. Callers can either
  328. * pass a single {@link goog.math.Size} or a pair of numbers (first the number
  329. * of columns, then the number of rows) to this method. In both cases, the
  330. * number of rows is optional and will be calculated automatically if needed.
  331. * It is an error to attempt to change the size of the palette after it has
  332. * been rendered.
  333. * @param {goog.math.Size|number} size Either a size object or the number of
  334. * columns.
  335. * @param {number=} opt_rows The number of rows (optional).
  336. */
  337. goog.ui.Palette.prototype.setSize = function(size, opt_rows) {
  338. if (this.getElement()) {
  339. throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
  340. }
  341. this.size_ = goog.isNumber(size) ?
  342. new goog.math.Size(size, /** @type {number} */ (opt_rows)) :
  343. size;
  344. // Adjust size, if needed.
  345. this.adjustSize_();
  346. };
  347. /**
  348. * Returns the 0-based index of the currently highlighted palette item, or -1
  349. * if no item is highlighted.
  350. * @return {number} Index of the highlighted item (-1 if none).
  351. */
  352. goog.ui.Palette.prototype.getHighlightedIndex = function() {
  353. return this.highlightedIndex_;
  354. };
  355. /**
  356. * Returns the currently highlighted palette item, or null if no item is
  357. * highlighted.
  358. * @return {Node} The highlighted item (undefined if none).
  359. */
  360. goog.ui.Palette.prototype.getHighlightedItem = function() {
  361. var items = this.getContent();
  362. return items && items[this.highlightedIndex_];
  363. };
  364. /**
  365. * @return {Element} The highlighted cell.
  366. * @private
  367. */
  368. goog.ui.Palette.prototype.getHighlightedCellElement_ = function() {
  369. return this.getRenderer().getCellForItem(this.getHighlightedItem());
  370. };
  371. /**
  372. * Highlights the item at the given 0-based index, or removes the highlight
  373. * if the argument is -1 or out of range. Any previously-highlighted item
  374. * will be un-highlighted.
  375. * @param {number} index 0-based index of the item to highlight.
  376. */
  377. goog.ui.Palette.prototype.setHighlightedIndex = function(index) {
  378. if (index != this.highlightedIndex_) {
  379. this.highlightIndex_(this.highlightedIndex_, false);
  380. this.lastHighlightedIndex_ = this.highlightedIndex_;
  381. this.highlightedIndex_ = index;
  382. this.highlightIndex_(index, true);
  383. this.dispatchEvent(goog.ui.Palette.EventType.AFTER_HIGHLIGHT);
  384. }
  385. };
  386. /**
  387. * Highlights the given item, or removes the highlight if the argument is null
  388. * or invalid. Any previously-highlighted item will be un-highlighted.
  389. * @param {Node|undefined} item Item to highlight.
  390. */
  391. goog.ui.Palette.prototype.setHighlightedItem = function(item) {
  392. var items = /** @type {Array<Node>} */ (this.getContent());
  393. this.setHighlightedIndex(
  394. (items && item) ? goog.array.indexOf(items, item) : -1);
  395. };
  396. /**
  397. * Returns the 0-based index of the currently selected palette item, or -1
  398. * if no item is selected.
  399. * @return {number} Index of the selected item (-1 if none).
  400. */
  401. goog.ui.Palette.prototype.getSelectedIndex = function() {
  402. return this.selectionModel_ ? this.selectionModel_.getSelectedIndex() : -1;
  403. };
  404. /**
  405. * Returns the currently selected palette item, or null if no item is selected.
  406. * @return {Node} The selected item (null if none).
  407. */
  408. goog.ui.Palette.prototype.getSelectedItem = function() {
  409. return this.selectionModel_ ?
  410. /** @type {Node} */ (this.selectionModel_.getSelectedItem()) :
  411. null;
  412. };
  413. /**
  414. * Selects the item at the given 0-based index, or clears the selection
  415. * if the argument is -1 or out of range. Any previously-selected item
  416. * will be deselected.
  417. * @param {number} index 0-based index of the item to select.
  418. */
  419. goog.ui.Palette.prototype.setSelectedIndex = function(index) {
  420. if (this.selectionModel_) {
  421. this.selectionModel_.setSelectedIndex(index);
  422. }
  423. };
  424. /**
  425. * Selects the given item, or clears the selection if the argument is null or
  426. * invalid. Any previously-selected item will be deselected.
  427. * @param {Node} item Item to select.
  428. */
  429. goog.ui.Palette.prototype.setSelectedItem = function(item) {
  430. if (this.selectionModel_) {
  431. this.selectionModel_.setSelectedItem(item);
  432. }
  433. };
  434. /**
  435. * Private helper; highlights or un-highlights the item at the given index
  436. * based on the value of the Boolean argument. This implementation simply
  437. * applies highlight styling to the cell containing the item to be highighted.
  438. * Does nothing if the palette hasn't been rendered yet.
  439. * @param {number} index 0-based index of item to highlight or un-highlight.
  440. * @param {boolean} highlight If true, the item is highlighted; otherwise it
  441. * is un-highlighted.
  442. * @private
  443. */
  444. goog.ui.Palette.prototype.highlightIndex_ = function(index, highlight) {
  445. if (this.getElement()) {
  446. var items = this.getContent();
  447. if (items && index >= 0 && index < items.length) {
  448. var cellEl = this.getHighlightedCellElement_();
  449. if (this.currentCellControl_.getElement() != cellEl) {
  450. this.currentCellControl_.setElementInternal(cellEl);
  451. }
  452. if (this.currentCellControl_.tryHighlight(highlight)) {
  453. this.getRenderer().highlightCell(this, items[index], highlight);
  454. }
  455. }
  456. }
  457. };
  458. /** @override */
  459. goog.ui.Palette.prototype.setHighlighted = function(highlight) {
  460. if (highlight && this.highlightedIndex_ == -1) {
  461. // If there was a last highlighted index, use that. Otherwise, highlight the
  462. // first cell.
  463. this.setHighlightedIndex(
  464. this.lastHighlightedIndex_ > -1 ? this.lastHighlightedIndex_ : 0);
  465. } else if (!highlight) {
  466. this.setHighlightedIndex(-1);
  467. }
  468. // The highlight event should be fired once the component has updated its own
  469. // state.
  470. goog.ui.Palette.base(this, 'setHighlighted', highlight);
  471. };
  472. /**
  473. * Private helper; selects or deselects the given item based on the value of
  474. * the Boolean argument. This implementation simply applies selection styling
  475. * to the cell containing the item to be selected. Does nothing if the palette
  476. * hasn't been rendered yet.
  477. * @param {Node} item Item to select or deselect.
  478. * @param {boolean} select If true, the item is selected; otherwise it is
  479. * deselected.
  480. * @private
  481. */
  482. goog.ui.Palette.prototype.selectItem_ = function(item, select) {
  483. if (this.getElement()) {
  484. this.getRenderer().selectCell(this, item, select);
  485. }
  486. };
  487. /**
  488. * Calculates and updates the size of the palette based on any preset values
  489. * and the number of palette items. If there is no preset size, sets the
  490. * palette size to the smallest square big enough to contain all items. If
  491. * there is a preset number of columns, increases the number of rows to hold
  492. * all items if needed. (If there are too many rows, does nothing.)
  493. * @private
  494. */
  495. goog.ui.Palette.prototype.adjustSize_ = function() {
  496. var items = this.getContent();
  497. if (items) {
  498. if (this.size_ && this.size_.width) {
  499. // There is already a size set; honor the number of columns (if >0), but
  500. // increase the number of rows if needed.
  501. var minRows = Math.ceil(items.length / this.size_.width);
  502. if (!goog.isNumber(this.size_.height) || this.size_.height < minRows) {
  503. this.size_.height = minRows;
  504. }
  505. } else {
  506. // No size has been set; size the grid to the smallest square big enough
  507. // to hold all items (hey, why not?).
  508. var length = Math.ceil(Math.sqrt(items.length));
  509. this.size_ = new goog.math.Size(length, length);
  510. }
  511. } else {
  512. // No items; set size to 0x0.
  513. this.size_ = new goog.math.Size(0, 0);
  514. }
  515. };
  516. /**
  517. * A component to represent the currently highlighted cell.
  518. * @constructor
  519. * @extends {goog.ui.Control}
  520. * @private
  521. */
  522. goog.ui.Palette.CurrentCell_ = function() {
  523. goog.ui.Palette.CurrentCell_.base(this, 'constructor', null);
  524. this.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
  525. };
  526. goog.inherits(goog.ui.Palette.CurrentCell_, goog.ui.Control);
  527. /**
  528. * @param {boolean} highlight Whether to highlight or unhighlight the component.
  529. * @return {boolean} Whether it was successful.
  530. */
  531. goog.ui.Palette.CurrentCell_.prototype.tryHighlight = function(highlight) {
  532. this.setHighlighted(highlight);
  533. return this.isHighlighted() == highlight;
  534. };