123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- // Copyright 2007 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 A palette control. A palette is a grid that the user can
- * highlight or select via the keyboard or the mouse.
- *
- * @author attila@google.com (Attila Bodis)
- * @see ../demos/palette.html
- */
- goog.provide('goog.ui.Palette');
- goog.require('goog.array');
- goog.require('goog.dom');
- goog.require('goog.events');
- goog.require('goog.events.EventType');
- goog.require('goog.events.KeyCodes');
- goog.require('goog.math.Size');
- goog.require('goog.ui.Component');
- goog.require('goog.ui.Control');
- goog.require('goog.ui.PaletteRenderer');
- goog.require('goog.ui.SelectionModel');
- /**
- * A palette is a grid of DOM nodes that the user can highlight or select via
- * the keyboard or the mouse. The selection state of the palette is controlled
- * an ACTION event. Event listeners may retrieve the selected item using the
- * {@link #getSelectedItem} or {@link #getSelectedIndex} method.
- *
- * Use this class as the base for components like color palettes or emoticon
- * pickers. Use {@link #setContent} to set/change the items in the palette
- * after construction. See palette.html demo for example usage.
- *
- * @param {Array<Node>} items Array of DOM nodes to be displayed as items
- * in the palette grid (limited to one per cell).
- * @param {goog.ui.PaletteRenderer=} opt_renderer Renderer used to render or
- * decorate the palette; defaults to {@link goog.ui.PaletteRenderer}.
- * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
- * document interaction.
- * @constructor
- * @extends {goog.ui.Control}
- */
- goog.ui.Palette = function(items, opt_renderer, opt_domHelper) {
- goog.ui.Palette.base(
- this, 'constructor', items,
- opt_renderer || goog.ui.PaletteRenderer.getInstance(), opt_domHelper);
- this.setAutoStates(
- goog.ui.Component.State.CHECKED | goog.ui.Component.State.SELECTED |
- goog.ui.Component.State.OPENED,
- false);
- /**
- * A fake component for dispatching events on palette cell changes.
- * @type {!goog.ui.Palette.CurrentCell_}
- * @private
- */
- this.currentCellControl_ = new goog.ui.Palette.CurrentCell_();
- this.currentCellControl_.setParentEventTarget(this);
- /**
- * @private {number} The last highlighted index, or -1 if it never had one.
- */
- this.lastHighlightedIndex_ = -1;
- };
- goog.inherits(goog.ui.Palette, goog.ui.Control);
- goog.tagUnsealableClass(goog.ui.Palette);
- /**
- * Events fired by the palette object
- * @enum {string}
- */
- goog.ui.Palette.EventType = {
- AFTER_HIGHLIGHT: goog.events.getUniqueId('afterhighlight')
- };
- /**
- * Palette dimensions (columns x rows). If the number of rows is undefined,
- * it is calculated on first use.
- * @type {goog.math.Size}
- * @private
- */
- goog.ui.Palette.prototype.size_ = null;
- /**
- * Index of the currently highlighted item (-1 if none).
- * @type {number}
- * @private
- */
- goog.ui.Palette.prototype.highlightedIndex_ = -1;
- /**
- * Selection model controlling the palette's selection state.
- * @type {goog.ui.SelectionModel}
- * @private
- */
- goog.ui.Palette.prototype.selectionModel_ = null;
- // goog.ui.Component / goog.ui.Control implementation.
- /** @override */
- goog.ui.Palette.prototype.disposeInternal = function() {
- goog.ui.Palette.superClass_.disposeInternal.call(this);
- if (this.selectionModel_) {
- this.selectionModel_.dispose();
- this.selectionModel_ = null;
- }
- this.size_ = null;
- this.currentCellControl_.dispose();
- };
- /**
- * Overrides {@link goog.ui.Control#setContentInternal} by also updating the
- * grid size and the selection model. Considered protected.
- * @param {goog.ui.ControlContent} content Array of DOM nodes to be displayed
- * as items in the palette grid (one item per cell).
- * @protected
- * @override
- */
- goog.ui.Palette.prototype.setContentInternal = function(content) {
- var items = /** @type {Array<Node>} */ (content);
- goog.ui.Palette.superClass_.setContentInternal.call(this, items);
- // Adjust the palette size.
- this.adjustSize_();
- // Add the items to the selection model, replacing previous items (if any).
- if (this.selectionModel_) {
- // We already have a selection model; just replace the items.
- this.selectionModel_.clear();
- this.selectionModel_.addItems(items);
- } else {
- // Create a selection model, initialize the items, and hook up handlers.
- this.selectionModel_ = new goog.ui.SelectionModel(items);
- this.selectionModel_.setSelectionHandler(goog.bind(this.selectItem_, this));
- this.getHandler().listen(
- this.selectionModel_, goog.events.EventType.SELECT,
- this.handleSelectionChange);
- }
- // In all cases, clear the highlight.
- this.highlightedIndex_ = -1;
- };
- /**
- * Overrides {@link goog.ui.Control#getCaption} to return the empty string,
- * since palettes don't have text captions.
- * @return {string} The empty string.
- * @override
- */
- goog.ui.Palette.prototype.getCaption = function() {
- return '';
- };
- /**
- * Overrides {@link goog.ui.Control#setCaption} to be a no-op, since palettes
- * don't have text captions.
- * @param {string} caption Ignored.
- * @override
- */
- goog.ui.Palette.prototype.setCaption = function(caption) {
- // Do nothing.
- };
- // Palette event handling.
- /**
- * Handles mouseover events. Overrides {@link goog.ui.Control#handleMouseOver}
- * by determining which palette item (if any) was moused over, highlighting it,
- * and un-highlighting any previously-highlighted item.
- * @param {goog.events.BrowserEvent} e Mouse event to handle.
- * @override
- */
- goog.ui.Palette.prototype.handleMouseOver = function(e) {
- goog.ui.Palette.superClass_.handleMouseOver.call(this, e);
- var item = this.getRenderer().getContainingItem(this, e.target);
- if (item && e.relatedTarget && goog.dom.contains(item, e.relatedTarget)) {
- // Ignore internal mouse moves.
- return;
- }
- if (item != this.getHighlightedItem()) {
- this.setHighlightedItem(item);
- }
- };
- /**
- * Handles mousedown events. Overrides {@link goog.ui.Control#handleMouseDown}
- * by ensuring that the item on which the user moused down is highlighted.
- * @param {goog.events.Event} e Mouse event to handle.
- * @override
- */
- goog.ui.Palette.prototype.handleMouseDown = function(e) {
- goog.ui.Palette.superClass_.handleMouseDown.call(this, e);
- if (this.isActive()) {
- // Make sure we move the highlight to the cell on which the user moused
- // down.
- var item = this.getRenderer().getContainingItem(this, e.target);
- if (item != this.getHighlightedItem()) {
- this.setHighlightedItem(item);
- }
- }
- };
- /**
- * Selects the currently highlighted palette item (triggered by mouseup or by
- * keyboard action). Overrides {@link goog.ui.Control#performActionInternal}
- * by selecting the highlighted item and dispatching an ACTION event.
- * @param {goog.events.Event} e Mouse or key event that triggered the action.
- * @return {boolean} True if the action was allowed to proceed, false otherwise.
- * @override
- */
- goog.ui.Palette.prototype.performActionInternal = function(e) {
- var highlightedItem = this.getHighlightedItem();
- if (highlightedItem) {
- if (e && this.shouldSelectHighlightedItem_(e)) {
- this.setSelectedItem(highlightedItem);
- }
- return goog.ui.Palette.base(this, 'performActionInternal', e);
- }
- return false;
- };
- /**
- * Determines whether to select the highlighted item while handling an internal
- * action. The highlighted item should not be selected if the action is a mouse
- * event occurring outside the palette or in an "empty" cell.
- * @param {!goog.events.Event} e Mouseup or key event being handled.
- * @return {boolean} True if the highlighted item should be selected.
- * @private
- */
- goog.ui.Palette.prototype.shouldSelectHighlightedItem_ = function(e) {
- if (!this.getSelectedItem()) {
- // It's always ok to select when nothing is selected yet.
- return true;
- } else if (e.type != 'mouseup') {
- // Keyboard can only act on valid cells.
- return true;
- } else {
- // Return whether or not the mouse action was in the palette.
- return !!this.getRenderer().getContainingItem(this, e.target);
- }
- };
- /**
- * Handles keyboard events dispatched while the palette has focus. Moves the
- * highlight on arrow keys, and selects the highlighted item on Enter or Space.
- * Returns true if the event was handled, false otherwise. In particular, if
- * the user attempts to navigate out of the grid, the highlight isn't changed,
- * and this method returns false; it is then up to the parent component to
- * handle the event (e.g. by wrapping the highlight around). Overrides {@link
- * goog.ui.Control#handleKeyEvent}.
- * @param {goog.events.KeyEvent} e Key event to handle.
- * @return {boolean} True iff the key event was handled by the component.
- * @override
- */
- goog.ui.Palette.prototype.handleKeyEvent = function(e) {
- var items = this.getContent();
- var numItems = items ? items.length : 0;
- var numColumns = this.size_.width;
- // If the component is disabled or the palette is empty, bail.
- if (numItems == 0 || !this.isEnabled()) {
- return false;
- }
- // User hit ENTER or SPACE; trigger action.
- if (e.keyCode == goog.events.KeyCodes.ENTER ||
- e.keyCode == goog.events.KeyCodes.SPACE) {
- return this.performActionInternal(e);
- }
- // User hit HOME or END; move highlight.
- if (e.keyCode == goog.events.KeyCodes.HOME) {
- this.setHighlightedIndex(0);
- return true;
- } else if (e.keyCode == goog.events.KeyCodes.END) {
- this.setHighlightedIndex(numItems - 1);
- return true;
- }
- // If nothing is highlighted, start from the selected index. If nothing is
- // selected either, highlightedIndex is -1.
- var highlightedIndex = this.highlightedIndex_ < 0 ? this.getSelectedIndex() :
- this.highlightedIndex_;
- switch (e.keyCode) {
- case goog.events.KeyCodes.LEFT:
- // If the highlighted index is uninitialized, or is at the beginning, move
- // it to the end.
- if (highlightedIndex == -1 || highlightedIndex == 0) {
- highlightedIndex = numItems;
- }
- this.setHighlightedIndex(highlightedIndex - 1);
- e.preventDefault();
- return true;
- break;
- case goog.events.KeyCodes.RIGHT:
- // If the highlighted index at the end, move it to the beginning.
- if (highlightedIndex == numItems - 1) {
- highlightedIndex = -1;
- }
- this.setHighlightedIndex(highlightedIndex + 1);
- e.preventDefault();
- return true;
- break;
- case goog.events.KeyCodes.UP:
- if (highlightedIndex == -1) {
- highlightedIndex = numItems + numColumns - 1;
- }
- if (highlightedIndex >= numColumns) {
- this.setHighlightedIndex(highlightedIndex - numColumns);
- e.preventDefault();
- return true;
- }
- break;
- case goog.events.KeyCodes.DOWN:
- if (highlightedIndex == -1) {
- highlightedIndex = -numColumns;
- }
- if (highlightedIndex < numItems - numColumns) {
- this.setHighlightedIndex(highlightedIndex + numColumns);
- e.preventDefault();
- return true;
- }
- break;
- }
- return false;
- };
- /**
- * Handles selection change events dispatched by the selection model.
- * @param {goog.events.Event} e Selection event to handle.
- */
- goog.ui.Palette.prototype.handleSelectionChange = function(e) {
- // No-op in the base class.
- };
- // Palette management.
- /**
- * Returns the size of the palette grid.
- * @return {goog.math.Size} Palette size (columns x rows).
- */
- goog.ui.Palette.prototype.getSize = function() {
- return this.size_;
- };
- /**
- * Sets the size of the palette grid to the given size. Callers can either
- * pass a single {@link goog.math.Size} or a pair of numbers (first the number
- * of columns, then the number of rows) to this method. In both cases, the
- * number of rows is optional and will be calculated automatically if needed.
- * It is an error to attempt to change the size of the palette after it has
- * been rendered.
- * @param {goog.math.Size|number} size Either a size object or the number of
- * columns.
- * @param {number=} opt_rows The number of rows (optional).
- */
- goog.ui.Palette.prototype.setSize = function(size, opt_rows) {
- if (this.getElement()) {
- throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
- }
- this.size_ = goog.isNumber(size) ?
- new goog.math.Size(size, /** @type {number} */ (opt_rows)) :
- size;
- // Adjust size, if needed.
- this.adjustSize_();
- };
- /**
- * Returns the 0-based index of the currently highlighted palette item, or -1
- * if no item is highlighted.
- * @return {number} Index of the highlighted item (-1 if none).
- */
- goog.ui.Palette.prototype.getHighlightedIndex = function() {
- return this.highlightedIndex_;
- };
- /**
- * Returns the currently highlighted palette item, or null if no item is
- * highlighted.
- * @return {Node} The highlighted item (undefined if none).
- */
- goog.ui.Palette.prototype.getHighlightedItem = function() {
- var items = this.getContent();
- return items && items[this.highlightedIndex_];
- };
- /**
- * @return {Element} The highlighted cell.
- * @private
- */
- goog.ui.Palette.prototype.getHighlightedCellElement_ = function() {
- return this.getRenderer().getCellForItem(this.getHighlightedItem());
- };
- /**
- * Highlights the item at the given 0-based index, or removes the highlight
- * if the argument is -1 or out of range. Any previously-highlighted item
- * will be un-highlighted.
- * @param {number} index 0-based index of the item to highlight.
- */
- goog.ui.Palette.prototype.setHighlightedIndex = function(index) {
- if (index != this.highlightedIndex_) {
- this.highlightIndex_(this.highlightedIndex_, false);
- this.lastHighlightedIndex_ = this.highlightedIndex_;
- this.highlightedIndex_ = index;
- this.highlightIndex_(index, true);
- this.dispatchEvent(goog.ui.Palette.EventType.AFTER_HIGHLIGHT);
- }
- };
- /**
- * Highlights the given item, or removes the highlight if the argument is null
- * or invalid. Any previously-highlighted item will be un-highlighted.
- * @param {Node|undefined} item Item to highlight.
- */
- goog.ui.Palette.prototype.setHighlightedItem = function(item) {
- var items = /** @type {Array<Node>} */ (this.getContent());
- this.setHighlightedIndex(
- (items && item) ? goog.array.indexOf(items, item) : -1);
- };
- /**
- * Returns the 0-based index of the currently selected palette item, or -1
- * if no item is selected.
- * @return {number} Index of the selected item (-1 if none).
- */
- goog.ui.Palette.prototype.getSelectedIndex = function() {
- return this.selectionModel_ ? this.selectionModel_.getSelectedIndex() : -1;
- };
- /**
- * Returns the currently selected palette item, or null if no item is selected.
- * @return {Node} The selected item (null if none).
- */
- goog.ui.Palette.prototype.getSelectedItem = function() {
- return this.selectionModel_ ?
- /** @type {Node} */ (this.selectionModel_.getSelectedItem()) :
- null;
- };
- /**
- * Selects the item at the given 0-based index, or clears the selection
- * if the argument is -1 or out of range. Any previously-selected item
- * will be deselected.
- * @param {number} index 0-based index of the item to select.
- */
- goog.ui.Palette.prototype.setSelectedIndex = function(index) {
- if (this.selectionModel_) {
- this.selectionModel_.setSelectedIndex(index);
- }
- };
- /**
- * Selects the given item, or clears the selection if the argument is null or
- * invalid. Any previously-selected item will be deselected.
- * @param {Node} item Item to select.
- */
- goog.ui.Palette.prototype.setSelectedItem = function(item) {
- if (this.selectionModel_) {
- this.selectionModel_.setSelectedItem(item);
- }
- };
- /**
- * Private helper; highlights or un-highlights the item at the given index
- * based on the value of the Boolean argument. This implementation simply
- * applies highlight styling to the cell containing the item to be highighted.
- * Does nothing if the palette hasn't been rendered yet.
- * @param {number} index 0-based index of item to highlight or un-highlight.
- * @param {boolean} highlight If true, the item is highlighted; otherwise it
- * is un-highlighted.
- * @private
- */
- goog.ui.Palette.prototype.highlightIndex_ = function(index, highlight) {
- if (this.getElement()) {
- var items = this.getContent();
- if (items && index >= 0 && index < items.length) {
- var cellEl = this.getHighlightedCellElement_();
- if (this.currentCellControl_.getElement() != cellEl) {
- this.currentCellControl_.setElementInternal(cellEl);
- }
- if (this.currentCellControl_.tryHighlight(highlight)) {
- this.getRenderer().highlightCell(this, items[index], highlight);
- }
- }
- }
- };
- /** @override */
- goog.ui.Palette.prototype.setHighlighted = function(highlight) {
- if (highlight && this.highlightedIndex_ == -1) {
- // If there was a last highlighted index, use that. Otherwise, highlight the
- // first cell.
- this.setHighlightedIndex(
- this.lastHighlightedIndex_ > -1 ? this.lastHighlightedIndex_ : 0);
- } else if (!highlight) {
- this.setHighlightedIndex(-1);
- }
- // The highlight event should be fired once the component has updated its own
- // state.
- goog.ui.Palette.base(this, 'setHighlighted', highlight);
- };
- /**
- * Private helper; selects or deselects the given item based on the value of
- * the Boolean argument. This implementation simply applies selection styling
- * to the cell containing the item to be selected. Does nothing if the palette
- * hasn't been rendered yet.
- * @param {Node} item Item to select or deselect.
- * @param {boolean} select If true, the item is selected; otherwise it is
- * deselected.
- * @private
- */
- goog.ui.Palette.prototype.selectItem_ = function(item, select) {
- if (this.getElement()) {
- this.getRenderer().selectCell(this, item, select);
- }
- };
- /**
- * Calculates and updates the size of the palette based on any preset values
- * and the number of palette items. If there is no preset size, sets the
- * palette size to the smallest square big enough to contain all items. If
- * there is a preset number of columns, increases the number of rows to hold
- * all items if needed. (If there are too many rows, does nothing.)
- * @private
- */
- goog.ui.Palette.prototype.adjustSize_ = function() {
- var items = this.getContent();
- if (items) {
- if (this.size_ && this.size_.width) {
- // There is already a size set; honor the number of columns (if >0), but
- // increase the number of rows if needed.
- var minRows = Math.ceil(items.length / this.size_.width);
- if (!goog.isNumber(this.size_.height) || this.size_.height < minRows) {
- this.size_.height = minRows;
- }
- } else {
- // No size has been set; size the grid to the smallest square big enough
- // to hold all items (hey, why not?).
- var length = Math.ceil(Math.sqrt(items.length));
- this.size_ = new goog.math.Size(length, length);
- }
- } else {
- // No items; set size to 0x0.
- this.size_ = new goog.math.Size(0, 0);
- }
- };
- /**
- * A component to represent the currently highlighted cell.
- * @constructor
- * @extends {goog.ui.Control}
- * @private
- */
- goog.ui.Palette.CurrentCell_ = function() {
- goog.ui.Palette.CurrentCell_.base(this, 'constructor', null);
- this.setDispatchTransitionEvents(goog.ui.Component.State.HOVER, true);
- };
- goog.inherits(goog.ui.Palette.CurrentCell_, goog.ui.Control);
- /**
- * @param {boolean} highlight Whether to highlight or unhighlight the component.
- * @return {boolean} Whether it was successful.
- */
- goog.ui.Palette.CurrentCell_.prototype.tryHighlight = function(highlight) {
- this.setHighlighted(highlight);
- return this.isHighlighted() == highlight;
- };
|