// 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. // Copyright 2007 Google Inc. All Rights Reserved. /** * @fileoverview Abstract class for all UI components. This defines the standard * design pattern that all UI components should follow. * */ goog.provide('goog.ui.Component'); goog.provide('goog.ui.Component.Error'); goog.provide('goog.ui.Component.EventType'); goog.provide('goog.ui.Component.State'); goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.dom.DomHelper'); goog.require('goog.events'); goog.require('goog.events.Event'); goog.require('goog.events.EventHandler'); goog.require('goog.events.EventTarget'); goog.require('goog.object'); goog.require('goog.style'); goog.require('goog.ui.IdGenerator'); /** * Default implementation of UI component. * * @param {goog.dom.DomHelper} opt_domHelper Optional DOM helper. * @constructor * @extends {goog.events.EventTarget} */ goog.ui.Component = function(opt_domHelper) { goog.events.EventTarget.call(this); this.dom_ = opt_domHelper || goog.dom.getDomHelper(); // Set the defalt right to left value. this.rightToLeft_ = goog.ui.Component.defaultRightToLeft_; }; goog.inherits(goog.ui.Component, goog.events.EventTarget); /** * Generator for unique IDs. * @type {goog.ui.IdGenerator} * @private */ goog.ui.Component.prototype.idGenerator_ = goog.ui.IdGenerator.getInstance(); /** * The default right to left value. * @type {boolean?} * @private */ goog.ui.Component.defaultRightToLeft_ = null; /** * Common events fired by components so that event propagation is useful. Not * all components are expected to dispatch or listen for all event types. * Events dispatched before a state transition should be cancelable to prevent * the corresponding state change. * @enum {string} */ goog.ui.Component.EventType = { /** Dispatched before the component becomes visible. */ BEFORE_SHOW: 'beforeshow', /** * Dispatched after the component becomes visible. * NOTE: For goog.ui.Container, this actually fires before containers * are shown. Use goog.ui.Container.EventType.AFTER_SHOW if you want an event * that fires after a goog.ui.Container is shown. */ SHOW: 'show', /** Dispatched before the component becomes hidden. */ HIDE: 'hide', /** Dispatched before the component becomes disabled. */ DISABLE: 'disable', /** Dispatched before the component becomes enabled. */ ENABLE: 'enable', /** Dispatched before the component becomes highlighted. */ HIGHLIGHT: 'highlight', /** Dispatched before the component becomes un-highlighted. */ UNHIGHLIGHT: 'unhighlight', /** Dispatched before the component becomes activated. */ ACTIVATE: 'activate', /** Dispatched before the component becomes deactivated. */ DEACTIVATE: 'deactivate', /** Dispatched before the component becomes selected. */ SELECT: 'select', /** Dispatched before the component becomes un-selected. */ UNSELECT: 'unselect', /** Dispatched before a component becomes checked. */ CHECK: 'check', /** Dispatched before a component becomes un-checked. */ UNCHECK: 'uncheck', /** Dispatched before a component becomes focused. */ FOCUS: 'focus', /** Dispatched before a component becomes blurred. */ BLUR: 'blur', /** Dispatched before a component is opened (expanded). */ OPEN: 'open', /** Dispatched before a component is closed (collapsed). */ CLOSE: 'close', /** Dispatched after a component is moused over. */ ENTER: 'enter', /** Dispatched after a component is moused out of. */ LEAVE: 'leave', /** Dispatched after the user activates the component. */ ACTION: 'action', /** Dispatched after the external-facing state of a component is changed. */ CHANGE: 'change' }; /** * Errors thrown by the component. * @enum {string} */ goog.ui.Component.Error = { /** * Error when a method is not supported. */ NOT_SUPPORTED: 'Method not supported', /** * Error when the given element can not be decorated. */ DECORATE_INVALID: 'Invalid element to decorate', /** * Error when the component is already rendered and another render attempt is * made. */ ALREADY_RENDERED: 'Component already rendered', /** * Error when an attempt is made to set the parent of a component in a way * that would result in an inconsistent object graph. */ PARENT_UNABLE_TO_BE_SET: 'Unable to set parent component', /** * Error when an attempt is made to add a child component at an out-of-bounds * index. We don't support sparse child arrays. */ CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds', /** * Error when an attempt is made to remove a child component from a component * other than its parent. */ NOT_OUR_CHILD: 'Child is not in parent component', /** * Error when an operation requiring DOM interaction is made when the * component is not in the document */ NOT_IN_DOCUMENT: 'Operation not supported while component is not in document', /** * Error when an invalid component state is encountered. */ STATE_INVALID: 'Invalid component state' }; /** * Common component states. Components may have distinct appearance depending * on what state(s) apply to them. Not all components are expected to support * all states. * @enum {number} */ goog.ui.Component.State = { /** * Union of all supported component states. */ ALL: 0xFF, /** * Component is disabled. * @see goog.ui.Component.EventType.DISABLE * @see goog.ui.Component.EventType.ENABLE */ DISABLED: 0x01, /** * Component is highlighted. * @see goog.ui.Component.EventType.HIGHLIGHT * @see goog.ui.Component.EventType.UNHIGHLIGHT */ HOVER: 0x02, /** * Component is active (or "pressed"). * @see goog.ui.Component.EventType.ACTIVATE * @see goog.ui.Component.EventType.DEACTIVATE */ ACTIVE: 0x04, /** * Component is selected. * @see goog.ui.Component.EventType.SELECT * @see goog.ui.Component.EventType.UNSELECT */ SELECTED: 0x08, /** * Component is checked. * @see goog.ui.Component.EventType.CHECK * @see goog.ui.Component.EventType.UNCHECK */ CHECKED: 0x10, /** * Component has focus. * @see goog.ui.Component.EventType.FOCUS * @see goog.ui.Component.EventType.BLUR */ FOCUSED: 0x20, /** * Component is opened (expanded). Applies to tree nodes, menu buttons, * submenus, zippys (zippies?), etc. * @see goog.ui.Component.EventType.OPEN * @see goog.ui.Component.EventType.CLOSE */ OPENED: 0x40 }; /** * Static helper method; returns the type of event components are expected to * dispatch when transitioning to or from the given state. * @param {goog.ui.Component.State} state State to/from which the component * is transitioning. * @param {boolean} isEntering Whether the component is entering or leaving the * state. * @return {goog.ui.Component.EventType} Event type to dispatch. */ goog.ui.Component.getStateTransitionEvent = function(state, isEntering) { switch (state) { case goog.ui.Component.State.DISABLED: return isEntering ? goog.ui.Component.EventType.DISABLE : goog.ui.Component.EventType.ENABLE; case goog.ui.Component.State.HOVER: return isEntering ? goog.ui.Component.EventType.HIGHLIGHT : goog.ui.Component.EventType.UNHIGHLIGHT; case goog.ui.Component.State.ACTIVE: return isEntering ? goog.ui.Component.EventType.ACTIVATE : goog.ui.Component.EventType.DEACTIVATE; case goog.ui.Component.State.SELECTED: return isEntering ? goog.ui.Component.EventType.SELECT : goog.ui.Component.EventType.UNSELECT; case goog.ui.Component.State.CHECKED: return isEntering ? goog.ui.Component.EventType.CHECK : goog.ui.Component.EventType.UNCHECK; case goog.ui.Component.State.FOCUSED: return isEntering ? goog.ui.Component.EventType.FOCUS : goog.ui.Component.EventType.BLUR; case goog.ui.Component.State.OPENED: return isEntering ? goog.ui.Component.EventType.OPEN : goog.ui.Component.EventType.CLOSE; default: // Fall through. } // Invalid state. throw Error(goog.ui.Component.Error.STATE_INVALID); }; /** * Set the default right-to-left value. This causes all component's created from * this point foward to have the given value. This is useful for cases where * a given page is always in one directionality, avoiding unnecessary * right to left determinations. * @param {boolean?} rightToLeft Whether the components should be rendered * right-to-left. Null iff components should determine their directionality. */ goog.ui.Component.setDefaultRightToLeft = function(rightToLeft) { goog.ui.Component.defaultRightToLeft_ = rightToLeft; }; /** * Unique ID of the component, lazily initialized in {@link * goog.ui.Component#getId} if needed. This property is strictly private and * must not be accessed directly outside of this class! * @type {string?} * @private */ goog.ui.Component.prototype.id_ = null; /** * DomHelper used to interact with the document, allowing components to be * created in a different window. * @type {goog.dom.DomHelper?} * @protected * @suppress {underscore} */ goog.ui.Component.prototype.dom_ = null; /** * Whether the component is in the document. * @type {boolean} * @private */ goog.ui.Component.prototype.inDocument_ = false; // TODO: Stop referring to this private field in subclasses. /** * The DOM element for the component. * @type {Element?} * @private */ goog.ui.Component.prototype.element_ = null; /** * Event handler. * TODO: rename it to handler_ after all component subclasses in * inside Google have been cleaned up. * Code search: http://go/component_code_search * @type {goog.events.EventHandler} * @private */ goog.ui.Component.prototype.googUiComponentHandler_; /** * Whether the component is rendered right-to-left. Right-to-left is set * lazily when {@link #isRightToLeft} is called the first time, unless it has * been set by calling {@link #setRightToLeft} explicitly. * @type {boolean?} * @private */ goog.ui.Component.prototype.rightToLeft_ = null; /** * Arbitrary data object associated with the component. Such as meta-data. * @type {*} * @private */ goog.ui.Component.prototype.model_ = null; /** * Parent component to which events will be propagated. This property is * strictly private and must not be accessed directly outside of this class! * @type {goog.ui.Component?} * @private */ goog.ui.Component.prototype.parent_ = null; /** * Array of child components. Lazily initialized on first use. Must be kept in * sync with {@code childIndex_}. This property is strictly private and must * not be accessed directly outside of this class! * @type {Array.<goog.ui.Component>?} * @private */ goog.ui.Component.prototype.children_ = null; /** * Map of child component IDs to child components. Used for constant-time * random access to child components by ID. Lazily initialized on first use. * Must be kept in sync with {@code children_}. This property is strictly * private and must not be accessed directly outside of this class! * * We use a plain Object, not a {@link goog.structs.Map}, for simplicity. * This means components can't have children with IDs such as 'constructor' or * 'valueOf', but this shouldn't really be an issue in practice, and if it is, * we can always fix it later without changing the API. * * @type {Object?} * @private */ goog.ui.Component.prototype.childIndex_ = null; /** * Flag used to keep track of whether a component decorated an already existing * element or whether it created the DOM itself. If an element was decorated * dispose will remove the node from the document, it is left up to the app. * @type {boolean} * @private */ goog.ui.Component.prototype.wasDecorated_ = false; /** * Gets the unique ID for the instance of this component. If the instance * doesn't already have an ID, generates one on the fly. * @return {string} Unique component ID. */ goog.ui.Component.prototype.getId = function() { return this.id_ || (this.id_ = this.idGenerator_.getNextUniqueId()); }; /** * Assigns an ID to this component instance. It is the caller's responsibility * to guarantee that the ID is unique. If the component is a child of a parent * component, then the parent component's child index is updated to reflect the * new ID; this may throw an error if the parent already has a child with an ID * that conflicts with the new ID. * @param {string} id Unique component ID. */ goog.ui.Component.prototype.setId = function(id) { if (this.parent_ && this.parent_.childIndex_) { // Update the parent's child index. goog.object.remove(this.parent_.childIndex_, this.id_); goog.object.add(this.parent_.childIndex_, id, this); } // Update the component ID. this.id_ = id; }; /** * Gets the component's element. * @return {Element?} The element for the component. */ goog.ui.Component.prototype.getElement = function() { return this.element_; }; /** * Sets the component's root element to the given element. Considered * protected and final. * @param {Element} element Root element for the component. * @protected */ goog.ui.Component.prototype.setElementInternal = function(element) { this.element_ = element; }; /** * Returns the event handler for this component, lazily created the first time * this method is called. * @return {!goog.events.EventHandler} Event handler for this component. * @protected */ goog.ui.Component.prototype.getHandler = function() { return this.googUiComponentHandler_ || (this.googUiComponentHandler_ = new goog.events.EventHandler(this)); }; /** * Sets the parent of this component to use for event bubbling. Throws an error * if the component already has a parent or if an attempt is made to add a * component to itself as a child. Callers must use {@code removeChild} * or {@code removeChildAt} to remove components from their containers before * calling this method. * @see goog.ui.Component#removeChild * @see goog.ui.Component#removeChildAt * @param {goog.ui.Component} parent The parent component. */ goog.ui.Component.prototype.setParent = function(parent) { if (this == parent) { // Attempting to add a child to itself is an error. throw Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET); } if (parent && this.parent_ && this.id_ && this.parent_.getChild(this.id_) && this.parent_ != parent) { // This component is already the child of some parent, so it should be // removed using removeChild/removeChildAt first. throw Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET); } this.parent_ = parent; goog.ui.Component.superClass_.setParentEventTarget.call(this, parent); }; /** * Returns the component's parent, if any. * @return {goog.ui.Component?} The parent component. */ goog.ui.Component.prototype.getParent = function() { return this.parent_; }; /** * Overrides {@link goog.events.EventTarget#setParentEventTarget} to throw an * error if the parent component is set, and the argument is not the parent. * * @param {goog.events.EventTarget} parent Parent EventTarget (null if none). */ goog.ui.Component.prototype.setParentEventTarget = function(parent) { if (this.parent_ && this.parent_ != parent) { throw Error(goog.ui.Component.Error.NOT_SUPPORTED); } goog.ui.Component.superClass_.setParentEventTarget.call(this, parent); }; /** * Returns the dom helper that is being used on this component. * @return {goog.dom.DomHelper} The dom helper used on this component. */ goog.ui.Component.prototype.getDomHelper = function() { return this.dom_; }; /** * Determines whether the component has been added to the document. * @return {boolean} TRUE if rendered. Otherwise, FALSE. */ goog.ui.Component.prototype.isInDocument = function() { return this.inDocument_; }; /** * Creates the initial DOM representation for the component. The default * implementation is to set this.element_ = div. */ goog.ui.Component.prototype.createDom = function() { this.element_ = this.dom_.createElement('div'); }; /** * Renders the component. If a parent element is supplied, it should already be * in the document and then the component's element will be appended to it. If * there is no optional parent element and the element doesn't have a parentNode * then it will be appended to the document body. * * Throws an Error if the component is already rendered. * * @param {Element} opt_parentElement Optional parent element to render the * component into. */ goog.ui.Component.prototype.render = function(opt_parentElement) { this.render_(opt_parentElement); }; /** * Renders the component before another element. The other element should be in * the document already. * * Throws an Error if the component is already rendered. * * @param {Element} siblingElement Element to render the component before. */ goog.ui.Component.prototype.renderBefore = function(siblingElement) { this.render_(/** @type {Element} */(siblingElement.parentNode), siblingElement); }; /** * Renders the component. If a parent element is supplied, it should already be * in the document and then the component's element will be appended to it. If * there is no optional parent element and the element doesn't have a parentNode * then it will be appended to the document body. * * Throws an Error if the component is already rendered. * * @param {Element} opt_parentElement Optional parent element to render the * component into. * @param {Element} opt_beforeElement Element before which the component is to * be rendered. If left out the node is appended to the parent element. * @private */ goog.ui.Component.prototype.render_ = function(opt_parentElement, opt_beforeElement) { if (this.inDocument_) { throw Error(goog.ui.Component.Error.ALREADY_RENDERED); } if (!this.element_) { this.createDom(); } if (opt_parentElement) { opt_parentElement.insertBefore(this.element_, opt_beforeElement || null); } else { this.dom_.getDocument().body.appendChild(this.element_); } // If this component has a parent component that isn't in the document yet, // we don't call enterDocument() here. Instead, when the parent component // enters the document, the enterDocument() call will propagate to its // children, including this one. If the component doesn't have a parent // or if the parent is already in the document, we call enterDocument(). if (!this.parent_ || this.parent_.isInDocument()) { this.enterDocument(); } }; /** * Decorates the element for the UI component. * @param {Element} element Element to decorate. */ goog.ui.Component.prototype.decorate = function(element) { if (this.inDocument_) { throw Error(goog.ui.Component.Error.ALREADY_RENDERED); } else if (element && this.canDecorate(element)) { this.wasDecorated_ = true; // Set the DOM helper of the component to match the decorated element. if (!this.dom_ || this.dom_.getDocument() != goog.dom.getOwnerDocument(element)) { this.dom_ = goog.dom.getDomHelper(element); } // Call specific component decorate logic. this.decorateInternal(element); this.enterDocument(); } else { throw Error(goog.ui.Component.Error.DECORATE_INVALID); } }; /** * Determines if a given element can be decorated by this type of component. * This method should be overridden by inheriting objects. * @param {Element} element Element to decorate. * @return {boolean} True if the element can be decorated, false otherwise. */ goog.ui.Component.prototype.canDecorate = function(element) { return true; }; /** * @return {boolean} Whether the component was decorated. */ goog.ui.Component.prototype.wasDecorated = function() { return this.wasDecorated_; }; /** * Actually decorates the element. Should be overridden by inheriting objects. * This method can assume there are checks to ensure the component has not * already been rendered have occurred and that enter document will be called * afterwards. This method is considered protected. * @param {Element} element Element to decorate. * @protected */ goog.ui.Component.prototype.decorateInternal = function(element) { this.element_ = element; }; /** * Called when the component's element is known to be in the document. Anything * using document.getElementById etc. should be done at this stage. * * If the component contains child components, this call is propagated to its * children. */ goog.ui.Component.prototype.enterDocument = function() { this.inDocument_ = true; // Propagate enterDocument to child components that have a DOM, if any. this.forEachChild(function(child) { if (!child.isInDocument() && child.getElement()) { child.enterDocument(); } }); }; /** * Called by dispose to clean up the elements and listeners created by a * component, or by a parent component/application who has removed the * component from the document but wants to reuse it later. * * If the component contains child components, this call is propagated to its * children. * * It should be possible for the component to be rendered again once this method * has been called. */ goog.ui.Component.prototype.exitDocument = function() { // Propagate exitDocument to child components that have been rendered, if any. this.forEachChild(function(child) { if (child.isInDocument()) { child.exitDocument(); } }); if (this.googUiComponentHandler_) { this.googUiComponentHandler_.removeAll(); } this.inDocument_ = false; }; /** * Disposes of the component. Calls {@code exitDocument}, which is expected to * remove event handlers and clean up the component. Propagates the call to * the component's children, if any. Removes the component's DOM from the * document unless it was decorated. * @override */ goog.ui.Component.prototype.disposeInternal = function() { goog.ui.Component.superClass_.disposeInternal.call(this); if (this.inDocument_) { this.exitDocument(); } if (this.googUiComponentHandler_) { this.googUiComponentHandler_.dispose(); delete this.googUiComponentHandler_; } // Disposes of the component's children, if any. this.forEachChild(function(child) { child.dispose(); }); // Detach the component's element from the DOM, unless it was decorated. if (!this.wasDecorated_ && this.element_) { goog.dom.removeNode(this.element_); } this.children_ = null; this.childIndex_ = null; this.element_ = null; this.model_ = null; this.parent_ = null; }; /** * Helper function for subclasses that gets a unique id for a given fragment, * this can be used by components to * generate unique string ids for DOM elements * @param {string} idFragment A partial id. * @return {string} Unique element id. */ goog.ui.Component.prototype.makeId = function(idFragment) { return this.getId() + '.' + idFragment; }; /** * Returns the model associated with the UI component. * @return {*} The model. */ goog.ui.Component.prototype.getModel = function() { return this.model_; }; /** * Sets the model associated with the UI component. * @param {*} obj The model. */ goog.ui.Component.prototype.setModel = function(obj) { this.model_ = obj; }; /** * Helper function for returning the fragment portion of an id generated using * makeId(). * @param {string} id Id generated with makeId(). * @return {string} Fragment. */ goog.ui.Component.prototype.getFragmentFromId = function(id) { return id.substring(this.getId().length + 1); }; /** * Helper function for returning an element in the document with a unique id * generated using makeId(). * @param {string} idFragment The partial id. * @return {Element?} The element with the unique id, or null if it cannot be * found. */ goog.ui.Component.prototype.getElementByFragment = function(idFragment) { if (!this.inDocument_) { throw Error(goog.ui.Component.Error.NOT_IN_DOCUMENT); } return this.dom_.getElement(this.makeId(idFragment)); }; /** * Adds the specified component as the last child of this component. See * {@link goog.ui.Component#addChildAt} for detailed semantics. * * @see goog.ui.Component#addChildAt * @param {goog.ui.Component} child The new child component. * @param {boolean} opt_render If true, the child component will be rendered * into the parent. */ goog.ui.Component.prototype.addChild = function(child, opt_render) { this.addChildAt(child, this.getChildCount(), opt_render); }; /** * Adds the specified component as a child of this component at the given * 0-based index. * * Both {@code addChild} and {@code addChildAt} assume the following contract * between parent and child components: * <ul> * <li>the child component's element must be a descendant of the parent * component's element, and * <li>the DOM state of the child component must be consistent with the DOM * state of the parent component (see {@code isInDocument}). * </ul> * * In particular, {@code parent.addChild(child)} will throw an error if the * child component is already in the document, but the parent isn't. * * Clients of this API may call {@code addChild} and {@code addChildAt} with * {@code opt_render} set to true. If {@code opt_render} is true, calling these * methods will automatically render the child component's element into the * parent component's element. However, {@code parent.addChild(child, true)} * will throw an error if: * <ul> * <li>the parent component has no DOM (i.e. {@code parent.getElement()} is * null), or * <li>the child component is already in the document, regardless of the * parent's DOM state. * </ul> * * Finally, this method also throws an error if the new child already has a * different parent, or the given index is out of bounds. * * @see goog.ui.Component#addChild * @param {goog.ui.Component} child The new child component. * @param {number} index 0-based index at which the new child component is to be * added; must be between 0 and the current child count (inclusive). * @param {boolean} opt_render If true, the child component will be rendered * into the parent. */ goog.ui.Component.prototype.addChildAt = function(child, index, opt_render) { if (child.inDocument_ && (opt_render || !this.inDocument_)) { // Adding a child that's already in the document is an error, except if the // parent is also in the document and opt_render is false (e.g. decorate()). throw Error(goog.ui.Component.Error.ALREADY_RENDERED); } if (index < 0 || index > this.getChildCount()) { // Allowing sparse child arrays would lead to strange behavior, so we don't. throw Error(goog.ui.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS); } // Create the index and the child array on first use. if (!this.childIndex_ || !this.children_) { this.childIndex_ = {}; this.children_ = []; } // Moving child within component, remove old reference. if (child.getParent() == this) { goog.object.set(this.childIndex_, child.getId(), child); goog.array.remove(this.children_, child); // Add the child to this component. goog.object.add() throws an error if // a child with the same ID already exists. } else { goog.object.add(this.childIndex_, child.getId(), child); } // Set the parent of the child to this component. This throws an error if // the child is already contained by another component. child.setParent(this); goog.array.insertAt(this.children_, child, index); if (child.inDocument_ && this.inDocument_ && child.getParent() == this) { // Changing the position of an existing child, move the DOM node. var contentElement = this.getContentElement(); contentElement.insertBefore(child.getElement(), (contentElement.childNodes[index + 1] || null)); } else if (opt_render) { // If this (parent) component doesn't have a DOM yet, call createDom now // to make sure we render the child component's element into the correct // parent element (otherwise render_ with a null first argument would // render the child into the document body, which is almost certainly not // what we want). if (!this.element_) { this.createDom(); } // Render the child into the parent at the appropriate location. Note that // getChildAt(index + 1) returns undefined if inserting at the end. // TODO: We should have a renderer with a renderChildAt API. var sibling = this.getChildAt(index + 1); // render_() calls enterDocument() if the parent is already in the document. child.render_(this.getContentElement(), sibling ? sibling.element_ : null); } else { // We don't touch the DOM, but if the parent is in the document, the child // isn't, and the child has a DOM, then we call enterDocument on the child. if (this.inDocument_ && !child.inDocument_ && child.element_) { child.enterDocument(); } } }; /** * Returns the DOM element into which child components are to be rendered, * or null if the component itself hasn't been rendered yet. This default * implementation returns the component's root element. Subclasses with * complex DOM structures must override this method. * @return {Element?} Element to contain child elements (null if none). */ goog.ui.Component.prototype.getContentElement = function() { return this.element_; }; /** * Returns true if the component is rendered right-to-left, false otherwise. * The first time this function is invoked, the right-to-left rendering property * is set if it has not been already. * @return {boolean} Whether the control is rendered right-to-left. */ goog.ui.Component.prototype.isRightToLeft = function() { if (this.rightToLeft_ == null) { this.rightToLeft_ = goog.style.isRightToLeft(this.inDocument_ ? this.element_ : this.dom_.getDocument().body); } return /** @type {boolean} */(this.rightToLeft_); }; /** * Set is right-to-left. This function should be used if the component needs * to know the rendering direction during dom creation (i.e. before * {@link #enterDocument} is called and is right-to-left is set). * @param {boolean} rightToLeft Whether the component is rendered * right-to-left. */ goog.ui.Component.prototype.setRightToLeft = function(rightToLeft) { if (this.inDocument_) { throw Error(goog.ui.Component.Error.ALREADY_RENDERED); } this.rightToLeft_ = rightToLeft; }; /** * Returns true if the component has children. * @return {boolean} True if the component has children. */ goog.ui.Component.prototype.hasChildren = function() { return !!this.children_ && this.children_.length != 0; }; /** * Returns the number of children of this component. * @return {number} The number of children. */ goog.ui.Component.prototype.getChildCount = function() { return this.children_ ? this.children_.length : 0; }; /** * Returns an array containing the IDs of the children of this component, or an * empty array if the component has no children. * @return {Array.<string>} Child component IDs. */ goog.ui.Component.prototype.getChildIds = function() { var ids = []; // We don't use goog.object.getKeys(this.childIndex_) because we want to // return the IDs in the correct order as determined by this.children_. this.forEachChild(function(child) { // addChild()/addChildAt() guarantee that the child array isn't sparse. ids.push(child.getId()); }); return ids; }; /** * Returns the child with the given ID, or null if no such child exists. * @param {string} id Child component ID. * @return {goog.ui.Component?} The child with the given ID; null if none. */ goog.ui.Component.prototype.getChild = function(id) { // Use childIndex_ for O(1) access by ID. return (this.childIndex_ && id) ? (/** @type {goog.ui.Component} */ goog.object.get(this.childIndex_, id)) || null : null; }; /** * Returns the child at the given index, or null if the index is out of bounds. * @param {number} index 0-based index. * @return {goog.ui.Component?} The child at the given index; null if none. */ goog.ui.Component.prototype.getChildAt = function(index) { // Use children_ for access by index. return this.children_ ? this.children_[index] || null : null; }; /** * Calls the given function on each of this component's children in order. If * {@code opt_obj} is provided, it will be used as the 'this' object in the * function when called. The function should take two arguments: the child * component and its 0-based index. The return value is ignored. * @param {Function} f The function to call for every child component; should * take 2 arguments (the child and its index). * @param {Object} opt_obj Used as the 'this' object in f when called. */ goog.ui.Component.prototype.forEachChild = function(f, opt_obj) { if (this.children_) { goog.array.forEach(this.children_, f, opt_obj); } }; /** * Returns the 0-based index of the given child component, or -1 if no such * child is found. * @param {goog.ui.Component?} child The child component. * @return {number} 0-based index of the child component; -1 if not found. */ goog.ui.Component.prototype.indexOfChild = function(child) { return (this.children_ && child) ? goog.array.indexOf(this.children_, child) : -1; }; /** * Removes the given child from this component, and returns it. Throws an error * if the argument is invalid or if the specified child isn't found in the * parent component. The argument can either be a string (interpreted as the * ID of the child component to remove) or the child component itself. * * If {@code opt_unrender} is true, calls {@link goog.ui.component#exitDocument} * on the removed child, and subsequently detaches the child's DOM from the * document. Otherwise it is the caller's responsibility to clean up the child * component's DOM. * * @see goog.ui.Component#removeChildAt * @param {string|goog.ui.Component|null} child The ID of the child to remove, * or the child component itself. * @param {boolean} opt_unrender If true, calls {@code exitDocument} on the * removed child component, and detaches its DOM from the document. * @return {goog.ui.Component} The removed component, if any. */ goog.ui.Component.prototype.removeChild = function(child, opt_unrender) { if (child) { // Normalize child to be the object and id to be the ID string. This also // ensures that the child is really ours. var id = goog.isString(child) ? child : child.getId(); child = this.getChild(id); if (id && child) { goog.object.remove(this.childIndex_, id); goog.array.remove(this.children_, child); if (opt_unrender) { // Remove the child component's DOM from the document. We have to call // exitDocument first (see documentation). child.exitDocument(); if (child.element_) { goog.dom.removeNode(child.element_); } } // Child's parent must be set to null after exitDocument is called // so that the child can unlisten to its parent if required. child.setParent(null); } } if (!child) { throw Error(goog.ui.Component.Error.NOT_OUR_CHILD); } return /** @type {goog.ui.Component} */(child); }; /** * Removes the child at the given index from this component, and returns it. * Throws an error if the argument is out of bounds, or if the specified child * isn't found in the parent. See {@link goog.ui.Component#removeChild} for * detailed semantics. * * @see goog.ui.Component#removeChild * @param {number} index 0-based index of the child to remove. * @param {boolean} opt_unrender If true, calls {@code exitDocument} on the * removed child component, and detaches its DOM from the document. * @return {goog.ui.Component} The removed component, if any. */ goog.ui.Component.prototype.removeChildAt = function(index, opt_unrender) { // removeChild(null) will throw error. return this.removeChild(this.getChildAt(index), opt_unrender); }; /** * Removes every child component attached to this one. * * @see goog.ui.Component#removeChild * @param {boolean} opt_unrender If true, calls {@link #exitDocument} on the * removed child components, and detaches their DOM from the document. */ goog.ui.Component.prototype.removeChildren = function(opt_unrender) { while (this.hasChildren()) { this.removeChildAt(0, opt_unrender); } };