ratings.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. // Copyright 2006 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 base ratings widget that allows the user to select a rating,
  16. * like "star video" in Google Video. This fires a "change" event when the user
  17. * selects a rating.
  18. *
  19. * Keyboard:
  20. * ESC = Clear (if supported)
  21. * Home = 1 star
  22. * End = Full rating
  23. * Left arrow = Decrease rating
  24. * Right arrow = Increase rating
  25. * 0 = Clear (if supported)
  26. * 1 - 9 = nth star
  27. *
  28. * @see ../demos/ratings.html
  29. */
  30. goog.provide('goog.ui.Ratings');
  31. goog.provide('goog.ui.Ratings.EventType');
  32. goog.require('goog.a11y.aria');
  33. goog.require('goog.a11y.aria.Role');
  34. goog.require('goog.a11y.aria.State');
  35. goog.require('goog.asserts');
  36. goog.require('goog.dom');
  37. goog.require('goog.dom.TagName');
  38. goog.require('goog.dom.classlist');
  39. goog.require('goog.events.EventType');
  40. goog.require('goog.ui.Component');
  41. /**
  42. * A UI Control used for rating things, i.e. videos on Google Video.
  43. * @param {Array<string>=} opt_ratings Ratings. Default: [1,2,3,4,5].
  44. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  45. * @constructor
  46. * @extends {goog.ui.Component}
  47. */
  48. goog.ui.Ratings = function(opt_ratings, opt_domHelper) {
  49. goog.ui.Component.call(this, opt_domHelper);
  50. /**
  51. * Ordered ratings that can be picked, Default: [1,2,3,4,5]
  52. * @type {Array<string>}
  53. * @private
  54. */
  55. this.ratings_ = opt_ratings || ['1', '2', '3', '4', '5'];
  56. /**
  57. * Array containing references to the star elements
  58. * @type {Array<Element>}
  59. * @private
  60. */
  61. this.stars_ = [];
  62. // Awkward name because the obvious name is taken by subclasses already.
  63. /**
  64. * Whether the control is enabled.
  65. * @type {boolean}
  66. * @private
  67. */
  68. this.isEnabled_ = true;
  69. /**
  70. * The last index to be highlighted
  71. * @type {number}
  72. * @private
  73. */
  74. this.highlightedIndex_ = -1;
  75. /**
  76. * The currently selected index
  77. * @type {number}
  78. * @private
  79. */
  80. this.selectedIndex_ = -1;
  81. /**
  82. * An attached form field to set the value to
  83. * @type {HTMLInputElement|HTMLSelectElement|null}
  84. * @private
  85. */
  86. this.attachedFormField_ = null;
  87. };
  88. goog.inherits(goog.ui.Ratings, goog.ui.Component);
  89. goog.tagUnsealableClass(goog.ui.Ratings);
  90. /**
  91. * Default CSS class to be applied to the root element of components rendered
  92. * by this renderer.
  93. * @type {string}
  94. */
  95. goog.ui.Ratings.CSS_CLASS = goog.getCssName('goog-ratings');
  96. /**
  97. * Enums for Ratings event type.
  98. * @enum {string}
  99. */
  100. goog.ui.Ratings.EventType = {
  101. CHANGE: 'change',
  102. HIGHLIGHT_CHANGE: 'highlightchange',
  103. HIGHLIGHT: 'highlight',
  104. UNHIGHLIGHT: 'unhighlight'
  105. };
  106. /**
  107. * Decorate a HTML structure already in the document. Expects the structure:
  108. * <pre>
  109. * - div
  110. * - select
  111. * - option 1 #text = 1 star
  112. * - option 2 #text = 2 stars
  113. * - option 3 #text = 3 stars
  114. * - option N (where N is max number of ratings)
  115. * </pre>
  116. *
  117. * The div can contain other elements for graceful degredation, but they will be
  118. * hidden when the decoration occurs.
  119. *
  120. * @param {Element} el Div element to decorate.
  121. * @override
  122. */
  123. goog.ui.Ratings.prototype.decorateInternal = function(el) {
  124. var select = goog.dom.getElementsByTagName(
  125. goog.dom.TagName.SELECT, goog.asserts.assert(el))[0];
  126. if (!select) {
  127. throw Error(
  128. 'Can not decorate ' + el + ', with Ratings. Must ' +
  129. 'contain select box');
  130. }
  131. this.ratings_.length = 0;
  132. for (var i = 0, n = select.options.length; i < n; i++) {
  133. var option = select.options[i];
  134. this.ratings_.push(option.text);
  135. }
  136. this.setSelectedIndex(select.selectedIndex);
  137. select.style.display = 'none';
  138. this.attachedFormField_ = /** @type {HTMLSelectElement} */ (select);
  139. this.createDom();
  140. el.insertBefore(this.getElement(), select);
  141. };
  142. /**
  143. * Render the rating widget inside the provided element. This will override the
  144. * current content of the element.
  145. * @override
  146. */
  147. goog.ui.Ratings.prototype.enterDocument = function() {
  148. var el = this.getElement();
  149. goog.asserts.assert(el, 'The DOM element for ratings cannot be null.');
  150. goog.ui.Ratings.base(this, 'enterDocument');
  151. el.tabIndex = 0;
  152. goog.dom.classlist.add(el, this.getCssClass());
  153. goog.a11y.aria.setRole(el, goog.a11y.aria.Role.SLIDER);
  154. goog.a11y.aria.setState(el, goog.a11y.aria.State.VALUEMIN, 0);
  155. var max = this.ratings_.length - 1;
  156. goog.a11y.aria.setState(el, goog.a11y.aria.State.VALUEMAX, max);
  157. var handler = this.getHandler();
  158. handler.listen(el, 'keydown', this.onKeyDown_);
  159. // Create the elements for the stars
  160. for (var i = 0; i < this.ratings_.length; i++) {
  161. var star = this.getDomHelper().createDom(goog.dom.TagName.SPAN, {
  162. 'title': this.ratings_[i],
  163. 'class': this.getClassName_(i, false),
  164. 'index': i
  165. });
  166. this.stars_.push(star);
  167. el.appendChild(star);
  168. }
  169. handler.listen(el, goog.events.EventType.CLICK, this.onClick_);
  170. handler.listen(el, goog.events.EventType.MOUSEOUT, this.onMouseOut_);
  171. handler.listen(el, goog.events.EventType.MOUSEOVER, this.onMouseOver_);
  172. this.highlightIndex_(this.selectedIndex_);
  173. };
  174. /**
  175. * Should be called when the widget is removed from the document but may be
  176. * reused. This removes all the listeners the widget has attached and destroys
  177. * the DOM nodes it uses.
  178. * @override
  179. */
  180. goog.ui.Ratings.prototype.exitDocument = function() {
  181. goog.ui.Ratings.superClass_.exitDocument.call(this);
  182. for (var i = 0; i < this.stars_.length; i++) {
  183. this.getDomHelper().removeNode(this.stars_[i]);
  184. }
  185. this.stars_.length = 0;
  186. };
  187. /** @override */
  188. goog.ui.Ratings.prototype.disposeInternal = function() {
  189. goog.ui.Ratings.superClass_.disposeInternal.call(this);
  190. this.ratings_.length = 0;
  191. };
  192. /**
  193. * Returns the base CSS class used by subcomponents of this component.
  194. * @return {string} Component-specific CSS class.
  195. */
  196. goog.ui.Ratings.prototype.getCssClass = function() {
  197. return goog.ui.Ratings.CSS_CLASS;
  198. };
  199. /**
  200. * Sets the selected index. If the provided index is greater than the number of
  201. * ratings then the max is set. 0 is the first item, -1 is no selection.
  202. * @param {number} index The index of the rating to select.
  203. */
  204. goog.ui.Ratings.prototype.setSelectedIndex = function(index) {
  205. index = Math.max(-1, Math.min(index, this.ratings_.length - 1));
  206. if (index != this.selectedIndex_) {
  207. this.selectedIndex_ = index;
  208. this.highlightIndex_(this.selectedIndex_);
  209. if (this.attachedFormField_) {
  210. if (this.attachedFormField_.tagName == goog.dom.TagName.SELECT) {
  211. this.attachedFormField_.selectedIndex = index;
  212. } else {
  213. this.attachedFormField_.value =
  214. /** @type {string} */ (this.getValue());
  215. }
  216. var ratingsElement = this.getElement();
  217. goog.asserts.assert(
  218. ratingsElement, 'The DOM ratings element cannot be null.');
  219. goog.a11y.aria.setState(
  220. ratingsElement, goog.a11y.aria.State.VALUENOW, this.ratings_[index]);
  221. }
  222. this.dispatchEvent(goog.ui.Ratings.EventType.CHANGE);
  223. }
  224. };
  225. /**
  226. * @return {number} The index of the currently selected rating.
  227. */
  228. goog.ui.Ratings.prototype.getSelectedIndex = function() {
  229. return this.selectedIndex_;
  230. };
  231. /**
  232. * Returns the rating value of the currently selected rating
  233. * @return {?string} The value of the currently selected rating (or null).
  234. */
  235. goog.ui.Ratings.prototype.getValue = function() {
  236. return this.selectedIndex_ == -1 ? null : this.ratings_[this.selectedIndex_];
  237. };
  238. /**
  239. * Returns the index of the currently highlighted rating, -1 if the mouse isn't
  240. * currently over the widget
  241. * @return {number} The index of the currently highlighted rating.
  242. */
  243. goog.ui.Ratings.prototype.getHighlightedIndex = function() {
  244. return this.highlightedIndex_;
  245. };
  246. /**
  247. * Returns the value of the currently highlighted rating, null if the mouse
  248. * isn't currently over the widget
  249. * @return {?string} The value of the currently highlighted rating, or null.
  250. */
  251. goog.ui.Ratings.prototype.getHighlightedValue = function() {
  252. return this.highlightedIndex_ == -1 ? null :
  253. this.ratings_[this.highlightedIndex_];
  254. };
  255. /**
  256. * Sets the array of ratings that the comonent
  257. * @param {Array<string>} ratings Array of value to use as ratings.
  258. */
  259. goog.ui.Ratings.prototype.setRatings = function(ratings) {
  260. this.ratings_ = ratings;
  261. // TODO(user): If rendered update stars
  262. };
  263. /**
  264. * Gets the array of ratings that the component
  265. * @return {Array<string>} Array of ratings.
  266. */
  267. goog.ui.Ratings.prototype.getRatings = function() {
  268. return this.ratings_;
  269. };
  270. /**
  271. * Attaches an input or select element to the ratings widget. The value or
  272. * index of the field will be updated along with the ratings widget.
  273. * @param {HTMLSelectElement|HTMLInputElement} field The field to attach to.
  274. */
  275. goog.ui.Ratings.prototype.setAttachedFormField = function(field) {
  276. this.attachedFormField_ = field;
  277. };
  278. /**
  279. * Returns the attached input or select element to the ratings widget.
  280. * @return {HTMLSelectElement|HTMLInputElement|null} The attached form field.
  281. */
  282. goog.ui.Ratings.prototype.getAttachedFormField = function() {
  283. return this.attachedFormField_;
  284. };
  285. /**
  286. * Enables or disables the ratings control.
  287. * @param {boolean} enable Whether to enable or disable the control.
  288. */
  289. goog.ui.Ratings.prototype.setEnabled = function(enable) {
  290. this.isEnabled_ = enable;
  291. if (!enable) {
  292. // Undo any highlighting done during mouseover when disabling the control
  293. // and highlight the last selected rating.
  294. this.resetHighlights_();
  295. }
  296. };
  297. /**
  298. * @return {boolean} Whether the ratings control is enabled.
  299. */
  300. goog.ui.Ratings.prototype.isEnabled = function() {
  301. return this.isEnabled_;
  302. };
  303. /**
  304. * Handle the mouse moving over a star.
  305. * @param {goog.events.BrowserEvent} e The browser event.
  306. * @private
  307. */
  308. goog.ui.Ratings.prototype.onMouseOver_ = function(e) {
  309. if (!this.isEnabled()) {
  310. return;
  311. }
  312. if (goog.isDef(e.target.index)) {
  313. var n = e.target.index;
  314. if (this.highlightedIndex_ != n) {
  315. this.highlightIndex_(n);
  316. this.highlightedIndex_ = n;
  317. this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT_CHANGE);
  318. this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT);
  319. }
  320. }
  321. };
  322. /**
  323. * Handle the mouse moving over a star.
  324. * @param {goog.events.BrowserEvent} e The browser event.
  325. * @private
  326. */
  327. goog.ui.Ratings.prototype.onMouseOut_ = function(e) {
  328. // Only remove the highlight if the mouse is not moving to another star
  329. if (e.relatedTarget && !goog.isDef(e.relatedTarget.index)) {
  330. this.resetHighlights_();
  331. }
  332. };
  333. /**
  334. * Handle the mouse moving over a star.
  335. * @param {goog.events.BrowserEvent} e The browser event.
  336. * @private
  337. */
  338. goog.ui.Ratings.prototype.onClick_ = function(e) {
  339. if (!this.isEnabled()) {
  340. return;
  341. }
  342. if (goog.isDef(e.target.index)) {
  343. this.setSelectedIndex(e.target.index);
  344. }
  345. };
  346. /**
  347. * Handle the key down event. 0 = unselected in this case, 1 = the first rating
  348. * @param {goog.events.BrowserEvent} e The browser event.
  349. * @private
  350. */
  351. goog.ui.Ratings.prototype.onKeyDown_ = function(e) {
  352. if (!this.isEnabled()) {
  353. return;
  354. }
  355. switch (e.keyCode) {
  356. case 27: // esc
  357. this.setSelectedIndex(-1);
  358. break;
  359. case 36: // home
  360. this.setSelectedIndex(0);
  361. break;
  362. case 35: // end
  363. this.setSelectedIndex(this.ratings_.length);
  364. break;
  365. case 37: // left arrow
  366. this.setSelectedIndex(this.getSelectedIndex() - 1);
  367. break;
  368. case 39: // right arrow
  369. this.setSelectedIndex(this.getSelectedIndex() + 1);
  370. break;
  371. default:
  372. // Detected a numeric key stroke, such as 0 - 9. 0 clears, 1 is first
  373. // star, 9 is 9th star or last if there are less than 9 stars.
  374. var num = parseInt(String.fromCharCode(e.keyCode), 10);
  375. if (!isNaN(num)) {
  376. this.setSelectedIndex(num - 1);
  377. }
  378. }
  379. };
  380. /**
  381. * Resets the highlights to the selected rating to undo highlights due to hover
  382. * effects.
  383. * @private
  384. */
  385. goog.ui.Ratings.prototype.resetHighlights_ = function() {
  386. this.highlightIndex_(this.selectedIndex_);
  387. this.highlightedIndex_ = -1;
  388. this.dispatchEvent(goog.ui.Ratings.EventType.HIGHLIGHT_CHANGE);
  389. this.dispatchEvent(goog.ui.Ratings.EventType.UNHIGHLIGHT);
  390. };
  391. /**
  392. * Highlights the ratings up to a specific index
  393. * @param {number} n Index to highlight.
  394. * @private
  395. */
  396. goog.ui.Ratings.prototype.highlightIndex_ = function(n) {
  397. for (var i = 0, star; star = this.stars_[i]; i++) {
  398. goog.dom.classlist.set(star, this.getClassName_(i, i <= n));
  399. }
  400. };
  401. /**
  402. * Get the class name for a given rating. All stars have the class:
  403. * goog-ratings-star.
  404. * Other possible classnames dependent on position and state are:
  405. * goog-ratings-firststar-on
  406. * goog-ratings-firststar-off
  407. * goog-ratings-midstar-on
  408. * goog-ratings-midstar-off
  409. * goog-ratings-laststar-on
  410. * goog-ratings-laststar-off
  411. * @param {number} i Index to get class name for.
  412. * @param {boolean} on Whether it should be on.
  413. * @return {string} The class name.
  414. * @private
  415. */
  416. goog.ui.Ratings.prototype.getClassName_ = function(i, on) {
  417. var className;
  418. var enabledClassName;
  419. var baseClass = this.getCssClass();
  420. if (i === 0) {
  421. className = goog.getCssName(baseClass, 'firststar');
  422. } else if (i == this.ratings_.length - 1) {
  423. className = goog.getCssName(baseClass, 'laststar');
  424. } else {
  425. className = goog.getCssName(baseClass, 'midstar');
  426. }
  427. if (on) {
  428. className = goog.getCssName(className, 'on');
  429. } else {
  430. className = goog.getCssName(className, 'off');
  431. }
  432. if (this.isEnabled_) {
  433. enabledClassName = goog.getCssName(baseClass, 'enabled');
  434. } else {
  435. enabledClassName = goog.getCssName(baseClass, 'disabled');
  436. }
  437. return goog.getCssName(baseClass, 'star') + ' ' + className + ' ' +
  438. enabledClassName;
  439. };