zippy.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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 Zippy widget implementation.
  16. *
  17. * @author eae@google.com (Emil A Eklund)
  18. * @see ../demos/zippy.html
  19. */
  20. goog.provide('goog.ui.Zippy');
  21. goog.provide('goog.ui.Zippy.Events');
  22. goog.provide('goog.ui.ZippyEvent');
  23. goog.require('goog.a11y.aria');
  24. goog.require('goog.a11y.aria.Role');
  25. goog.require('goog.a11y.aria.State');
  26. goog.require('goog.dom');
  27. goog.require('goog.dom.classlist');
  28. goog.require('goog.events.Event');
  29. goog.require('goog.events.EventHandler');
  30. goog.require('goog.events.EventTarget');
  31. goog.require('goog.events.EventType');
  32. goog.require('goog.events.KeyCodes');
  33. goog.require('goog.events.KeyHandler');
  34. goog.require('goog.style');
  35. /**
  36. * Zippy widget. Expandable/collapsible container, clicking the header toggles
  37. * the visibility of the content.
  38. *
  39. * @extends {goog.events.EventTarget}
  40. * @param {Element|string|null} header Header element, either element
  41. * reference, string id or null if no header exists.
  42. * @param {Element|string|function():Element=} opt_content Content element
  43. * (if any), either element reference or string id. If skipped, the caller
  44. * should handle the TOGGLE event in its own way. If a function is passed,
  45. * then if will be called to create the content element the first time the
  46. * zippy is expanded.
  47. * @param {boolean=} opt_expanded Initial expanded/visibility state. If
  48. * undefined, attempts to infer the state from the DOM. Setting visibility
  49. * using one of the standard Soy templates guarantees correct inference.
  50. * @param {Element|string=} opt_expandedHeader Element to use as the header when
  51. * the zippy is expanded.
  52. * @param {goog.dom.DomHelper=} opt_domHelper An optional DOM helper.
  53. * @constructor
  54. */
  55. goog.ui.Zippy = function(
  56. header, opt_content, opt_expanded, opt_expandedHeader, opt_domHelper) {
  57. goog.ui.Zippy.base(this, 'constructor');
  58. /**
  59. * DomHelper used to interact with the document, allowing components to be
  60. * created in a different window.
  61. * @type {!goog.dom.DomHelper}
  62. * @private
  63. */
  64. this.dom_ = opt_domHelper || goog.dom.getDomHelper();
  65. /**
  66. * Header element or null if no header exists.
  67. * @type {Element}
  68. * @private
  69. */
  70. this.elHeader_ = this.dom_.getElement(header) || null;
  71. /**
  72. * When present, the header to use when the zippy is expanded.
  73. * @type {Element}
  74. * @private
  75. */
  76. this.elExpandedHeader_ = this.dom_.getElement(opt_expandedHeader || null);
  77. /**
  78. * Function that will create the content element, or false if there is no such
  79. * function.
  80. * @type {?function():Element}
  81. * @private
  82. */
  83. this.lazyCreateFunc_ = goog.isFunction(opt_content) ? opt_content : null;
  84. /**
  85. * Content element.
  86. * @type {Element}
  87. * @private
  88. */
  89. this.elContent_ = this.lazyCreateFunc_ || !opt_content ?
  90. null :
  91. this.dom_.getElement(/** @type {!Element} */ (opt_content));
  92. /**
  93. * Expanded state.
  94. * @type {boolean}
  95. * @private
  96. */
  97. this.expanded_ = opt_expanded == true;
  98. if (!goog.isDef(opt_expanded) && !this.lazyCreateFunc_) {
  99. // For the dual caption case, we can get expanded_ from the visibility of
  100. // the expandedHeader. For the single-caption case, we use the
  101. // presence/absence of the relevant class. Using one of the standard Soy
  102. // templates guarantees that this will work.
  103. if (this.elExpandedHeader_) {
  104. this.expanded_ = goog.style.isElementShown(this.elExpandedHeader_);
  105. } else if (this.elHeader_) {
  106. this.expanded_ = goog.dom.classlist.contains(
  107. this.elHeader_, goog.getCssName('goog-zippy-expanded'));
  108. }
  109. }
  110. /**
  111. * A keyboard events handler. If there are two headers it is shared for both.
  112. * @type {goog.events.EventHandler<!goog.ui.Zippy>}
  113. * @private
  114. */
  115. this.keyboardEventHandler_ = new goog.events.EventHandler(this);
  116. /**
  117. * The keyhandler used for listening on most key events. This takes care of
  118. * abstracting away some of the browser differences.
  119. * @private {!goog.events.KeyHandler}
  120. */
  121. this.keyHandler_ = new goog.events.KeyHandler();
  122. /**
  123. * A mouse events handler. If there are two headers it is shared for both.
  124. * @type {goog.events.EventHandler<!goog.ui.Zippy>}
  125. * @private
  126. */
  127. this.mouseEventHandler_ = new goog.events.EventHandler(this);
  128. var self = this;
  129. function addHeaderEvents(el) {
  130. if (el) {
  131. el.tabIndex = 0;
  132. goog.a11y.aria.setRole(el, self.getAriaRole());
  133. goog.dom.classlist.add(el, goog.getCssName('goog-zippy-header'));
  134. self.enableMouseEventsHandling_(el);
  135. self.enableKeyboardEventsHandling_(el);
  136. }
  137. }
  138. addHeaderEvents(this.elHeader_);
  139. addHeaderEvents(this.elExpandedHeader_);
  140. // initialize based on expanded state
  141. this.setExpanded(this.expanded_);
  142. };
  143. goog.inherits(goog.ui.Zippy, goog.events.EventTarget);
  144. goog.tagUnsealableClass(goog.ui.Zippy);
  145. /**
  146. * Constants for event names
  147. *
  148. * @const
  149. */
  150. goog.ui.Zippy.Events = {
  151. // Zippy will dispatch an ACTION event for user interaction. Mimics
  152. // {@code goog.ui.Controls#performActionInternal} by first changing
  153. // the toggle state and then dispatching an ACTION event.
  154. ACTION: 'action',
  155. // Zippy state is toggled from collapsed to expanded or vice versa.
  156. TOGGLE: 'toggle'
  157. };
  158. /**
  159. * Whether to listen for and handle mouse events; defaults to true.
  160. * @type {boolean}
  161. * @private
  162. */
  163. goog.ui.Zippy.prototype.handleMouseEvents_ = true;
  164. /**
  165. * Whether to listen for and handle key events; defaults to true.
  166. * @type {boolean}
  167. * @private
  168. */
  169. goog.ui.Zippy.prototype.handleKeyEvents_ = true;
  170. /** @override */
  171. goog.ui.Zippy.prototype.disposeInternal = function() {
  172. goog.ui.Zippy.base(this, 'disposeInternal');
  173. goog.dispose(this.keyboardEventHandler_);
  174. goog.dispose(this.keyHandler_);
  175. goog.dispose(this.mouseEventHandler_);
  176. };
  177. /**
  178. * @return {goog.a11y.aria.Role} The ARIA role to be applied to Zippy element.
  179. */
  180. goog.ui.Zippy.prototype.getAriaRole = function() {
  181. return goog.a11y.aria.Role.TAB;
  182. };
  183. /**
  184. * @return {HTMLElement} The content element.
  185. */
  186. goog.ui.Zippy.prototype.getContentElement = function() {
  187. return /** @type {!HTMLElement} */ (this.elContent_);
  188. };
  189. /**
  190. * @return {Element} The visible header element.
  191. */
  192. goog.ui.Zippy.prototype.getVisibleHeaderElement = function() {
  193. var expandedHeader = this.elExpandedHeader_;
  194. return expandedHeader && goog.style.isElementShown(expandedHeader) ?
  195. expandedHeader :
  196. this.elHeader_;
  197. };
  198. /**
  199. * Expands content pane.
  200. */
  201. goog.ui.Zippy.prototype.expand = function() {
  202. this.setExpanded(true);
  203. };
  204. /**
  205. * Collapses content pane.
  206. */
  207. goog.ui.Zippy.prototype.collapse = function() {
  208. this.setExpanded(false);
  209. };
  210. /**
  211. * Toggles expanded state.
  212. */
  213. goog.ui.Zippy.prototype.toggle = function() {
  214. this.setExpanded(!this.expanded_);
  215. };
  216. /**
  217. * Sets expanded state.
  218. *
  219. * @param {boolean} expanded Expanded/visibility state.
  220. */
  221. goog.ui.Zippy.prototype.setExpanded = function(expanded) {
  222. if (this.elContent_) {
  223. // Hide the element, if one is provided.
  224. goog.style.setElementShown(this.elContent_, expanded);
  225. } else if (expanded && this.lazyCreateFunc_) {
  226. // Assume that when the element is not hidden upon creation.
  227. this.elContent_ = this.lazyCreateFunc_();
  228. }
  229. if (this.elContent_) {
  230. goog.dom.classlist.add(
  231. this.elContent_, goog.getCssName('goog-zippy-content'));
  232. }
  233. if (this.elExpandedHeader_) {
  234. // Hide the show header and show the hide one.
  235. goog.style.setElementShown(this.elHeader_, !expanded);
  236. goog.style.setElementShown(this.elExpandedHeader_, expanded);
  237. } else {
  238. // Update header image, if any.
  239. this.updateHeaderClassName(expanded);
  240. }
  241. this.setExpandedInternal(expanded);
  242. // Fire toggle event
  243. this.dispatchEvent(
  244. new goog.ui.ZippyEvent(
  245. goog.ui.Zippy.Events.TOGGLE, this, this.expanded_));
  246. };
  247. /**
  248. * Sets expanded internal state.
  249. *
  250. * @param {boolean} expanded Expanded/visibility state.
  251. * @protected
  252. */
  253. goog.ui.Zippy.prototype.setExpandedInternal = function(expanded) {
  254. this.expanded_ = expanded;
  255. };
  256. /**
  257. * @return {boolean} Whether the zippy is expanded.
  258. */
  259. goog.ui.Zippy.prototype.isExpanded = function() {
  260. return this.expanded_;
  261. };
  262. /**
  263. * Updates the header element's className and ARIA (accessibility) EXPANDED
  264. * state.
  265. *
  266. * @param {boolean} expanded Expanded/visibility state.
  267. * @protected
  268. */
  269. goog.ui.Zippy.prototype.updateHeaderClassName = function(expanded) {
  270. if (this.elHeader_) {
  271. goog.dom.classlist.enable(
  272. this.elHeader_, goog.getCssName('goog-zippy-expanded'), expanded);
  273. goog.dom.classlist.enable(
  274. this.elHeader_, goog.getCssName('goog-zippy-collapsed'), !expanded);
  275. goog.a11y.aria.setState(
  276. this.elHeader_, goog.a11y.aria.State.EXPANDED, expanded);
  277. }
  278. };
  279. /**
  280. * @return {boolean} Whether the Zippy handles its own key events.
  281. */
  282. goog.ui.Zippy.prototype.isHandleKeyEvents = function() {
  283. return this.handleKeyEvents_;
  284. };
  285. /**
  286. * @return {boolean} Whether the Zippy handles its own mouse events.
  287. */
  288. goog.ui.Zippy.prototype.isHandleMouseEvents = function() {
  289. return this.handleMouseEvents_;
  290. };
  291. /**
  292. * Sets whether the Zippy handles it's own keyboard events.
  293. * @param {boolean} enable Whether the Zippy handles keyboard events.
  294. */
  295. goog.ui.Zippy.prototype.setHandleKeyboardEvents = function(enable) {
  296. if (this.handleKeyEvents_ != enable) {
  297. this.handleKeyEvents_ = enable;
  298. if (enable) {
  299. this.enableKeyboardEventsHandling_(this.elHeader_);
  300. this.enableKeyboardEventsHandling_(this.elExpandedHeader_);
  301. } else {
  302. this.keyboardEventHandler_.removeAll();
  303. this.keyHandler_.detach();
  304. }
  305. }
  306. };
  307. /**
  308. * Sets whether the Zippy handles it's own mouse events.
  309. * @param {boolean} enable Whether the Zippy handles mouse events.
  310. */
  311. goog.ui.Zippy.prototype.setHandleMouseEvents = function(enable) {
  312. if (this.handleMouseEvents_ != enable) {
  313. this.handleMouseEvents_ = enable;
  314. if (enable) {
  315. this.enableMouseEventsHandling_(this.elHeader_);
  316. this.enableMouseEventsHandling_(this.elExpandedHeader_);
  317. } else {
  318. this.mouseEventHandler_.removeAll();
  319. }
  320. }
  321. };
  322. /**
  323. * Enables keyboard events handling for the passed header element.
  324. * @param {Element} header The header element.
  325. * @private
  326. */
  327. goog.ui.Zippy.prototype.enableKeyboardEventsHandling_ = function(header) {
  328. if (header) {
  329. this.keyHandler_.attach(header);
  330. this.keyboardEventHandler_.listen(
  331. this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
  332. this.onHeaderKeyDown_);
  333. }
  334. };
  335. /**
  336. * Enables mouse events handling for the passed header element.
  337. * @param {Element} header The header element.
  338. * @private
  339. */
  340. goog.ui.Zippy.prototype.enableMouseEventsHandling_ = function(header) {
  341. if (header) {
  342. this.mouseEventHandler_.listen(
  343. header, goog.events.EventType.CLICK, this.onHeaderClick_);
  344. }
  345. };
  346. /**
  347. * KeyDown event handler for header element. Enter and space toggles expanded
  348. * state.
  349. *
  350. * @param {goog.events.BrowserEvent} event KeyDown event.
  351. * @private
  352. */
  353. goog.ui.Zippy.prototype.onHeaderKeyDown_ = function(event) {
  354. if (event.keyCode == goog.events.KeyCodes.ENTER ||
  355. event.keyCode == goog.events.KeyCodes.SPACE) {
  356. this.toggle();
  357. this.dispatchActionEvent_();
  358. // Prevent enter key from submitting form.
  359. event.preventDefault();
  360. event.stopPropagation();
  361. }
  362. };
  363. /**
  364. * Click event handler for header element.
  365. *
  366. * @param {goog.events.BrowserEvent} event Click event.
  367. * @private
  368. */
  369. goog.ui.Zippy.prototype.onHeaderClick_ = function(event) {
  370. this.toggle();
  371. this.dispatchActionEvent_();
  372. };
  373. /**
  374. * Dispatch an ACTION event whenever there is user interaction with the header.
  375. * Please note that after the zippy state change is completed a TOGGLE event
  376. * will be dispatched. However, the TOGGLE event is dispatch on every toggle,
  377. * including programmatic call to {@code #toggle}.
  378. * @private
  379. */
  380. goog.ui.Zippy.prototype.dispatchActionEvent_ = function() {
  381. this.dispatchEvent(new goog.events.Event(goog.ui.Zippy.Events.ACTION, this));
  382. };
  383. /**
  384. * Object representing a zippy toggle event.
  385. *
  386. * @param {string} type Event type.
  387. * @param {goog.ui.Zippy} target Zippy widget initiating event.
  388. * @param {boolean} expanded Expanded state.
  389. * @extends {goog.events.Event}
  390. * @constructor
  391. * @final
  392. */
  393. goog.ui.ZippyEvent = function(type, target, expanded) {
  394. goog.ui.ZippyEvent.base(this, 'constructor', type, target);
  395. /**
  396. * The expanded state.
  397. * @type {boolean}
  398. */
  399. this.expanded = expanded;
  400. };
  401. goog.inherits(goog.ui.ZippyEvent, goog.events.Event);