| // 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 Zippy widget implementation. |
| * |
| * @author eae@google.com (Emil A Eklund) |
| * @see ../demos/zippy.html |
| */ |
| |
| goog.provide('goog.ui.Zippy'); |
| goog.provide('goog.ui.Zippy.Events'); |
| goog.provide('goog.ui.ZippyEvent'); |
| |
| goog.require('goog.a11y.aria'); |
| goog.require('goog.a11y.aria.Role'); |
| goog.require('goog.a11y.aria.State'); |
| goog.require('goog.dom'); |
| goog.require('goog.dom.classlist'); |
| goog.require('goog.events.Event'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.events.KeyCodes'); |
| goog.require('goog.style'); |
| |
| |
| |
| /** |
| * Zippy widget. Expandable/collapsible container, clicking the header toggles |
| * the visibility of the content. |
| * |
| * @extends {goog.events.EventTarget} |
| * @param {Element|string|null} header Header element, either element |
| * reference, string id or null if no header exists. |
| * @param {Element|string|function():Element=} opt_content Content element |
| * (if any), either element reference or string id. If skipped, the caller |
| * should handle the TOGGLE event in its own way. If a function is passed, |
| * then if will be called to create the content element the first time the |
| * zippy is expanded. |
| * @param {boolean=} opt_expanded Initial expanded/visibility state. If |
| * undefined, attempts to infer the state from the DOM. Setting visibility |
| * using one of the standard Soy templates guarantees correct inference. |
| * @param {Element|string=} opt_expandedHeader Element to use as the header when |
| * the zippy is expanded. |
| * @param {goog.dom.DomHelper=} opt_domHelper An optional DOM helper. |
| * @constructor |
| */ |
| goog.ui.Zippy = function(header, opt_content, opt_expanded, |
| opt_expandedHeader, opt_domHelper) { |
| goog.ui.Zippy.base(this, 'constructor'); |
| |
| /** |
| * DomHelper used to interact with the document, allowing components to be |
| * created in a different window. |
| * @type {!goog.dom.DomHelper} |
| * @private |
| */ |
| this.dom_ = opt_domHelper || goog.dom.getDomHelper(); |
| |
| /** |
| * Header element or null if no header exists. |
| * @type {Element} |
| * @private |
| */ |
| this.elHeader_ = this.dom_.getElement(header) || null; |
| |
| /** |
| * When present, the header to use when the zippy is expanded. |
| * @type {Element} |
| * @private |
| */ |
| this.elExpandedHeader_ = this.dom_.getElement(opt_expandedHeader || null); |
| |
| /** |
| * Function that will create the content element, or false if there is no such |
| * function. |
| * @type {?function():Element} |
| * @private |
| */ |
| this.lazyCreateFunc_ = goog.isFunction(opt_content) ? opt_content : null; |
| |
| /** |
| * Content element. |
| * @type {Element} |
| * @private |
| */ |
| this.elContent_ = this.lazyCreateFunc_ || !opt_content ? null : |
| this.dom_.getElement(/** @type {!Element} */ (opt_content)); |
| |
| /** |
| * Expanded state. |
| * @type {boolean} |
| * @private |
| */ |
| this.expanded_ = opt_expanded == true; |
| if (!goog.isDef(opt_expanded) && !this.lazyCreateFunc_) { |
| // For the dual caption case, we can get expanded_ from the visibility of |
| // the expandedHeader. For the single-caption case, we use the |
| // presence/absence of the relevant class. Using one of the standard Soy |
| // templates guarantees that this will work. |
| if (this.elExpandedHeader_) { |
| this.expanded_ = goog.style.isElementShown(this.elExpandedHeader_); |
| } else if (this.elHeader_) { |
| this.expanded_ = goog.dom.classlist.contains( |
| this.elHeader_, goog.getCssName('goog-zippy-expanded')); |
| } |
| } |
| |
| |
| /** |
| * A keyboard events handler. If there are two headers it is shared for both. |
| * @type {goog.events.EventHandler<!goog.ui.Zippy>} |
| * @private |
| */ |
| this.keyboardEventHandler_ = new goog.events.EventHandler(this); |
| |
| /** |
| * A mouse events handler. If there are two headers it is shared for both. |
| * @type {goog.events.EventHandler<!goog.ui.Zippy>} |
| * @private |
| */ |
| this.mouseEventHandler_ = new goog.events.EventHandler(this); |
| |
| var self = this; |
| function addHeaderEvents(el) { |
| if (el) { |
| el.tabIndex = 0; |
| goog.a11y.aria.setRole(el, self.getAriaRole()); |
| goog.dom.classlist.add(el, goog.getCssName('goog-zippy-header')); |
| self.enableMouseEventsHandling_(el); |
| self.enableKeyboardEventsHandling_(el); |
| } |
| } |
| addHeaderEvents(this.elHeader_); |
| addHeaderEvents(this.elExpandedHeader_); |
| |
| // initialize based on expanded state |
| this.setExpanded(this.expanded_); |
| }; |
| goog.inherits(goog.ui.Zippy, goog.events.EventTarget); |
| goog.tagUnsealableClass(goog.ui.Zippy); |
| |
| |
| /** |
| * Constants for event names |
| * |
| * @const |
| */ |
| goog.ui.Zippy.Events = { |
| // Zippy will dispatch an ACTION event for user interaction. Mimics |
| // {@code goog.ui.Controls#performActionInternal} by first changing |
| // the toggle state and then dispatching an ACTION event. |
| ACTION: 'action', |
| // Zippy state is toggled from collapsed to expanded or vice versa. |
| TOGGLE: 'toggle' |
| }; |
| |
| |
| /** |
| * Whether to listen for and handle mouse events; defaults to true. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.Zippy.prototype.handleMouseEvents_ = true; |
| |
| |
| /** |
| * Whether to listen for and handle key events; defaults to true. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.Zippy.prototype.handleKeyEvents_ = true; |
| |
| |
| /** @override */ |
| goog.ui.Zippy.prototype.disposeInternal = function() { |
| goog.ui.Zippy.base(this, 'disposeInternal'); |
| goog.dispose(this.keyboardEventHandler_); |
| goog.dispose(this.mouseEventHandler_); |
| }; |
| |
| |
| /** |
| * @return {goog.a11y.aria.Role} The ARIA role to be applied to Zippy element. |
| */ |
| goog.ui.Zippy.prototype.getAriaRole = function() { |
| return goog.a11y.aria.Role.TAB; |
| }; |
| |
| |
| /** |
| * @return {Element} The content element. |
| */ |
| goog.ui.Zippy.prototype.getContentElement = function() { |
| return this.elContent_; |
| }; |
| |
| |
| /** |
| * @return {Element} The visible header element. |
| */ |
| goog.ui.Zippy.prototype.getVisibleHeaderElement = function() { |
| var expandedHeader = this.elExpandedHeader_; |
| return expandedHeader && goog.style.isElementShown(expandedHeader) ? |
| expandedHeader : this.elHeader_; |
| }; |
| |
| |
| /** |
| * Expands content pane. |
| */ |
| goog.ui.Zippy.prototype.expand = function() { |
| this.setExpanded(true); |
| }; |
| |
| |
| /** |
| * Collapses content pane. |
| */ |
| goog.ui.Zippy.prototype.collapse = function() { |
| this.setExpanded(false); |
| }; |
| |
| |
| /** |
| * Toggles expanded state. |
| */ |
| goog.ui.Zippy.prototype.toggle = function() { |
| this.setExpanded(!this.expanded_); |
| }; |
| |
| |
| /** |
| * Sets expanded state. |
| * |
| * @param {boolean} expanded Expanded/visibility state. |
| */ |
| goog.ui.Zippy.prototype.setExpanded = function(expanded) { |
| if (this.elContent_) { |
| // Hide the element, if one is provided. |
| goog.style.setElementShown(this.elContent_, expanded); |
| } else if (expanded && this.lazyCreateFunc_) { |
| // Assume that when the element is not hidden upon creation. |
| this.elContent_ = this.lazyCreateFunc_(); |
| } |
| if (this.elContent_) { |
| goog.dom.classlist.add(this.elContent_, |
| goog.getCssName('goog-zippy-content')); |
| } |
| |
| if (this.elExpandedHeader_) { |
| // Hide the show header and show the hide one. |
| goog.style.setElementShown(this.elHeader_, !expanded); |
| goog.style.setElementShown(this.elExpandedHeader_, expanded); |
| } else { |
| // Update header image, if any. |
| this.updateHeaderClassName(expanded); |
| } |
| |
| this.setExpandedInternal(expanded); |
| |
| // Fire toggle event |
| this.dispatchEvent(new goog.ui.ZippyEvent(goog.ui.Zippy.Events.TOGGLE, |
| this, this.expanded_)); |
| }; |
| |
| |
| /** |
| * Sets expanded internal state. |
| * |
| * @param {boolean} expanded Expanded/visibility state. |
| * @protected |
| */ |
| goog.ui.Zippy.prototype.setExpandedInternal = function(expanded) { |
| this.expanded_ = expanded; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the zippy is expanded. |
| */ |
| goog.ui.Zippy.prototype.isExpanded = function() { |
| return this.expanded_; |
| }; |
| |
| |
| /** |
| * Updates the header element's className and ARIA (accessibility) EXPANDED |
| * state. |
| * |
| * @param {boolean} expanded Expanded/visibility state. |
| * @protected |
| */ |
| goog.ui.Zippy.prototype.updateHeaderClassName = function(expanded) { |
| if (this.elHeader_) { |
| goog.dom.classlist.enable(this.elHeader_, |
| goog.getCssName('goog-zippy-expanded'), expanded); |
| goog.dom.classlist.enable(this.elHeader_, |
| goog.getCssName('goog-zippy-collapsed'), !expanded); |
| goog.a11y.aria.setState(this.elHeader_, |
| goog.a11y.aria.State.EXPANDED, |
| expanded); |
| } |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the Zippy handles its own key events. |
| */ |
| goog.ui.Zippy.prototype.isHandleKeyEvents = function() { |
| return this.handleKeyEvents_; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the Zippy handles its own mouse events. |
| */ |
| goog.ui.Zippy.prototype.isHandleMouseEvents = function() { |
| return this.handleMouseEvents_; |
| }; |
| |
| |
| /** |
| * Sets whether the Zippy handles it's own keyboard events. |
| * @param {boolean} enable Whether the Zippy handles keyboard events. |
| */ |
| goog.ui.Zippy.prototype.setHandleKeyboardEvents = function(enable) { |
| if (this.handleKeyEvents_ != enable) { |
| this.handleKeyEvents_ = enable; |
| if (enable) { |
| this.enableKeyboardEventsHandling_(this.elHeader_); |
| this.enableKeyboardEventsHandling_(this.elExpandedHeader_); |
| } else { |
| this.keyboardEventHandler_.removeAll(); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Sets whether the Zippy handles it's own mouse events. |
| * @param {boolean} enable Whether the Zippy handles mouse events. |
| */ |
| goog.ui.Zippy.prototype.setHandleMouseEvents = function(enable) { |
| if (this.handleMouseEvents_ != enable) { |
| this.handleMouseEvents_ = enable; |
| if (enable) { |
| this.enableMouseEventsHandling_(this.elHeader_); |
| this.enableMouseEventsHandling_(this.elExpandedHeader_); |
| } else { |
| this.mouseEventHandler_.removeAll(); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Enables keyboard events handling for the passed header element. |
| * @param {Element} header The header element. |
| * @private |
| */ |
| goog.ui.Zippy.prototype.enableKeyboardEventsHandling_ = function(header) { |
| if (header) { |
| this.keyboardEventHandler_.listen(header, goog.events.EventType.KEYDOWN, |
| this.onHeaderKeyDown_); |
| } |
| }; |
| |
| |
| /** |
| * Enables mouse events handling for the passed header element. |
| * @param {Element} header The header element. |
| * @private |
| */ |
| goog.ui.Zippy.prototype.enableMouseEventsHandling_ = function(header) { |
| if (header) { |
| this.mouseEventHandler_.listen(header, goog.events.EventType.CLICK, |
| this.onHeaderClick_); |
| } |
| }; |
| |
| |
| /** |
| * KeyDown event handler for header element. Enter and space toggles expanded |
| * state. |
| * |
| * @param {goog.events.BrowserEvent} event KeyDown event. |
| * @private |
| */ |
| goog.ui.Zippy.prototype.onHeaderKeyDown_ = function(event) { |
| if (event.keyCode == goog.events.KeyCodes.ENTER || |
| event.keyCode == goog.events.KeyCodes.SPACE) { |
| |
| this.toggle(); |
| this.dispatchActionEvent_(); |
| |
| // Prevent enter key from submitting form. |
| event.preventDefault(); |
| |
| event.stopPropagation(); |
| } |
| }; |
| |
| |
| /** |
| * Click event handler for header element. |
| * |
| * @param {goog.events.BrowserEvent} event Click event. |
| * @private |
| */ |
| goog.ui.Zippy.prototype.onHeaderClick_ = function(event) { |
| this.toggle(); |
| this.dispatchActionEvent_(); |
| }; |
| |
| |
| /** |
| * Dispatch an ACTION event whenever there is user interaction with the header. |
| * Please note that after the zippy state change is completed a TOGGLE event |
| * will be dispatched. However, the TOGGLE event is dispatch on every toggle, |
| * including programmatic call to {@code #toggle}. |
| * @private |
| */ |
| goog.ui.Zippy.prototype.dispatchActionEvent_ = function() { |
| this.dispatchEvent(new goog.events.Event(goog.ui.Zippy.Events.ACTION, this)); |
| }; |
| |
| |
| |
| /** |
| * Object representing a zippy toggle event. |
| * |
| * @param {string} type Event type. |
| * @param {goog.ui.Zippy} target Zippy widget initiating event. |
| * @param {boolean} expanded Expanded state. |
| * @extends {goog.events.Event} |
| * @constructor |
| * @final |
| */ |
| goog.ui.ZippyEvent = function(type, target, expanded) { |
| goog.ui.ZippyEvent.base(this, 'constructor', type, target); |
| |
| /** |
| * The expanded state. |
| * @type {boolean} |
| */ |
| this.expanded = expanded; |
| }; |
| goog.inherits(goog.ui.ZippyEvent, goog.events.Event); |