Path: blob/trunk/third_party/closure/goog/ui/palette.js
4116 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview A palette control. A palette is a grid that the user can8* highlight or select via the keyboard or the mouse.9*10* @see ../demos/palette.html11*/1213goog.provide('goog.ui.Palette');1415goog.require('goog.asserts');16goog.require('goog.dom');17goog.require('goog.events');18goog.require('goog.events.EventType');19goog.require('goog.events.KeyCodes');20goog.require('goog.math.Size');21goog.require('goog.style');22goog.require('goog.ui.Component');23goog.require('goog.ui.Control');24goog.require('goog.ui.PaletteRenderer');25goog.require('goog.ui.SelectionModel');26goog.requireType('goog.events.BrowserEvent');27goog.requireType('goog.events.Event');28goog.requireType('goog.events.KeyEvent');29goog.requireType('goog.ui.ControlContent');30313233/**34* A palette is a grid of DOM nodes that the user can highlight or select via35* the keyboard or the mouse. The selection state of the palette is controlled36* an ACTION event. Event listeners may retrieve the selected item using the37* {@link #getSelectedItem} or {@link #getSelectedIndex} method.38*39* Use this class as the base for components like color palettes or emoticon40* pickers. Use {@link #setContent} to set/change the items in the palette41* after construction. See palette.html demo for example usage.42*43* @param {Array<Node>} items Array of DOM nodes to be displayed as items44* in the palette grid (limited to one per cell).45* @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or46* decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.47* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for48* document interaction.49* @constructor50* @extends {goog.ui.Control}51*/52goog.ui.Palette = function(items, opt_renderer, opt_domHelper) {53'use strict';54goog.ui.Palette.base(55this, 'constructor', items,56opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);57this.setAutoStates(58goog.ui.Component.State.CHECKED | goog.ui.Component.State.SELECTED |59goog.ui.Component.State.OPENED,60false);6162/**63* A fake component for dispatching events on palette cell changes.64* @type {!goog.ui.Palette.CurrentCell_}65* @private66*/67this.currentCellControl_ = new goog.ui.Palette.CurrentCell_();68this.currentCellControl_.setParentEventTarget(this);6970/**71* @private {number} The last highlighted index, or -1 if it never had one.72*/73this.lastHighlightedIndex_ = -1;74};75goog.inherits(goog.ui.Palette, goog.ui.Control);767778/**79* Events fired by the palette object80* @enum {string}81*/82goog.ui.Palette.EventType = {83AFTER_HIGHLIGHT: goog.events.getUniqueId('afterhighlight')84};858687/**88* Palette dimensions (columns x rows). If the number of rows is undefined,89* it is calculated on first use.90* @type {?goog.math.Size}91* @private92*/93goog.ui.Palette.prototype.size_ = null;949596/**97* Index of the currently highlighted item (-1 if none).98* @type {number}99* @private100*/101goog.ui.Palette.prototype.highlightedIndex_ = -1;102103104/**105* Selection model controlling the palette's selection state.106* @type {?goog.ui.SelectionModel}107* @private108*/109goog.ui.Palette.prototype.selectionModel_ = null;110111112// goog.ui.Component / goog.ui.Control implementation.113114115/** @override */116goog.ui.Palette.prototype.disposeInternal = function() {117'use strict';118goog.ui.Palette.superClass_.disposeInternal.call(this);119120if (this.selectionModel_) {121this.selectionModel_.dispose();122this.selectionModel_ = null;123}124125this.size_ = null;126127this.currentCellControl_.dispose();128};129130131/**132* Overrides {@link goog.ui.Control#setContentInternal} by also updating the133* grid size and the selection model. Considered protected.134* @param {goog.ui.ControlContent} content Array of DOM nodes to be displayed135* as items in the palette grid (one item per cell).136* @protected137* @override138*/139goog.ui.Palette.prototype.setContentInternal = function(content) {140'use strict';141var items = /** @type {Array<Node>} */ (content);142goog.ui.Palette.superClass_.setContentInternal.call(this, items);143144// Adjust the palette size.145this.adjustSize_();146147// Add the items to the selection model, replacing previous items (if any).148if (this.selectionModel_) {149// We already have a selection model; just replace the items.150this.selectionModel_.clear();151this.selectionModel_.addItems(items);152} else {153// Create a selection model, initialize the items, and hook up handlers.154this.selectionModel_ = new goog.ui.SelectionModel(items);155this.selectionModel_.setSelectionHandler(goog.bind(this.selectItem_, this));156this.getHandler().listen(157this.selectionModel_, goog.events.EventType.SELECT,158this.handleSelectionChange);159}160161// In all cases, clear the highlight.162this.highlightedIndex_ = -1;163};164165166/**167* Overrides {@link goog.ui.Control#getCaption} to return the empty string,168* since palettes don't have text captions.169* @return {string} The empty string.170* @override171*/172goog.ui.Palette.prototype.getCaption = function() {173'use strict';174return '';175};176177178/**179* Overrides {@link goog.ui.Control#setCaption} to be a no-op, since palettes180* don't have text captions.181* @param {string} caption Ignored.182* @override183*/184goog.ui.Palette.prototype.setCaption = function(caption) {185// Do nothing.186};187188189// Palette event handling.190191192/**193* Handles mouseover events. Overrides {@link goog.ui.Control#handleMouseOver}194* by determining which palette item (if any) was moused over, highlighting it,195* and un-highlighting any previously-highlighted item.196* @param {goog.events.BrowserEvent} e Mouse event to handle.197* @override198*/199goog.ui.Palette.prototype.handleMouseOver = function(e) {200'use strict';201goog.ui.Palette.superClass_.handleMouseOver.call(this, e);202203/** @suppress {strictMissingProperties} Added to tighten compiler checks */204var item = this.getRenderer().getContainingItem(this, e.target);205if (item && e.relatedTarget && goog.dom.contains(item, e.relatedTarget)) {206// Ignore internal mouse moves.207return;208}209210if (item != this.getHighlightedItem()) {211this.setHighlightedItem(item);212}213};214215216/**217* Handles mousedown events. Overrides {@link goog.ui.Control#handleMouseDown}218* by ensuring that the item on which the user moused down is highlighted.219* @param {goog.events.Event} e Mouse event to handle.220* @override221*/222goog.ui.Palette.prototype.handleMouseDown = function(e) {223'use strict';224goog.ui.Palette.superClass_.handleMouseDown.call(this, e);225226if (this.isActive()) {227// Make sure we move the highlight to the cell on which the user moused228// down.229/** @suppress {strictMissingProperties} Added to tighten compiler checks */230var item = this.getRenderer().getContainingItem(this, e.target);231if (item != this.getHighlightedItem()) {232this.setHighlightedItem(item);233}234}235};236237238/**239* Selects the currently highlighted palette item (triggered by mouseup or by240* keyboard action). Overrides {@link goog.ui.Control#performActionInternal}241* by selecting the highlighted item and dispatching an ACTION event.242* @param {goog.events.Event} e Mouse or key event that triggered the action.243* @return {boolean} True if the action was allowed to proceed, false otherwise.244* @override245*/246goog.ui.Palette.prototype.performActionInternal = function(e) {247'use strict';248var highlightedItem = this.getHighlightedItem();249if (highlightedItem) {250if (e && this.shouldSelectHighlightedItem_(e)) {251this.setSelectedItem(highlightedItem);252}253return goog.ui.Palette.base(this, 'performActionInternal', e);254}255return false;256};257258259/**260* Determines whether to select the highlighted item while handling an internal261* action. The highlighted item should not be selected if the action is a mouse262* event occurring outside the palette or in an "empty" cell.263* @param {!goog.events.Event} e Mouseup or key event being handled.264* @return {boolean} True if the highlighted item should be selected.265* @private266* @suppress {strictMissingProperties} Added to tighten compiler checks267*/268goog.ui.Palette.prototype.shouldSelectHighlightedItem_ = function(e) {269'use strict';270if (!this.getSelectedItem()) {271// It's always ok to select when nothing is selected yet.272return true;273} else if (e.type != 'mouseup') {274// Keyboard can only act on valid cells.275return true;276} else {277// Return whether or not the mouse action was in the palette.278return !!this.getRenderer().getContainingItem(this, e.target);279}280};281282283/**284* Handles keyboard events dispatched while the palette has focus. Moves the285* highlight on arrow keys, and selects the highlighted item on Enter or Space.286* Returns true if the event was handled, false otherwise. In particular, if287* the user attempts to navigate out of the grid, the highlight isn't changed,288* and this method returns false; it is then up to the parent component to289* handle the event (e.g. by wrapping the highlight around). Overrides {@link290* goog.ui.Control#handleKeyEvent}.291* @param {goog.events.KeyEvent} e Key event to handle.292* @return {boolean} True iff the key event was handled by the component.293* @override294*/295goog.ui.Palette.prototype.handleKeyEvent = function(e) {296'use strict';297var items = this.getContent();298/** @suppress {strictMissingProperties} Added to tighten compiler checks */299var numItems = items ? items.length : 0;300var numColumns = this.size_.width;301302// If the component is disabled or the palette is empty, bail.303if (numItems == 0 || !this.isEnabled()) {304return false;305}306307// User hit ENTER or SPACE; trigger action.308if (e.keyCode == goog.events.KeyCodes.ENTER ||309e.keyCode == goog.events.KeyCodes.SPACE) {310return this.performActionInternal(e);311}312313// User hit HOME or END; move highlight.314if (e.keyCode == goog.events.KeyCodes.HOME) {315this.setHighlightedIndexInternal_(0, true /* scrollIntoView */);316return true;317} else if (e.keyCode == goog.events.KeyCodes.END) {318this.setHighlightedIndexInternal_(numItems - 1, true /* scrollIntoView */);319return true;320}321322// If nothing is highlighted, start from the selected index. If nothing is323// selected either, highlightedIndex is -1.324var highlightedIndex = this.highlightedIndex_ < 0 ? this.getSelectedIndex() :325this.highlightedIndex_;326327switch (e.keyCode) {328case goog.events.KeyCodes.LEFT:329// If the highlighted index is uninitialized, or is at the beginning, move330// it to the end.331if (highlightedIndex == -1 || highlightedIndex == 0) {332highlightedIndex = numItems;333}334this.setHighlightedIndexInternal_(335highlightedIndex - 1, true /* scrollIntoView */);336e.preventDefault();337return true;338break;339340case goog.events.KeyCodes.RIGHT:341// If the highlighted index at the end, move it to the beginning.342if (highlightedIndex == numItems - 1) {343highlightedIndex = -1;344}345this.setHighlightedIndexInternal_(346highlightedIndex + 1, true /* scrollIntoView */);347e.preventDefault();348return true;349break;350351case goog.events.KeyCodes.UP:352if (highlightedIndex == -1) {353highlightedIndex = numItems + numColumns - 1;354}355if (highlightedIndex >= numColumns) {356this.setHighlightedIndexInternal_(357highlightedIndex - numColumns, true /* scrollIntoView */);358e.preventDefault();359return true;360}361break;362363case goog.events.KeyCodes.DOWN:364if (highlightedIndex == -1) {365highlightedIndex = -numColumns;366}367if (highlightedIndex < numItems - numColumns) {368this.setHighlightedIndexInternal_(369highlightedIndex + numColumns, true /* scrollIntoView */);370e.preventDefault();371return true;372}373break;374}375376return false;377};378379380/**381* Handles selection change events dispatched by the selection model.382* @param {goog.events.Event} e Selection event to handle.383*/384goog.ui.Palette.prototype.handleSelectionChange = function(e) {385// No-op in the base class.386};387388389// Palette management.390391392/**393* Returns the size of the palette grid.394* @return {goog.math.Size} Palette size (columns x rows).395*/396goog.ui.Palette.prototype.getSize = function() {397'use strict';398return this.size_;399};400401402/**403* Sets the size of the palette grid to the given size. Callers can either404* pass a single {@link goog.math.Size} or a pair of numbers (first the number405* of columns, then the number of rows) to this method. In both cases, the406* number of rows is optional and will be calculated automatically if needed.407* It is an error to attempt to change the size of the palette after it has408* been rendered.409* @param {goog.math.Size|number} size Either a size object or the number of410* columns.411* @param {number=} opt_rows The number of rows (optional).412*/413goog.ui.Palette.prototype.setSize = function(size, opt_rows) {414'use strict';415if (this.getElement()) {416throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);417}418419this.size_ = (typeof size === 'number') ?420new goog.math.Size(size, /** @type {number} */ (opt_rows)) :421size;422423// Adjust size, if needed.424this.adjustSize_();425};426427428/**429* Returns the 0-based index of the currently highlighted palette item, or -1430* if no item is highlighted.431* @return {number} Index of the highlighted item (-1 if none).432*/433goog.ui.Palette.prototype.getHighlightedIndex = function() {434'use strict';435return this.highlightedIndex_;436};437438439/**440* Returns the currently highlighted palette item, or null if no item is441* highlighted.442* @return {Node} The highlighted item (undefined if none).443*/444goog.ui.Palette.prototype.getHighlightedItem = function() {445'use strict';446var items = this.getContent();447return items && items[this.highlightedIndex_];448};449450451/**452* @return {Element} The highlighted cell.453* @private454* @suppress {strictMissingProperties} Added to tighten compiler checks455*/456goog.ui.Palette.prototype.getHighlightedCellElement_ = function() {457'use strict';458return this.getRenderer().getCellForItem(this.getHighlightedItem());459};460461462/**463* Highlights the item at the given 0-based index, or removes the highlight464* if the argument is -1 or out of range. Any previously-highlighted item465* will be un-highlighted.466* @param {number} index 0-based index of the item to highlight.467*/468goog.ui.Palette.prototype.setHighlightedIndex = function(index) {469'use strict';470this.setHighlightedIndexInternal_(index, false /* scrollIntoView */);471};472473474/**475* @param {number} index 0-based index of the item to highlight.476* @param {boolean} scrollIntoView Whether to bring the highlighted item into477* view by potentially scrolling the palette's container. This has no effect478* if the palette is not in a scrollbale container.479* @private480*/481goog.ui.Palette.prototype.setHighlightedIndexInternal_ = function(482index, scrollIntoView) {483'use strict';484if (index != this.highlightedIndex_) {485this.highlightIndex_(this.highlightedIndex_, false);486this.lastHighlightedIndex_ = this.highlightedIndex_;487this.highlightedIndex_ = index;488this.highlightIndex_(index, true);489if (scrollIntoView && this.getParent()) {490var highlightedElement = goog.asserts.assert(491this.getHighlightedCellElement_(),492'Highlighted item must exist to scroll to make it visible in ' +493'container. Please check that index is non-negative and valid.');494goog.style.scrollIntoContainerView(495highlightedElement, this.getParent().getElementStrict());496}497this.dispatchEvent(goog.ui.Palette.EventType.AFTER_HIGHLIGHT);498}499};500501502/**503* Highlights the given item, or removes the highlight if the argument is null504* or invalid. Any previously-highlighted item will be un-highlighted.505* @param {Node|undefined} item Item to highlight.506*/507goog.ui.Palette.prototype.setHighlightedItem = function(item) {508'use strict';509var items = /** @type {Array<Node>} */ (this.getContent());510this.setHighlightedIndex((items && item) ? items.indexOf(item) : -1);511};512513514/**515* Returns the 0-based index of the currently selected palette item, or -1516* if no item is selected.517* @return {number} Index of the selected item (-1 if none).518*/519goog.ui.Palette.prototype.getSelectedIndex = function() {520'use strict';521return this.selectionModel_ ? this.selectionModel_.getSelectedIndex() : -1;522};523524525/**526* Returns the currently selected palette item, or null if no item is selected.527* @return {Node} The selected item (null if none).528*/529goog.ui.Palette.prototype.getSelectedItem = function() {530'use strict';531return this.selectionModel_ ?532/** @type {Node} */ (this.selectionModel_.getSelectedItem()) :533null;534};535536537/**538* Selects the item at the given 0-based index, or clears the selection539* if the argument is -1 or out of range. Any previously-selected item540* will be deselected.541* @param {number} index 0-based index of the item to select.542*/543goog.ui.Palette.prototype.setSelectedIndex = function(index) {544'use strict';545if (this.selectionModel_) {546this.selectionModel_.setSelectedIndex(index);547}548};549550551/**552* Selects the given item, or clears the selection if the argument is null or553* invalid. Any previously-selected item will be deselected.554* @param {Node} item Item to select.555*/556goog.ui.Palette.prototype.setSelectedItem = function(item) {557'use strict';558if (this.selectionModel_) {559this.selectionModel_.setSelectedItem(item);560}561};562563564/**565* Private helper; highlights or un-highlights the item at the given index566* based on the value of the Boolean argument. This implementation simply567* applies highlight styling to the cell containing the item to be highighted.568* Does nothing if the palette hasn't been rendered yet.569* @param {number} index 0-based index of item to highlight or un-highlight.570* @param {boolean} highlight If true, the item is highlighted; otherwise it571* is un-highlighted.572* @private573* @suppress {strictMissingProperties} Added to tighten compiler checks574*/575goog.ui.Palette.prototype.highlightIndex_ = function(index, highlight) {576'use strict';577if (this.getElement()) {578var items = this.getContent();579if (items && index >= 0 && index < items.length) {580var cellEl = this.getHighlightedCellElement_();581if (this.currentCellControl_.getElement() != cellEl) {582this.currentCellControl_.setElementInternal(cellEl);583}584if (this.currentCellControl_.tryHighlight(highlight)) {585this.getRenderer().highlightCell(this, items[index], highlight);586}587}588}589};590591592/** @override */593goog.ui.Palette.prototype.setHighlighted = function(highlight) {594'use strict';595if (highlight && this.highlightedIndex_ == -1) {596// If there was a last highlighted index, use that. Otherwise, highlight the597// first cell.598this.setHighlightedIndex(599this.lastHighlightedIndex_ > -1 ? this.lastHighlightedIndex_ : 0);600} else if (!highlight) {601this.setHighlightedIndex(-1);602}603// The highlight event should be fired once the component has updated its own604// state.605goog.ui.Palette.base(this, 'setHighlighted', highlight);606};607608609/**610* Private helper; selects or deselects the given item based on the value of611* the Boolean argument. This implementation simply applies selection styling612* to the cell containing the item to be selected. Does nothing if the palette613* hasn't been rendered yet.614* @param {Node} item Item to select or deselect.615* @param {boolean} select If true, the item is selected; otherwise it is616* deselected.617* @private618* @suppress {strictMissingProperties} Added to tighten compiler checks619*/620goog.ui.Palette.prototype.selectItem_ = function(item, select) {621'use strict';622if (this.getElement()) {623this.getRenderer().selectCell(this, item, select);624}625};626627628/**629* Calculates and updates the size of the palette based on any preset values630* and the number of palette items. If there is no preset size, sets the631* palette size to the smallest square big enough to contain all items. If632* there is a preset number of columns, increases the number of rows to hold633* all items if needed. (If there are too many rows, does nothing.)634* @private635*/636goog.ui.Palette.prototype.adjustSize_ = function() {637'use strict';638var items = this.getContent();639if (items) {640if (this.size_ && this.size_.width) {641// There is already a size set; honor the number of columns (if >0), but642// increase the number of rows if needed.643/**644* @suppress {strictMissingProperties} Added to tighten compiler checks645*/646var minRows = Math.ceil(items.length / this.size_.width);647if (typeof this.size_.height !== 'number' ||648this.size_.height < minRows) {649this.size_.height = minRows;650}651} else {652// No size has been set; size the grid to the smallest square big enough653// to hold all items (hey, why not?).654/**655* @suppress {strictMissingProperties} Added to tighten compiler checks656*/657var length = Math.ceil(Math.sqrt(items.length));658this.size_ = new goog.math.Size(length, length);659}660} else {661// No items; set size to 0x0.662this.size_ = new goog.math.Size(0, 0);663}664};665666667668/**669* A component to represent the currently highlighted cell.670* @constructor671* @extends {goog.ui.Control}672* @private673*/674goog.ui.Palette.CurrentCell_ = function() {675'use strict';676goog.ui.Palette.CurrentCell_.base(this, 'constructor', null);677this.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);678};679goog.inherits(goog.ui.Palette.CurrentCell_, goog.ui.Control);680681682/**683* @param {boolean} highlight Whether to highlight or unhighlight the component.684* @return {boolean} Whether it was successful.685*/686goog.ui.Palette.CurrentCell_.prototype.tryHighlight = function(highlight) {687'use strict';688this.setHighlighted(highlight);689return this.isHighlighted() == highlight;690};691692693