123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302 |
- // Copyright 2007 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 Abstract class for all UI components. This defines the standard
- * design pattern that all UI components should follow.
- *
- * @author attila@google.com (Attila Bodis)
- * @see ../demos/samplecomponent.html
- * @see http://code.google.com/p/closure-library/wiki/IntroToComponents
- */
- 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.asserts');
- goog.require('goog.dom');
- goog.require('goog.dom.NodeType');
- goog.require('goog.dom.TagName');
- 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}
- * @suppress {underscore}
- */
- goog.ui.Component = function(opt_domHelper) {
- goog.events.EventTarget.call(this);
- /**
- * DomHelper used to interact with the document, allowing components to be
- * created in a different window.
- * @protected {!goog.dom.DomHelper}
- * @suppress {underscore|visibility}
- */
- this.dom_ = opt_domHelper || goog.dom.getDomHelper();
- /**
- * 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.
- * @private {?boolean}
- */
- this.rightToLeft_ = goog.ui.Component.defaultRightToLeft_;
- /**
- * 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!
- * @private {?string}
- */
- this.id_ = null;
- /**
- * Whether the component is in the document.
- * @private {boolean}
- */
- this.inDocument_ = false;
- // TODO(attila): Stop referring to this private field in subclasses.
- /**
- * The DOM element for the component.
- * @private {Element}
- */
- this.element_ = null;
- /**
- * Event handler.
- * TODO(user): rename it to handler_ after all component subclasses in
- * inside Google have been cleaned up.
- * Code search: http://go/component_code_search
- * @private {goog.events.EventHandler|undefined}
- */
- this.googUiComponentHandler_ = void 0;
- /**
- * Arbitrary data object associated with the component. Such as meta-data.
- * @private {*}
- */
- this.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!
- * @private {goog.ui.Component?}
- */
- this.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!
- * @private {Array<goog.ui.Component>?}
- */
- this.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.
- *
- * @private {Object}
- */
- this.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 is decorated, dispose will leave the node in the document.
- * It is up to the app to remove the node.
- *
- * If an element was rendered, dispose will remove the node automatically.
- *
- * @private {boolean}
- */
- this.wasDecorated_ = false;
- };
- goog.inherits(goog.ui.Component, goog.events.EventTarget);
- /**
- * @define {boolean} Whether to support calling decorate with an element that is
- * not yet in the document. If true, we check if the element is in the
- * document, and avoid calling enterDocument if it isn't. If false, we
- * maintain legacy behavior (always call enterDocument from decorate).
- */
- goog.define('goog.ui.Component.ALLOW_DETACHED_DECORATION', false);
- /**
- * Generator for unique IDs.
- * @type {goog.ui.IdGenerator}
- * @private
- */
- goog.ui.Component.prototype.idGenerator_ = goog.ui.IdGenerator.getInstance();
- // TODO(gboyer): See if we can remove this and just check goog.i18n.bidi.IS_RTL.
- /**
- * @define {number} Defines the default BIDI directionality.
- * 0: Unknown.
- * 1: Left-to-right.
- * -1: Right-to-left.
- */
- goog.define('goog.ui.Component.DEFAULT_BIDI_DIR', 0);
- /**
- * The default right to left value.
- * @type {?boolean}
- * @private
- */
- goog.ui.Component.defaultRightToLeft_ =
- (goog.ui.Component.DEFAULT_BIDI_DIR == 1) ?
- false :
- (goog.ui.Component.DEFAULT_BIDI_DIR == -1) ? true : 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(user): 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 forward 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;
- };
- /**
- * 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_;
- };
- /**
- * Gets the component's element. This differs from getElement in that
- * it assumes that the element exists (i.e. the component has been
- * rendered/decorated) and will cause an assertion error otherwise (if
- * assertion is enabled).
- * @return {!Element} The element for the component.
- */
- goog.ui.Component.prototype.getElementStrict = function() {
- var el = this.element_;
- goog.asserts.assert(
- el, 'Can not call getElementStrict before rendering/decorating.');
- return el;
- };
- /**
- * Sets the component's root element to the given element. Considered
- * protected and final.
- *
- * This should generally only be called during createDom. Setting the element
- * does not actually change which element is rendered, only the element that is
- * associated with this UI component.
- *
- * This should only be used by subclasses and its associated renderers.
- *
- * @param {Element} element Root element for the component.
- */
- goog.ui.Component.prototype.setElementInternal = function(element) {
- this.element_ = element;
- };
- /**
- * Returns an array of all the elements in this component's DOM with the
- * provided className.
- * @param {string} className The name of the class to look for.
- * @return {!IArrayLike<!Element>} The items found with the class name provided.
- */
- goog.ui.Component.prototype.getElementsByClass = function(className) {
- return this.element_ ?
- this.dom_.getElementsByClass(className, this.element_) :
- [];
- };
- /**
- * Returns the first element in this component's DOM with the provided
- * className.
- * @param {string} className The name of the class to look for.
- * @return {Element} The first item with the class name provided.
- */
- goog.ui.Component.prototype.getElementByClass = function(className) {
- return this.element_ ? this.dom_.getElementByClass(className, this.element_) :
- null;
- };
- /**
- * Similar to {@code getElementByClass} except that it expects the
- * element to be present in the dom thus returning a required value. Otherwise,
- * will assert.
- * @param {string} className The name of the class to look for.
- * @return {!Element} The first item with the class name provided.
- */
- goog.ui.Component.prototype.getRequiredElementByClass = function(className) {
- var el = this.getElementByClass(className);
- goog.asserts.assert(
- el, 'Expected element in component with class: %s', className);
- return el;
- };
- /**
- * Returns the event handler for this component, lazily created the first time
- * this method is called.
- * @return {!goog.events.EventHandler<T>} Event handler for this component.
- * @protected
- * @this {T}
- * @template T
- */
- goog.ui.Component.prototype.getHandler = function() {
- // TODO(user): templated "this" values currently result in "this" being
- // "unknown" in the body of the function.
- var self = /** @type {goog.ui.Component} */ (this);
- if (!self.googUiComponentHandler_) {
- self.googUiComponentHandler_ = new goog.events.EventHandler(self);
- }
- return self.googUiComponentHandler_;
- };
- /**
- * 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.
- * @override
- */
- 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(goog.dom.TagName.DIV);
- };
- /**
- * Renders the component. If a parent element is supplied, 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.
- *
- * If this component has a parent component, and the parent component is
- * not in the document already, then this will not call {@code enterDocument}
- * on this component.
- *
- * 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 {Node} sibling Node to render the component before.
- */
- goog.ui.Component.prototype.renderBefore = function(sibling) {
- this.render_(/** @type {Element} */ (sibling.parentNode), sibling);
- };
- /**
- * Renders the component. If a parent element is supplied, 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.
- *
- * If this component has a parent component, and the parent component is
- * not in the document already, then this will not call {@code enterDocument}
- * on this component.
- *
- * Throws an Error if the component is already rendered.
- *
- * @param {Element=} opt_parentElement Optional parent element to render the
- * component into.
- * @param {Node=} opt_beforeNode Node 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_beforeNode) {
- 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_beforeNode || 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. If the element is in the
- * document, the enterDocument method will be called.
- *
- * If goog.ui.Component.ALLOW_DETACHED_DECORATION is false, the caller must
- * pass an element that is in the document.
- *
- * @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.
- var doc = goog.dom.getOwnerDocument(element);
- if (!this.dom_ || this.dom_.getDocument() != doc) {
- this.dom_ = goog.dom.getDomHelper(element);
- }
- // Call specific component decorate logic.
- this.decorateInternal(element);
- // If supporting detached decoration, check that element is in doc.
- if (!goog.ui.Component.ALLOW_DETACHED_DECORATION ||
- goog.dom.contains(doc, 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.
- // If a child was decorated before entering the document (permitted when
- // goog.ui.Component.ALLOW_DETACHED_DECORATION is true), its enterDocument
- // will be called here.
- 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
- * @protected
- */
- goog.ui.Component.prototype.disposeInternal = function() {
- 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;
- goog.ui.Component.superClass_.disposeInternal.call(this);
- };
- /**
- * 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;
- };
- /**
- * Makes a collection of ids. This is a convenience method for makeId. The
- * object's values are the id fragments and the new values are the generated
- * ids. The key will remain the same.
- * @param {Object} object The object that will be used to create the ids.
- * @return {!Object<string, string>} An object of id keys to generated ids.
- */
- goog.ui.Component.prototype.makeIds = function(object) {
- var ids = {};
- for (var key in object) {
- ids[key] = this.makeId(object[key]);
- }
- return ids;
- };
- /**
- * 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) {
- // TODO(gboyer): addChildAt(child, this.getChildCount(), false) will
- // reposition any already-rendered child to the end. Instead, perhaps
- // addChild(child, false) should never reposition the child; instead, clients
- // that need the repositioning will use addChildAt explicitly. Right now,
- // clients can get around this by calling addChild before calling decorate.
- 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}) in the
- * steady state -- the exception is to addChildAt(child, i, false) and
- * then immediately decorate/render the child.
- * </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. If the parent does not yet have an element, then
- * {@code createDom} will automatically be invoked on the parent before
- * rendering the child.
- *
- * Invoking {@code parent.addChild(child, true)} will throw an error if the
- * child component is already in the document, regardless of the parent's DOM
- * state.
- *
- * If {@code opt_render} is true and the parent component is not already
- * in the document, {@code enterDocument} will not be called on this component
- * at this point.
- *
- * 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.
- * @return {void} Nada.
- */
- goog.ui.Component.prototype.addChildAt = function(child, index, opt_render) {
- goog.asserts.assert(!!child, 'Provided element must not be null.');
- 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 (if
- // necessary).
- var contentElement = this.getContentElement();
- var insertBeforeElement = contentElement.childNodes[index] || null;
- if (insertBeforeElement != child.getElement()) {
- contentElement.insertBefore(child.getElement(), insertBeforeElement);
- }
- } 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(attila): 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 if (
- this.inDocument_ && !child.inDocument_ && child.element_ &&
- child.element_.parentNode &&
- // Under some circumstances, IE8 implicitly creates a Document Fragment
- // for detached nodes, so ensure the parent is an Element as it should be.
- child.element_.parentNode.nodeType == goog.dom.NodeType.ELEMENT) {
- // We don't touch the DOM, but if the parent is in the document, and the
- // child element is in the document but not marked as such, then we call
- // enterDocument on the child.
- // TODO(gboyer): It would be nice to move this condition entirely, but
- // there's a large risk of breaking existing applications that manually
- // append the child to the DOM and then call addChild.
- 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 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(this:T,?,number):?} f The function to call for every
- * child component; should take 2 arguments (the child and its index).
- * @param {T=} opt_obj Used as the 'this' object in f when called.
- * @template T
- */
- 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 and returns them.
- *
- * @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.
- * @return {!Array<goog.ui.Component>} The removed components if any.
- */
- goog.ui.Component.prototype.removeChildren = function(opt_unrender) {
- var removedChildren = [];
- while (this.hasChildren()) {
- removedChildren.push(this.removeChildAt(0, opt_unrender));
- }
- return removedChildren;
- };
|