blob: 94591faaf41522d51a42a2221a50d754da4c0c74 [file] [log] [blame]
// Copyright 2006 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 base ratings widget that allows the user to select a rating,
* like "star video" in Google Video. This fires a "change" event when the user
* selects a rating.
*
* Keyboard:
* ESC = Clear (if supported)
* Home = 1 star
* End = Full rating
* Left arrow = Decrease rating
* Right arrow = Increase rating
* 0 = Clear (if supported)
* 1 - 9 = nth star
*
* @see ../demos/ratings.html
*/
goog.provide('goog.ui.Ratings');
goog.provide('goog.ui.Ratings.EventType');
goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.asserts');
goog.require('goog.dom.classlist');
goog.require('goog.events.EventType');
goog.require('goog.ui.Component');
/**
* A UI Control used for rating things, i.e. videos on Google Video.
* @param {Array<string>=} opt_ratings Ratings. Default: [1,2,3,4,5].
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
* @constructor
* @extends {goog.ui.Component}
*/
goog.ui.Ratings = function(opt_ratings, opt_domHelper) {
goog.ui.Component.call(this, opt_domHelper);
/**
* Ordered ratings that can be picked, Default: [1,2,3,4,5]
* @type {Array<string>}
* @private
*/
this.ratings_ = opt_ratings || ['1', '2', '3', '4', '5'];
/**
* Array containing references to the star elements
* @type {Array<Element>}
* @private
*/
this.stars_ = [];
// Awkward name because the obvious name is taken by subclasses already.
/**
* Whether the control is enabled.
* @type {boolean}
* @private
*/
this.isEnabled_ = true;
/**
* The last index to be highlighted
* @type {number}
* @private
*/
this.highlightedIndex_ = -1;
/**
* The currently selected index
* @type {number}
* @private
*/
this.selectedIndex_ = -1;
/**
* An attached form field to set the value to
* @type {HTMLInputElement|HTMLSelectElement|null}
* @private
*/
this.attachedFormField_ = null;
};
goog.inherits(goog.ui.Ratings, goog.ui.Component);
goog.tagUnsealableClass(goog.ui.Ratings);
/**
* Default CSS class to be applied to the root element of components rendered
* by this renderer.
* @type {string}
*/
goog.ui.Ratings.CSS_CLASS = goog.getCssName('goog-ratings');
/**
* Enums for Ratings event type.
* @enum {string}
*/
goog.ui.Ratings.EventType = {
CHANGE: 'change',
HIGHLIGHT_CHANGE: 'highlightchange',
HIGHLIGHT: 'highlight',
UNHIGHLIGHT: 'unhighlight'
};
/**
* Decorate a HTML structure already in the document. Expects the structure:
* <pre>
* - div
* - select
* - option 1 #text = 1 star
* - option 2 #text = 2 stars
* - option 3 #text = 3 stars
* - option N (where N is max number of ratings)
* </pre>
*
* The div can contain other elements for graceful degredation, but they will be
* hidden when the decoration occurs.
*
* @param {Element} el Div element to decorate.
* @override
*/
goog.ui.Ratings.prototype.decorateInternal = function(el) {
var select = el.getElementsByTagName('select')[0];
if (!select) {
throw Error('Can not decorate ' + el + ', with Ratings. Must ' +
'contain select box');
}
this.ratings_.length = 0;
for (var i = 0, n = select.options.length; i < n; i++) {
var option = select.options[i];
this.ratings_.push(option.text);
}
this.setSelectedIndex(select.selectedIndex);
select.style.display = 'none';
this.attachedFormField_ = select;
this.createDom();
el.insertBefore(this.getElement(), select);
};
/**
* Render the rating widget inside the provided element. This will override the
* current content of the element.
* @override
*/
goog.ui.Ratings.prototype.enterDocument = function() {
var el = this.getElement();
goog.asserts.assert(el, 'The DOM element for ratings cannot be null.');
el.tabIndex = 0;
goog.dom.classlist.add(el, this.getCssClass());
goog.a11y.aria.setRole(el, goog.a11y.aria.Role.SLIDER);
goog.a11y.aria.setState(el, goog.a11y.aria.State.VALUEMIN, 0);
var max = this.ratings_.length - 1;
goog.a11y.aria.setState(el, goog.a11y.aria.State.VALUEMAX, max);
var handler = this.getHandler();
handler.listen(el, 'keydown', this.onKeyDown_);
// Create the elements for the stars
for (var i = 0; i < this.ratings_.length; i++) {
var star = this.getDomHelper().createDom('span', {
'title': this.ratings_[i],
'class': this.getClassName_(i, false),
'index': i});
this.stars_.push(star);
el.appendChild(star);
}
handler.listen(el, goog.events.EventType.CLICK, this.onClick_);
handler.listen(el, goog.events.EventType.MOUSEOUT, this.onMouseOut_);
handler.listen(el, goog.events.EventType.MOUSEOVER, this.onMouseOver_);
this.highlightIndex_(this.selectedIndex_);
};
/**
* Should be called when the widget is removed from the document but may be
* reused. This removes all the listeners the widget has attached and destroys
* the DOM nodes it uses.
* @override
*/
goog.ui.Ratings.prototype.exitDocument = function() {
goog.ui.Ratings.superClass_.exitDocument.call(this);
for (var i = 0; i < this.stars_.length; i++) {
this.getDomHelper().removeNode(this.stars_[i]);
}
this.stars_.length = 0;
};
/** @override */
goog.ui.Ratings.prototype.disposeInternal = function() {
goog.ui.Ratings.superClass_.disposeInternal.call(this);
this.ratings_.length = 0;
};
/**
* Returns the base CSS class used by subcomponents of this component.
* @return {string} Component-specific CSS class.
*/
goog.ui.Ratings.prototype.getCssClass = function() {
return goog.ui.Ratings.CSS_CLASS;
};
/**
* Sets the selected index. If the provided index is greater than the number of
* ratings then the max is set. 0 is the first item, -1 is no selection.
* @param {number} index The index of the rating to select.
*/
goog.ui.Ratings.prototype.setSelectedIndex = function(index) {
index = Math.max(-1, Math.min(index, this.ratings_.length - 1));
if (index != this.selectedIndex_) {
this.selectedIndex_ = index;
this.highlightIndex_(this.selectedIndex_);
if (this.attachedFormField_) {
if (this.attachedFormField_.tagName == 'SELECT') {
this.attachedFormField_.selectedIndex = index;
} else {
this.attachedFormField_.value =
/** @type {string} */ (this.getValue());
}
var ratingsElement = this.getElement();
goog.asserts.assert(ratingsElement,
'The DOM ratings element cannot be null.');
goog.a11y.aria.setState(ratingsElement,
goog.a11y.aria.State.VALUENOW,
this.ratings_[index]);
}
this.dispatchEvent(goog.ui.Ratings.EventType.CHANGE);
}
};
/**
* @return {number} The index of the currently selected rating.
*/
goog.ui.Ratings.prototype.getSelectedIndex = function() {
return this.selectedIndex_;
};
/**
* Returns the rating value of the currently selected rating
* @return {?string} The value of the currently selected rating (or null).
*/
goog.ui.Ratings.prototype.getValue = function() {
return this.selectedIndex_ == -1 ? null : this.ratings_[this.selectedIndex_];
};
/**
* Returns the index of the currently highlighted rating, -1 if the mouse isn't
* currently over the widget
* @return {number} The index of the currently highlighted rating.
*/
goog.ui.Ratings.prototype.getHighlightedIndex = function() {
return this.highlightedIndex_;
};
/**
* Returns the value of the currently highlighted rating, null if the mouse
* isn't currently over the widget
* @return {?string} The value of the currently highlighted rating, or null.
*/
goog.ui.Ratings.prototype.getHighlightedValue = function() {
return this.highlightedIndex_ == -1 ? null :
this.ratings_[this.highlightedIndex_];
};
/**
* Sets the array of ratings that the comonent
* @param {Array<string>} ratings Array of value to use as ratings.
*/
goog.ui.Ratings.prototype.setRatings = function(ratings) {
this.ratings_ = ratings;
// TODO(user): If rendered update stars
};
/**
* Gets the array of ratings that the component
* @return {Array<string>} Array of ratings.
*/
goog.ui.Ratings.prototype.getRatings = function() {
return this.ratings_;
};
/**
* Attaches an input or select element to the ratings widget. The value or
* index of the field will be updated along with the ratings widget.
* @param {HTMLSelectElement|HTMLInputElement} field The field to attach to.
*/
goog.ui.Ratings.prototype.setAttachedFormField = function(field) {
this.attachedFormField_ = field;
};
/**
* Returns the attached input or select element to the ratings widget.
* @return {HTMLSelectElement|HTMLInputElement|null} The attached form field.
*/
goog.ui.Ratings.prototype.getAttachedFormField = function() {
return this.attachedFormField_;
};
/**
* Enables or disables the ratings control.
* @param {boolean} enable Whether to enable or disable the control.
*/
goog.ui.Ratings.prototype.setEnabled = function(enable) {
this.isEnabled_ = enable;
if (!enable) {
// Undo any highlighting done during mouseover when disabling the control
// and highlight the last selected rating.
this.resetHighlights_();
}
};
/**
* @return {boolean} Whether the ratings control is enabled.
*/
goog.ui.Ratings.prototype.isEnabled = function() {
return this.isEnabled_;
};
/**
* Handle the mouse moving over a star.
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.Ratings.prototype.onMouseOver_ = function(e) {
if (!this.isEnabled()) {
return;
}
if (goog.isDef(e.target.index)) {
var n = e.target.index;
if (this.highlightedIndex_ != n) {
this.highlightIndex_(n);
this.highlightedIndex_ = n;
this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT_CHANGE);
this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT);
}
}
};
/**
* Handle the mouse moving over a star.
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.Ratings.prototype.onMouseOut_ = function(e) {
// Only remove the highlight if the mouse is not moving to another star
if (e.relatedTarget && !goog.isDef(e.relatedTarget.index)) {
this.resetHighlights_();
}
};
/**
* Handle the mouse moving over a star.
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.Ratings.prototype.onClick_ = function(e) {
if (!this.isEnabled()) {
return;
}
if (goog.isDef(e.target.index)) {
this.setSelectedIndex(e.target.index);
}
};
/**
* Handle the key down event. 0 = unselected in this case, 1 = the first rating
* @param {goog.events.BrowserEvent} e The browser event.
* @private
*/
goog.ui.Ratings.prototype.onKeyDown_ = function(e) {
if (!this.isEnabled()) {
return;
}
switch (e.keyCode) {
case 27: // esc
this.setSelectedIndex(-1);
break;
case 36: // home
this.setSelectedIndex(0);
break;
case 35: // end
this.setSelectedIndex(this.ratings_.length);
break;
case 37: // left arrow
this.setSelectedIndex(this.getSelectedIndex() - 1);
break;
case 39: // right arrow
this.setSelectedIndex(this.getSelectedIndex() + 1);
break;
default:
// Detected a numeric key stroke, such as 0 - 9. 0 clears, 1 is first
// star, 9 is 9th star or last if there are less than 9 stars.
var num = parseInt(String.fromCharCode(e.keyCode), 10);
if (!isNaN(num)) {
this.setSelectedIndex(num - 1);
}
}
};
/**
* Resets the highlights to the selected rating to undo highlights due to hover
* effects.
* @private
*/
goog.ui.Ratings.prototype.resetHighlights_ = function() {
this.highlightIndex_(this.selectedIndex_);
this.highlightedIndex_ = -1;
this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT_CHANGE);
this.dispatchEvent(goog.ui.Ratings.EventType.UNHIGHLIGHT);
};
/**
* Highlights the ratings up to a specific index
* @param {number} n Index to highlight.
* @private
*/
goog.ui.Ratings.prototype.highlightIndex_ = function(n) {
for (var i = 0, star; star = this.stars_[i]; i++) {
goog.dom.classlist.set(star, this.getClassName_(i, i <= n));
}
};
/**
* Get the class name for a given rating. All stars have the class:
* goog-ratings-star.
* Other possible classnames dependent on position and state are:
* goog-ratings-firststar-on
* goog-ratings-firststar-off
* goog-ratings-midstar-on
* goog-ratings-midstar-off
* goog-ratings-laststar-on
* goog-ratings-laststar-off
* @param {number} i Index to get class name for.
* @param {boolean} on Whether it should be on.
* @return {string} The class name.
* @private
*/
goog.ui.Ratings.prototype.getClassName_ = function(i, on) {
var className;
var enabledClassName;
var baseClass = this.getCssClass();
if (i === 0) {
className = goog.getCssName(baseClass, 'firststar');
} else if (i == this.ratings_.length - 1) {
className = goog.getCssName(baseClass, 'laststar');
} else {
className = goog.getCssName(baseClass, 'midstar');
}
if (on) {
className = goog.getCssName(className, 'on');
} else {
className = goog.getCssName(className, 'off');
}
if (this.isEnabled_) {
enabledClassName = goog.getCssName(baseClass, 'enabled');
} else {
enabledClassName = goog.getCssName(baseClass, 'disabled');
}
return goog.getCssName(baseClass, 'star') + ' ' + className +
' ' + enabledClassName;
};