Path: blob/trunk/third_party/closure/goog/ui/component.js
4500 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview Abstract class for all UI components. This defines the standard8* design pattern that all UI components should follow.9*10* @see ../demos/samplecomponent.html11* @see http://code.google.com/p/closure-library/wiki/IntroToComponents12*/1314goog.provide('goog.ui.Component');15goog.provide('goog.ui.Component.Error');16goog.provide('goog.ui.Component.EventType');17goog.provide('goog.ui.Component.State');1819goog.require('goog.array');20goog.require('goog.asserts');21goog.require('goog.dom');22goog.require('goog.dom.NodeType');23goog.require('goog.dom.TagName');24goog.require('goog.events.EventHandler');25goog.require('goog.events.EventTarget');26goog.require('goog.object');27goog.require('goog.style');28goog.require('goog.ui.IdGenerator');29303132/**33* Default implementation of UI component.34*35* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.36* @constructor37* @extends {goog.events.EventTarget}38* @suppress {underscore}39*/40goog.ui.Component = function(opt_domHelper) {41'use strict';42goog.events.EventTarget.call(this);43/**44* DomHelper used to interact with the document, allowing components to be45* created in a different window.46* @protected {!goog.dom.DomHelper}47* @suppress {underscore|visibility}48*/49this.dom_ = opt_domHelper || goog.dom.getDomHelper();5051/**52* Whether the component is rendered right-to-left. Right-to-left is set53* lazily when {@link #isRightToLeft} is called the first time, unless it has54* been set by calling {@link #setRightToLeft} explicitly.55* @private {?boolean}56*/57this.rightToLeft_ = goog.ui.Component.defaultRightToLeft_;5859/**60* Unique ID of the component, lazily initialized in {@link61* goog.ui.Component#getId} if needed. This property is strictly private and62* must not be accessed directly outside of this class!63* @private {?string}64*/65this.id_ = null;6667/**68* Whether the component is in the document.69* @private {boolean}70*/71this.inDocument_ = false;7273// TODO(attila): Stop referring to this private field in subclasses.74/**75* The DOM element for the component.76* @private {?Element}77*/78this.element_ = null;7980/**81* Event handler.82* Code search: http://go/component_code_search83* @private {goog.events.EventHandler|undefined}84*/85this.googUiComponentHandler_ = void 0;8687/**88* Arbitrary data object associated with the component. Such as meta-data.89* @private {*}90*/91this.model_ = null;9293/**94* Parent component to which events will be propagated. This property is95* strictly private and must not be accessed directly outside of this class!96* @private {goog.ui.Component?}97*/98this.parent_ = null;99100/**101* Array of child components. Lazily initialized on first use. Must be kept102* in sync with `childIndex_`. This property is strictly private and103* must not be accessed directly outside of this class!104* @private {?Array<?goog.ui.Component>}105*/106this.children_ = null;107108/**109* Map of child component IDs to child components. Used for constant-time110* random access to child components by ID. Lazily initialized on first use.111* Must be kept in sync with `children_`. This property is strictly112* private and must not be accessed directly outside of this class!113*114* We use a plain Object, not a {@link goog.structs.Map}, for simplicity.115* This means components can't have children with IDs such as 'constructor' or116* 'valueOf', but this shouldn't really be an issue in practice, and if it is,117* we can always fix it later without changing the API.118*119* @private {?Object}120*/121this.childIndex_ = null;122123/**124* Flag used to keep track of whether a component decorated an already125* existing element or whether it created the DOM itself.126*127* If an element is decorated, dispose will leave the node in the document.128* It is up to the app to remove the node.129*130* If an element was rendered, dispose will remove the node automatically.131*132* @private {boolean}133*/134this.wasDecorated_ = false;135136/**137* If true, listen for PointerEvent types rather than MouseEvent types. This138* allows supporting drag gestures for touch/stylus input.139* @private {boolean}140*/141this.pointerEventsEnabled_ = false;142};143goog.inherits(goog.ui.Component, goog.events.EventTarget);144145146/**147* @define {boolean} Whether to support calling decorate with an element that is148* not yet in the document. If true, we check if the element is in the149* document, and avoid calling enterDocument if it isn't. If false, we150* maintain legacy behavior (always call enterDocument from decorate).151*/152goog.ui.Component.ALLOW_DETACHED_DECORATION =153goog.define('goog.ui.Component.ALLOW_DETACHED_DECORATION', false);154155156/**157* Generator for unique IDs.158* @type {goog.ui.IdGenerator}159* @private160*/161goog.ui.Component.prototype.idGenerator_ = goog.ui.IdGenerator.getInstance();162163164// TODO(gboyer): See if we can remove this and just check goog.i18n.bidi.IS_RTL.165/**166* @define {number} Defines the default BIDI directionality.167* 0: Unknown.168* 1: Left-to-right.169* -1: Right-to-left.170*/171goog.ui.Component.DEFAULT_BIDI_DIR =172goog.define('goog.ui.Component.DEFAULT_BIDI_DIR', 0);173174175/**176* The default right to left value.177* @type {?boolean}178* @private179*/180goog.ui.Component.defaultRightToLeft_ =181(goog.ui.Component.DEFAULT_BIDI_DIR == 1) ?182false :183(goog.ui.Component.DEFAULT_BIDI_DIR == -1) ? true : null;184185186/**187* Common events fired by components so that event propagation is useful. Not188* all components are expected to dispatch or listen for all event types.189* Events dispatched before a state transition should be cancelable to prevent190* the corresponding state change.191* @enum {string}192*/193goog.ui.Component.EventType = {194/** Dispatched before the component becomes visible. */195BEFORE_SHOW: 'beforeshow',196197/**198* Dispatched after the component becomes visible.199* NOTE(user): For goog.ui.Container, this actually fires before containers200* are shown. Use goog.ui.Container.EventType.AFTER_SHOW if you want an event201* that fires after a goog.ui.Container is shown.202*/203SHOW: 'show',204205/** Dispatched before the component becomes hidden. */206HIDE: 'hide',207208/** Dispatched before the component becomes disabled. */209DISABLE: 'disable',210211/** Dispatched before the component becomes enabled. */212ENABLE: 'enable',213214/** Dispatched before the component becomes highlighted. */215HIGHLIGHT: 'highlight',216217/** Dispatched before the component becomes un-highlighted. */218UNHIGHLIGHT: 'unhighlight',219220/** Dispatched before the component becomes activated. */221ACTIVATE: 'activate',222223/** Dispatched before the component becomes deactivated. */224DEACTIVATE: 'deactivate',225226/** Dispatched before the component becomes selected. */227SELECT: 'select',228229/** Dispatched before the component becomes un-selected. */230UNSELECT: 'unselect',231232/** Dispatched before a component becomes checked. */233CHECK: 'check',234235/** Dispatched before a component becomes un-checked. */236UNCHECK: 'uncheck',237238/** Dispatched before a component becomes focused. */239FOCUS: 'focus',240241/** Dispatched before a component becomes blurred. */242BLUR: 'blur',243244/** Dispatched before a component is opened (expanded). */245OPEN: 'open',246247/** Dispatched before a component is closed (collapsed). */248CLOSE: 'close',249250/** Dispatched after a component is moused over. */251ENTER: 'enter',252253/** Dispatched after a component is moused out of. */254LEAVE: 'leave',255256/** Dispatched after the user activates the component. */257ACTION: 'action',258259/** Dispatched after the external-facing state of a component is changed. */260CHANGE: 'change'261};262263264/**265* Errors thrown by the component.266* @enum {string}267*/268goog.ui.Component.Error = {269/**270* Error when a method is not supported.271*/272NOT_SUPPORTED: 'Method not supported',273274/**275* Error when the given element can not be decorated.276*/277DECORATE_INVALID: 'Invalid element to decorate',278279/**280* Error when the component is already rendered and another render attempt is281* made.282*/283ALREADY_RENDERED: 'Component already rendered',284285/**286* Error when an attempt is made to set the parent of a component in a way287* that would result in an inconsistent object graph.288*/289PARENT_UNABLE_TO_BE_SET: 'Unable to set parent component',290291/**292* Error when an attempt is made to add a child component at an out-of-bounds293* index. We don't support sparse child arrays.294*/295CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds',296297/**298* Error when an attempt is made to remove a child component from a component299* other than its parent.300*/301NOT_OUR_CHILD: 'Child is not in parent component',302303/**304* Error when an operation requiring DOM interaction is made when the305* component is not in the document306*/307NOT_IN_DOCUMENT: 'Operation not supported while component is not in document',308309/**310* Error when an invalid component state is encountered.311*/312STATE_INVALID: 'Invalid component state'313};314315316/**317* Common component states. Components may have distinct appearance depending318* on what state(s) apply to them. Not all components are expected to support319* all states.320* @enum {number}321*/322goog.ui.Component.State = {323/**324* Union of all supported component states.325*/326ALL: 0xFF,327328/**329* Component is disabled.330* @see goog.ui.Component.EventType.DISABLE331* @see goog.ui.Component.EventType.ENABLE332*/333DISABLED: 0x01,334335/**336* Component is highlighted.337* @see goog.ui.Component.EventType.HIGHLIGHT338* @see goog.ui.Component.EventType.UNHIGHLIGHT339*/340HOVER: 0x02,341342/**343* Component is active (or "pressed").344* @see goog.ui.Component.EventType.ACTIVATE345* @see goog.ui.Component.EventType.DEACTIVATE346*/347ACTIVE: 0x04,348349/**350* Component is selected.351* @see goog.ui.Component.EventType.SELECT352* @see goog.ui.Component.EventType.UNSELECT353*/354SELECTED: 0x08,355356/**357* Component is checked.358* @see goog.ui.Component.EventType.CHECK359* @see goog.ui.Component.EventType.UNCHECK360*/361CHECKED: 0x10,362363/**364* Component has focus.365* @see goog.ui.Component.EventType.FOCUS366* @see goog.ui.Component.EventType.BLUR367*/368FOCUSED: 0x20,369370/**371* Component is opened (expanded). Applies to tree nodes, menu buttons,372* submenus, zippys (zippies?), etc.373* @see goog.ui.Component.EventType.OPEN374* @see goog.ui.Component.EventType.CLOSE375*/376OPENED: 0x40377};378379380/**381* Static helper method; returns the type of event components are expected to382* dispatch when transitioning to or from the given state.383* @param {goog.ui.Component.State} state State to/from which the component384* is transitioning.385* @param {boolean} isEntering Whether the component is entering or leaving the386* state.387* @return {goog.ui.Component.EventType} Event type to dispatch.388*/389goog.ui.Component.getStateTransitionEvent = function(state, isEntering) {390'use strict';391switch (state) {392case goog.ui.Component.State.DISABLED:393return isEntering ? goog.ui.Component.EventType.DISABLE :394goog.ui.Component.EventType.ENABLE;395case goog.ui.Component.State.HOVER:396return isEntering ? goog.ui.Component.EventType.HIGHLIGHT :397goog.ui.Component.EventType.UNHIGHLIGHT;398case goog.ui.Component.State.ACTIVE:399return isEntering ? goog.ui.Component.EventType.ACTIVATE :400goog.ui.Component.EventType.DEACTIVATE;401case goog.ui.Component.State.SELECTED:402return isEntering ? goog.ui.Component.EventType.SELECT :403goog.ui.Component.EventType.UNSELECT;404case goog.ui.Component.State.CHECKED:405return isEntering ? goog.ui.Component.EventType.CHECK :406goog.ui.Component.EventType.UNCHECK;407case goog.ui.Component.State.FOCUSED:408return isEntering ? goog.ui.Component.EventType.FOCUS :409goog.ui.Component.EventType.BLUR;410case goog.ui.Component.State.OPENED:411return isEntering ? goog.ui.Component.EventType.OPEN :412goog.ui.Component.EventType.CLOSE;413default:414// Fall through.415}416417// Invalid state.418throw new Error(goog.ui.Component.Error.STATE_INVALID);419};420421422/**423* Set the default right-to-left value. This causes all component's created from424* this point forward to have the given value. This is useful for cases where425* a given page is always in one directionality, avoiding unnecessary426* right to left determinations.427* @param {?boolean} rightToLeft Whether the components should be rendered428* right-to-left. Null iff components should determine their directionality.429*/430goog.ui.Component.setDefaultRightToLeft = function(rightToLeft) {431'use strict';432goog.ui.Component.defaultRightToLeft_ = rightToLeft;433};434435436/**437* Gets the unique ID for the instance of this component. If the instance438* doesn't already have an ID, generates one on the fly.439* @return {string} Unique component ID.440*/441goog.ui.Component.prototype.getId = function() {442'use strict';443return this.id_ || (this.id_ = this.idGenerator_.getNextUniqueId());444};445446447/**448* Assigns an ID to this component instance. It is the caller's responsibility449* to guarantee that the ID is unique. If the component is a child of a parent450* component, then the parent component's child index is updated to reflect the451* new ID; this may throw an error if the parent already has a child with an ID452* that conflicts with the new ID.453* @param {string} id Unique component ID.454*/455goog.ui.Component.prototype.setId = function(id) {456'use strict';457if (this.parent_ && this.parent_.childIndex_) {458// Update the parent's child index.459goog.object.remove(this.parent_.childIndex_, this.id_);460goog.object.add(this.parent_.childIndex_, id, this);461}462463// Update the component ID.464this.id_ = id;465};466467468/**469* Gets the component's element.470* @return {?Element} The element for the component.471*/472goog.ui.Component.prototype.getElement = function() {473'use strict';474return this.element_;475};476477478/**479* Gets the component's element. This differs from getElement in that480* it assumes that the element exists (i.e. the component has been481* rendered/decorated) and will cause an assertion error otherwise (if482* assertion is enabled).483* @return {!Element} The element for the component.484*/485goog.ui.Component.prototype.getElementStrict = function() {486'use strict';487var el = this.element_;488goog.asserts.assert(489el, 'Can not call getElementStrict before rendering/decorating.');490return el;491};492493494/**495* Sets the component's root element to the given element. Considered496* protected and final.497*498* This should generally only be called during createDom. Setting the element499* does not actually change which element is rendered, only the element that is500* associated with this UI component.501*502* This should only be used by subclasses and its associated renderers.503*504* @param {Element} element Root element for the component.505*/506goog.ui.Component.prototype.setElementInternal = function(element) {507'use strict';508this.element_ = element;509};510511512/**513* Returns an array of all the elements in this component's DOM with the514* provided className.515* @param {string} className The name of the class to look for.516* @return {!IArrayLike<!Element>} The items found with the class name provided.517*/518goog.ui.Component.prototype.getElementsByClass = function(className) {519'use strict';520return this.element_ ?521this.dom_.getElementsByClass(className, this.element_) :522[];523};524525526/**527* Returns the first element in this component's DOM with the provided528* className.529* @param {string} className The name of the class to look for.530* @return {Element} The first item with the class name provided.531*/532goog.ui.Component.prototype.getElementByClass = function(className) {533'use strict';534return this.element_ ? this.dom_.getElementByClass(className, this.element_) :535null;536};537538539/**540* Similar to `getElementByClass` except that it expects the541* element to be present in the dom thus returning a required value. Otherwise,542* will assert.543* @param {string} className The name of the class to look for.544* @return {!Element} The first item with the class name provided.545*/546goog.ui.Component.prototype.getRequiredElementByClass = function(className) {547'use strict';548var el = this.getElementByClass(className);549goog.asserts.assert(550el, 'Expected element in component with class: %s', className);551return el;552};553554555/**556* Returns the event handler for this component, lazily created the first time557* this method is called.558* @return {!goog.events.EventHandler<T>} Event handler for this component.559* @protected560* @this {T}561* @template T562*/563goog.ui.Component.prototype.getHandler = function() {564'use strict';565// TODO(user): templated "this" values currently result in "this" being566// "unknown" in the body of the function.567var self = /** @type {goog.ui.Component} */ (this);568if (!self.googUiComponentHandler_) {569self.googUiComponentHandler_ = new goog.events.EventHandler(self);570}571return goog.asserts.assert(self.googUiComponentHandler_);572};573574575/**576* Sets the parent of this component to use for event bubbling. Throws an error577* if the component already has a parent or if an attempt is made to add a578* component to itself as a child. Callers must use `removeChild`579* or `removeChildAt` to remove components from their containers before580* calling this method.581* @see goog.ui.Component#removeChild582* @see goog.ui.Component#removeChildAt583* @param {goog.ui.Component} parent The parent component.584*/585goog.ui.Component.prototype.setParent = function(parent) {586'use strict';587if (this == parent) {588// Attempting to add a child to itself is an error.589throw new Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);590}591592if (parent && this.parent_ && this.id_ && this.parent_.getChild(this.id_) &&593this.parent_ != parent) {594// This component is already the child of some parent, so it should be595// removed using removeChild/removeChildAt first.596throw new Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);597}598599this.parent_ = parent;600goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);601};602603604/**605* Returns the component's parent, if any.606* @return {goog.ui.Component?} The parent component.607*/608goog.ui.Component.prototype.getParent = function() {609'use strict';610return this.parent_;611};612613614/**615* Overrides {@link goog.events.EventTarget#setParentEventTarget} to throw an616* error if the parent component is set, and the argument is not the parent.617* @override618*/619goog.ui.Component.prototype.setParentEventTarget = function(parent) {620'use strict';621if (this.parent_ && this.parent_ != parent) {622throw new Error(goog.ui.Component.Error.NOT_SUPPORTED);623}624goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);625};626627628/**629* Returns the dom helper that is being used on this component.630* @return {!goog.dom.DomHelper} The dom helper used on this component.631*/632goog.ui.Component.prototype.getDomHelper = function() {633'use strict';634return this.dom_;635};636637638/**639* Determines whether the component has been added to the document.640* @return {boolean} TRUE if rendered. Otherwise, FALSE.641*/642goog.ui.Component.prototype.isInDocument = function() {643'use strict';644return this.inDocument_;645};646647648/**649* Creates the initial DOM representation for the component. The default650* implementation is to set this.element_ = div.651*/652goog.ui.Component.prototype.createDom = function() {653'use strict';654this.element_ = this.dom_.createElement(goog.dom.TagName.DIV);655};656657658/**659* Renders the component. If a parent element is supplied, the component's660* element will be appended to it. If there is no optional parent element and661* the element doesn't have a parentNode then it will be appended to the662* document body.663*664* If this component has a parent component, and the parent component is665* not in the document already, then this will not call `enterDocument`666* on this component.667*668* Throws an Error if the component is already rendered.669*670* @param {Element=} opt_parentElement Optional parent element to render the671* component into.672*/673goog.ui.Component.prototype.render = function(opt_parentElement) {674'use strict';675this.render_(opt_parentElement);676};677678679/**680* Renders the component before another element. The other element should be in681* the document already.682*683* Throws an Error if the component is already rendered.684*685* @param {Node} sibling Node to render the component before.686*/687goog.ui.Component.prototype.renderBefore = function(sibling) {688'use strict';689this.render_(/** @type {Element} */ (sibling.parentNode), sibling);690};691692693/**694* Renders the component. If a parent element is supplied, the component's695* element will be appended to it. If there is no optional parent element and696* the element doesn't have a parentNode then it will be appended to the697* document body.698*699* If this component has a parent component, and the parent component is700* not in the document already, then this will not call `enterDocument`701* on this component.702*703* Throws an Error if the component is already rendered.704*705* @param {Element=} opt_parentElement Optional parent element to render the706* component into.707* @param {Node=} opt_beforeNode Node before which the component is to708* be rendered. If left out the node is appended to the parent element.709* @private710*/711goog.ui.Component.prototype.render_ = function(712opt_parentElement, opt_beforeNode) {713'use strict';714if (this.inDocument_) {715throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);716}717718if (!this.element_) {719this.createDom();720}721722if (opt_parentElement) {723opt_parentElement.insertBefore(724/** @type {!Node} */ (this.element_), opt_beforeNode || null);725} else {726this.dom_.getDocument().body.appendChild(727/** @type {!Node} */ (this.element_));728}729730// If this component has a parent component that isn't in the document yet,731// we don't call enterDocument() here. Instead, when the parent component732// enters the document, the enterDocument() call will propagate to its733// children, including this one. If the component doesn't have a parent734// or if the parent is already in the document, we call enterDocument().735if (!this.parent_ || this.parent_.isInDocument()) {736this.enterDocument();737}738};739740741/**742* Decorates the element for the UI component. If the element is in the743* document, the enterDocument method will be called.744*745* If goog.ui.Component.ALLOW_DETACHED_DECORATION is false, the caller must746* pass an element that is in the document.747*748* @param {Element} element Element to decorate.749*/750goog.ui.Component.prototype.decorate = function(element) {751'use strict';752if (this.inDocument_) {753throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);754} else if (element && this.canDecorate(element)) {755this.wasDecorated_ = true;756757// Set the DOM helper of the component to match the decorated element.758var doc = goog.dom.getOwnerDocument(element);759if (!this.dom_ || this.dom_.getDocument() != doc) {760this.dom_ = goog.dom.getDomHelper(element);761}762763// Call specific component decorate logic.764this.decorateInternal(element);765766// If supporting detached decoration, check that element is in doc.767if (!goog.ui.Component.ALLOW_DETACHED_DECORATION ||768goog.dom.contains(doc, element)) {769this.enterDocument();770}771} else {772throw new Error(goog.ui.Component.Error.DECORATE_INVALID);773}774};775776777/**778* Determines if a given element can be decorated by this type of component.779* This method should be overridden by inheriting objects.780* @param {Element} element Element to decorate.781* @return {boolean} True if the element can be decorated, false otherwise.782*/783goog.ui.Component.prototype.canDecorate = function(element) {784'use strict';785return true;786};787788789/**790* @return {boolean} Whether the component was decorated.791*/792goog.ui.Component.prototype.wasDecorated = function() {793'use strict';794return this.wasDecorated_;795};796797798/**799* Actually decorates the element. Should be overridden by inheriting objects.800* This method can assume there are checks to ensure the component has not801* already been rendered have occurred and that enter document will be called802* afterwards. This method is considered protected.803* @param {Element} element Element to decorate.804* @protected805*/806goog.ui.Component.prototype.decorateInternal = function(element) {807'use strict';808this.element_ = element;809};810811812/**813* Called when the component's element is known to be in the document. Anything814* using document.getElementById etc. should be done at this stage.815*816* If the component contains child components, this call is propagated to its817* children.818*/819goog.ui.Component.prototype.enterDocument = function() {820'use strict';821this.inDocument_ = true;822823// Propagate enterDocument to child components that have a DOM, if any.824// If a child was decorated before entering the document (permitted when825// goog.ui.Component.ALLOW_DETACHED_DECORATION is true), its enterDocument826// will be called here.827this.forEachChild(function(child) {828'use strict';829if (!child.isInDocument() && child.getElement()) {830child.enterDocument();831}832});833};834835836/**837* Called by dispose to clean up the elements and listeners created by a838* component, or by a parent component/application who has removed the839* component from the document but wants to reuse it later.840*841* If the component contains child components, this call is propagated to its842* children.843*844* It should be possible for the component to be rendered again once this method845* has been called.846*/847goog.ui.Component.prototype.exitDocument = function() {848'use strict';849// Propagate exitDocument to child components that have been rendered, if any.850this.forEachChild(function(child) {851'use strict';852if (child.isInDocument()) {853child.exitDocument();854}855});856857if (this.googUiComponentHandler_) {858this.googUiComponentHandler_.removeAll();859}860861this.inDocument_ = false;862};863864865/**866* Disposes of the component. Calls `exitDocument`, which is expected to867* remove event handlers and clean up the component. Propagates the call to868* the component's children, if any. Removes the component's DOM from the869* document unless it was decorated.870* @override871* @protected872*/873goog.ui.Component.prototype.disposeInternal = function() {874'use strict';875if (this.inDocument_) {876this.exitDocument();877}878879if (this.googUiComponentHandler_) {880this.googUiComponentHandler_.dispose();881delete this.googUiComponentHandler_;882}883884// Disposes of the component's children, if any.885this.forEachChild(function(child) {886'use strict';887child.dispose();888});889890// Detach the component's element from the DOM, unless it was decorated.891if (!this.wasDecorated_ && this.element_) {892goog.dom.removeNode(this.element_);893}894895this.children_ = null;896this.childIndex_ = null;897this.element_ = null;898this.model_ = null;899this.parent_ = null;900901goog.ui.Component.superClass_.disposeInternal.call(this);902};903904905/**906* Helper function for subclasses that gets a unique id for a given fragment,907* this can be used by components to generate unique string ids for DOM908* elements.909* @param {string} idFragment A partial id.910* @return {string} Unique element id.911*/912goog.ui.Component.prototype.makeId = function(idFragment) {913'use strict';914return this.getId() + '.' + idFragment;915};916917918/**919* Makes a collection of ids. This is a convenience method for makeId. The920* object's values are the id fragments and the new values are the generated921* ids. The key will remain the same.922* @param {Object} object The object that will be used to create the ids.923* @return {!Object<string, string>} An object of id keys to generated ids.924*/925goog.ui.Component.prototype.makeIds = function(object) {926'use strict';927var ids = {};928for (var key in object) {929ids[key] = this.makeId(object[key]);930}931return ids;932};933934935/**936* Returns the model associated with the UI component.937* @return {*} The model.938*/939goog.ui.Component.prototype.getModel = function() {940'use strict';941return this.model_;942};943944945/**946* Sets the model associated with the UI component.947* @param {*} obj The model.948*/949goog.ui.Component.prototype.setModel = function(obj) {950'use strict';951this.model_ = obj;952};953954955/**956* Helper function for returning the fragment portion of an id generated using957* makeId().958* @param {string} id Id generated with makeId().959* @return {string} Fragment.960*/961goog.ui.Component.prototype.getFragmentFromId = function(id) {962'use strict';963return id.substring(this.getId().length + 1);964};965966967/**968* Helper function for returning an element in the document with a unique id969* generated using makeId().970* @param {string} idFragment The partial id.971* @return {Element} The element with the unique id, or null if it cannot be972* found.973*/974goog.ui.Component.prototype.getElementByFragment = function(idFragment) {975'use strict';976if (!this.inDocument_) {977throw new Error(goog.ui.Component.Error.NOT_IN_DOCUMENT);978}979return this.dom_.getElement(this.makeId(idFragment));980};981982983/**984* Adds the specified component as the last child of this component. See985* {@link goog.ui.Component#addChildAt} for detailed semantics.986*987* @see goog.ui.Component#addChildAt988* @param {goog.ui.Component} child The new child component.989* @param {boolean=} opt_render If true, the child component will be rendered990* into the parent.991*/992goog.ui.Component.prototype.addChild = function(child, opt_render) {993'use strict';994// TODO(gboyer): addChildAt(child, this.getChildCount(), false) will995// reposition any already-rendered child to the end. Instead, perhaps996// addChild(child, false) should never reposition the child; instead, clients997// that need the repositioning will use addChildAt explicitly. Right now,998// clients can get around this by calling addChild before calling decorate.999this.addChildAt(child, this.getChildCount(), opt_render);1000};100110021003/**1004* Adds the specified component as a child of this component at the given1005* 0-based index.1006*1007* Both `addChild` and `addChildAt` assume the following contract1008* between parent and child components:1009* <ul>1010* <li>the child component's element must be a descendant of the parent1011* component's element, and1012* <li>the DOM state of the child component must be consistent with the DOM1013* state of the parent component (see `isInDocument`) in the1014* steady state -- the exception is to addChildAt(child, i, false) and1015* then immediately decorate/render the child.1016* </ul>1017*1018* In particular, `parent.addChild(child)` will throw an error if the1019* child component is already in the document, but the parent isn't.1020*1021* Clients of this API may call `addChild` and `addChildAt` with1022* `opt_render` set to true. If `opt_render` is true, calling these1023* methods will automatically render the child component's element into the1024* parent component's element. If the parent does not yet have an element, then1025* `createDom` will automatically be invoked on the parent before1026* rendering the child.1027*1028* Invoking {@code parent.addChild(child, true)} will throw an error if the1029* child component is already in the document, regardless of the parent's DOM1030* state.1031*1032* If `opt_render` is true and the parent component is not already1033* in the document, `enterDocument` will not be called on this component1034* at this point.1035*1036* Finally, this method also throws an error if the new child already has a1037* different parent, or the given index is out of bounds.1038*1039* @see goog.ui.Component#addChild1040* @param {goog.ui.Component} child The new child component.1041* @param {number} index 0-based index at which the new child component is to be1042* added; must be between 0 and the current child count (inclusive).1043* @param {boolean=} opt_render If true, the child component will be rendered1044* into the parent.1045* @return {void} Nada.1046*/1047goog.ui.Component.prototype.addChildAt = function(child, index, opt_render) {1048'use strict';1049goog.asserts.assert(!!child, 'Provided element must not be null.');10501051if (child.inDocument_ && (opt_render || !this.inDocument_)) {1052// Adding a child that's already in the document is an error, except if the1053// parent is also in the document and opt_render is false (e.g. decorate()).1054throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);1055}10561057if (index < 0 || index > this.getChildCount()) {1058// Allowing sparse child arrays would lead to strange behavior, so we don't.1059throw new Error(goog.ui.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS);1060}10611062// Create the index and the child array on first use.1063if (!this.childIndex_ || !this.children_) {1064this.childIndex_ = {};1065this.children_ = [];1066}10671068// Moving child within component, remove old reference.1069if (child.getParent() == this) {1070goog.object.set(this.childIndex_, child.getId(), child);1071goog.array.remove(this.children_, child);10721073// Add the child to this component. goog.object.add() throws an error if1074// a child with the same ID already exists.1075} else {1076goog.object.add(this.childIndex_, child.getId(), child);1077}10781079// Set the parent of the child to this component. This throws an error if1080// the child is already contained by another component.1081child.setParent(this);1082goog.array.insertAt(this.children_, child, index);10831084if (child.inDocument_ && this.inDocument_ && child.getParent() == this) {1085// Changing the position of an existing child, move the DOM node (if1086// necessary).1087var contentElement = this.getContentElement();1088var elementAtDestinationIndex = contentElement.childNodes[index] || null;1089// Don't move the node if it's already in the destination index.1090if (elementAtDestinationIndex != child.getElement()) {1091// We remove the node before calculating the new index, otherwise we get1092// an off-by-one error when we move it to the right of its current index.1093if (child.getElement().parentElement == contentElement) {1094contentElement.removeChild(child.getElement());1095}1096var insertBeforeElement = contentElement.childNodes[index] || null;1097contentElement.insertBefore(1098/** @type {!Node} */ (child.getElement()), insertBeforeElement);1099}1100} else if (opt_render) {1101// If this (parent) component doesn't have a DOM yet, call createDom now1102// to make sure we render the child component's element into the correct1103// parent element (otherwise render_ with a null first argument would1104// render the child into the document body, which is almost certainly not1105// what we want).1106if (!this.element_) {1107this.createDom();1108}1109// Render the child into the parent at the appropriate location. Note that1110// getChildAt(index + 1) returns undefined if inserting at the end.1111// TODO(attila): We should have a renderer with a renderChildAt API.1112var sibling = this.getChildAt(index + 1);1113// render_() calls enterDocument() if the parent is already in the document.1114child.render_(this.getContentElement(), sibling ? sibling.element_ : null);1115} else if (1116this.inDocument_ && !child.inDocument_ && child.element_ &&1117child.element_.parentNode &&1118// Under some circumstances, IE8 implicitly creates a Document Fragment1119// for detached nodes, so ensure the parent is an Element as it should be.1120child.element_.parentNode.nodeType == goog.dom.NodeType.ELEMENT) {1121// We don't touch the DOM, but if the parent is in the document, and the1122// child element is in the document but not marked as such, then we call1123// enterDocument on the child.1124// TODO(gboyer): It would be nice to move this condition entirely, but1125// there's a large risk of breaking existing applications that manually1126// append the child to the DOM and then call addChild.1127child.enterDocument();1128}1129};113011311132/**1133* Returns the DOM element into which child components are to be rendered,1134* or null if the component itself hasn't been rendered yet. This default1135* implementation returns the component's root element. Subclasses with1136* complex DOM structures must override this method.1137* @return {Element} Element to contain child elements (null if none).1138*/1139goog.ui.Component.prototype.getContentElement = function() {1140'use strict';1141return this.element_;1142};114311441145/**1146* Returns true if the component is rendered right-to-left, false otherwise.1147* The first time this function is invoked, the right-to-left rendering property1148* is set if it has not been already.1149* @return {boolean} Whether the control is rendered right-to-left.1150*/1151goog.ui.Component.prototype.isRightToLeft = function() {1152'use strict';1153if (this.rightToLeft_ == null) {1154this.rightToLeft_ = goog.style.isRightToLeft(1155this.inDocument_ ? this.element_ : this.dom_.getDocument().body);1156}1157return this.rightToLeft_;1158};115911601161/**1162* Set is right-to-left. This function should be used if the component needs1163* to know the rendering direction during dom creation (i.e. before1164* {@link #enterDocument} is called and is right-to-left is set).1165* @param {boolean} rightToLeft Whether the component is rendered1166* right-to-left.1167*/1168goog.ui.Component.prototype.setRightToLeft = function(rightToLeft) {1169'use strict';1170if (this.inDocument_) {1171throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);1172}1173this.rightToLeft_ = rightToLeft;1174};117511761177/**1178* Returns true if the component has children.1179* @return {boolean} True if the component has children.1180*/1181goog.ui.Component.prototype.hasChildren = function() {1182'use strict';1183return !!this.children_ && this.children_.length != 0;1184};118511861187/**1188* Returns the number of children of this component.1189* @return {number} The number of children.1190*/1191goog.ui.Component.prototype.getChildCount = function() {1192'use strict';1193return this.children_ ? this.children_.length : 0;1194};119511961197/**1198* Returns an array containing the IDs of the children of this component, or an1199* empty array if the component has no children.1200* @return {!Array<string>} Child component IDs.1201*/1202goog.ui.Component.prototype.getChildIds = function() {1203'use strict';1204var ids = [];12051206// We don't use goog.object.getKeys(this.childIndex_) because we want to1207// return the IDs in the correct order as determined by this.children_.1208this.forEachChild(function(child) {1209'use strict';1210// addChild()/addChildAt() guarantee that the child array isn't sparse.1211ids.push(child.getId());1212});12131214return ids;1215};121612171218/**1219* Returns the child with the given ID, or null if no such child exists.1220* @param {string} id Child component ID.1221* @return {goog.ui.Component?} The child with the given ID; null if none.1222*/1223goog.ui.Component.prototype.getChild = function(id) {1224'use strict';1225// Use childIndex_ for O(1) access by ID.1226return (this.childIndex_ && id) ?1227/** @type {goog.ui.Component} */ (1228goog.object.get(this.childIndex_, id)) ||1229null :1230null;1231};123212331234/**1235* Returns the child at the given index, or null if the index is out of bounds.1236* @param {number} index 0-based index.1237* @return {goog.ui.Component?} The child at the given index; null if none.1238*/1239goog.ui.Component.prototype.getChildAt = function(index) {1240'use strict';1241// Use children_ for access by index.1242return this.children_ ? this.children_[index] || null : null;1243};124412451246/**1247* Calls the given function on each of this component's children in order. If1248* `opt_obj` is provided, it will be used as the 'this' object in the1249* function when called. The function should take two arguments: the child1250* component and its 0-based index. The return value is ignored.1251* @param {function(this:T,?,number):?} f The function to call for every1252* child component; should take 2 arguments (the child and its index).1253* @param {T=} opt_obj Used as the 'this' object in f when called.1254* @template T1255*/1256goog.ui.Component.prototype.forEachChild = function(f, opt_obj) {1257'use strict';1258if (this.children_) {1259this.children_.forEach(f, opt_obj);1260}1261};126212631264/**1265* Returns the 0-based index of the given child component, or -1 if no such1266* child is found.1267* @param {goog.ui.Component?} child The child component.1268* @return {number} 0-based index of the child component; -1 if not found.1269*/1270goog.ui.Component.prototype.indexOfChild = function(child) {1271'use strict';1272return (this.children_ && child) ? this.children_.indexOf(child) : -1;1273};127412751276/**1277* Removes the given child from this component, and returns it. Throws an error1278* if the argument is invalid or if the specified child isn't found in the1279* parent component. The argument can either be a string (interpreted as the1280* ID of the child component to remove) or the child component itself.1281*1282* If `opt_unrender` is true, calls {@link goog.ui.component#exitDocument}1283* on the removed child, and subsequently detaches the child's DOM from the1284* document. Otherwise it is the caller's responsibility to clean up the child1285* component's DOM.1286*1287* @see goog.ui.Component#removeChildAt1288* @param {string|goog.ui.Component|null} child The ID of the child to remove,1289* or the child component itself.1290* @param {boolean=} opt_unrender If true, calls `exitDocument` on the1291* removed child component, and detaches its DOM from the document.1292* @return {?goog.ui.Component} The removed component, if any.1293*/1294goog.ui.Component.prototype.removeChild = function(child, opt_unrender) {1295'use strict';1296if (child) {1297// Normalize child to be the object and id to be the ID string. This also1298// ensures that the child is really ours.1299var id = (typeof child === 'string') ? child : child.getId();1300child = this.getChild(id);13011302if (id && child) {1303goog.object.remove(this.childIndex_, id);1304goog.array.remove(this.children_, child);13051306if (opt_unrender) {1307// Remove the child component's DOM from the document. We have to call1308// exitDocument first (see documentation).1309child.exitDocument();1310if (child.element_) {1311goog.dom.removeNode(child.element_);1312}1313}13141315// Child's parent must be set to null after exitDocument is called1316// so that the child can unlisten to its parent if required.1317child.setParent(null);1318}1319}13201321if (!child) {1322throw new Error(goog.ui.Component.Error.NOT_OUR_CHILD);1323}13241325return /** @type {!goog.ui.Component} */ (child);1326};132713281329/**1330* Removes the child at the given index from this component, and returns it.1331* Throws an error if the argument is out of bounds, or if the specified child1332* isn't found in the parent. See {@link goog.ui.Component#removeChild} for1333* detailed semantics.1334*1335* @see goog.ui.Component#removeChild1336* @param {number} index 0-based index of the child to remove.1337* @param {boolean=} opt_unrender If true, calls `exitDocument` on the1338* removed child component, and detaches its DOM from the document.1339* @return {goog.ui.Component} The removed component, if any.1340*/1341goog.ui.Component.prototype.removeChildAt = function(index, opt_unrender) {1342'use strict';1343// removeChild(null) will throw error.1344return this.removeChild(this.getChildAt(index), opt_unrender);1345};134613471348/**1349* Removes every child component attached to this one and returns them.1350*1351* @see goog.ui.Component#removeChild1352* @param {boolean=} opt_unrender If true, calls {@link #exitDocument} on the1353* removed child components, and detaches their DOM from the document.1354* @return {!Array<goog.ui.Component>} The removed components if any.1355*/1356goog.ui.Component.prototype.removeChildren = function(opt_unrender) {1357'use strict';1358var removedChildren = [];1359while (this.hasChildren()) {1360removedChildren.push(this.removeChildAt(0, opt_unrender));1361}1362return removedChildren;1363};136413651366/**1367* Returns whether this component should listen for PointerEvent types rather1368* than MouseEvent types. This allows supporting drag gestures for touch/stylus1369* input.1370* @return {boolean}1371*/1372goog.ui.Component.prototype.pointerEventsEnabled = function() {1373'use strict';1374return this.pointerEventsEnabled_;1375};137613771378/**1379* Indicates whether this component should listen for PointerEvent types rather1380* than MouseEvent types. This allows supporting drag gestures for touch/stylus1381* input. Must be called before enterDocument to listen for the correct event1382* types.1383* @param {boolean} enable1384*/1385goog.ui.Component.prototype.setPointerEventsEnabled = function(enable) {1386'use strict';1387if (this.inDocument_) {1388throw new Error(goog.ui.Component.Error.ALREADY_RENDERED);1389}1390this.pointerEventsEnabled_ = enable;1391};139213931394