component.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Abstract class for all UI components. This defines the standard
  16. * design pattern that all UI components should follow.
  17. *
  18. * @author attila@google.com (Attila Bodis)
  19. * @see ../demos/samplecomponent.html
  20. * @see http://code.google.com/p/closure-library/wiki/IntroToComponents
  21. */
  22. goog.provide('goog.ui.Component');
  23. goog.provide('goog.ui.Component.Error');
  24. goog.provide('goog.ui.Component.EventType');
  25. goog.provide('goog.ui.Component.State');
  26. goog.require('goog.array');
  27. goog.require('goog.asserts');
  28. goog.require('goog.dom');
  29. goog.require('goog.dom.NodeType');
  30. goog.require('goog.dom.TagName');
  31. goog.require('goog.events.EventHandler');
  32. goog.require('goog.events.EventTarget');
  33. goog.require('goog.object');
  34. goog.require('goog.style');
  35. goog.require('goog.ui.IdGenerator');
  36. /**
  37. * Default implementation of UI component.
  38. *
  39. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  40. * @constructor
  41. * @extends {goog.events.EventTarget}
  42. * @suppress {underscore}
  43. */
  44. goog.ui.Component = function(opt_domHelper) {
  45. goog.events.EventTarget.call(this);
  46. /**
  47. * DomHelper used to interact with the document, allowing components to be
  48. * created in a different window.
  49. * @protected {!goog.dom.DomHelper}
  50. * @suppress {underscore|visibility}
  51. */
  52. this.dom_ = opt_domHelper || goog.dom.getDomHelper();
  53. /**
  54. * Whether the component is rendered right-to-left. Right-to-left is set
  55. * lazily when {@link #isRightToLeft} is called the first time, unless it has
  56. * been set by calling {@link #setRightToLeft} explicitly.
  57. * @private {?boolean}
  58. */
  59. this.rightToLeft_ = goog.ui.Component.defaultRightToLeft_;
  60. /**
  61. * Unique ID of the component, lazily initialized in {@link
  62. * goog.ui.Component#getId} if needed. This property is strictly private and
  63. * must not be accessed directly outside of this class!
  64. * @private {?string}
  65. */
  66. this.id_ = null;
  67. /**
  68. * Whether the component is in the document.
  69. * @private {boolean}
  70. */
  71. this.inDocument_ = false;
  72. // TODO(attila): Stop referring to this private field in subclasses.
  73. /**
  74. * The DOM element for the component.
  75. * @private {Element}
  76. */
  77. this.element_ = null;
  78. /**
  79. * Event handler.
  80. * TODO(user): rename it to handler_ after all component subclasses in
  81. * inside Google have been cleaned up.
  82. * Code search: http://go/component_code_search
  83. * @private {goog.events.EventHandler|undefined}
  84. */
  85. this.googUiComponentHandler_ = void 0;
  86. /**
  87. * Arbitrary data object associated with the component. Such as meta-data.
  88. * @private {*}
  89. */
  90. this.model_ = null;
  91. /**
  92. * Parent component to which events will be propagated. This property is
  93. * strictly private and must not be accessed directly outside of this class!
  94. * @private {goog.ui.Component?}
  95. */
  96. this.parent_ = null;
  97. /**
  98. * Array of child components. Lazily initialized on first use. Must be kept
  99. * in sync with {@code childIndex_}. This property is strictly private and
  100. * must not be accessed directly outside of this class!
  101. * @private {Array<goog.ui.Component>?}
  102. */
  103. this.children_ = null;
  104. /**
  105. * Map of child component IDs to child components. Used for constant-time
  106. * random access to child components by ID. Lazily initialized on first use.
  107. * Must be kept in sync with {@code children_}. This property is strictly
  108. * private and must not be accessed directly outside of this class!
  109. *
  110. * We use a plain Object, not a {@link goog.structs.Map}, for simplicity.
  111. * This means components can't have children with IDs such as 'constructor' or
  112. * 'valueOf', but this shouldn't really be an issue in practice, and if it is,
  113. * we can always fix it later without changing the API.
  114. *
  115. * @private {Object}
  116. */
  117. this.childIndex_ = null;
  118. /**
  119. * Flag used to keep track of whether a component decorated an already
  120. * existing element or whether it created the DOM itself.
  121. *
  122. * If an element is decorated, dispose will leave the node in the document.
  123. * It is up to the app to remove the node.
  124. *
  125. * If an element was rendered, dispose will remove the node automatically.
  126. *
  127. * @private {boolean}
  128. */
  129. this.wasDecorated_ = false;
  130. };
  131. goog.inherits(goog.ui.Component, goog.events.EventTarget);
  132. /**
  133. * @define {boolean} Whether to support calling decorate with an element that is
  134. * not yet in the document. If true, we check if the element is in the
  135. * document, and avoid calling enterDocument if it isn't. If false, we
  136. * maintain legacy behavior (always call enterDocument from decorate).
  137. */
  138. goog.define('goog.ui.Component.ALLOW_DETACHED_DECORATION', false);
  139. /**
  140. * Generator for unique IDs.
  141. * @type {goog.ui.IdGenerator}
  142. * @private
  143. */
  144. goog.ui.Component.prototype.idGenerator_ = goog.ui.IdGenerator.getInstance();
  145. // TODO(gboyer): See if we can remove this and just check goog.i18n.bidi.IS_RTL.
  146. /**
  147. * @define {number} Defines the default BIDI directionality.
  148. * 0: Unknown.
  149. * 1: Left-to-right.
  150. * -1: Right-to-left.
  151. */
  152. goog.define('goog.ui.Component.DEFAULT_BIDI_DIR', 0);
  153. /**
  154. * The default right to left value.
  155. * @type {?boolean}
  156. * @private
  157. */
  158. goog.ui.Component.defaultRightToLeft_ =
  159. (goog.ui.Component.DEFAULT_BIDI_DIR == 1) ?
  160. false :
  161. (goog.ui.Component.DEFAULT_BIDI_DIR == -1) ? true : null;
  162. /**
  163. * Common events fired by components so that event propagation is useful. Not
  164. * all components are expected to dispatch or listen for all event types.
  165. * Events dispatched before a state transition should be cancelable to prevent
  166. * the corresponding state change.
  167. * @enum {string}
  168. */
  169. goog.ui.Component.EventType = {
  170. /** Dispatched before the component becomes visible. */
  171. BEFORE_SHOW: 'beforeshow',
  172. /**
  173. * Dispatched after the component becomes visible.
  174. * NOTE(user): For goog.ui.Container, this actually fires before containers
  175. * are shown. Use goog.ui.Container.EventType.AFTER_SHOW if you want an event
  176. * that fires after a goog.ui.Container is shown.
  177. */
  178. SHOW: 'show',
  179. /** Dispatched before the component becomes hidden. */
  180. HIDE: 'hide',
  181. /** Dispatched before the component becomes disabled. */
  182. DISABLE: 'disable',
  183. /** Dispatched before the component becomes enabled. */
  184. ENABLE: 'enable',
  185. /** Dispatched before the component becomes highlighted. */
  186. HIGHLIGHT: 'highlight',
  187. /** Dispatched before the component becomes un-highlighted. */
  188. UNHIGHLIGHT: 'unhighlight',
  189. /** Dispatched before the component becomes activated. */
  190. ACTIVATE: 'activate',
  191. /** Dispatched before the component becomes deactivated. */
  192. DEACTIVATE: 'deactivate',
  193. /** Dispatched before the component becomes selected. */
  194. SELECT: 'select',
  195. /** Dispatched before the component becomes un-selected. */
  196. UNSELECT: 'unselect',
  197. /** Dispatched before a component becomes checked. */
  198. CHECK: 'check',
  199. /** Dispatched before a component becomes un-checked. */
  200. UNCHECK: 'uncheck',
  201. /** Dispatched before a component becomes focused. */
  202. FOCUS: 'focus',
  203. /** Dispatched before a component becomes blurred. */
  204. BLUR: 'blur',
  205. /** Dispatched before a component is opened (expanded). */
  206. OPEN: 'open',
  207. /** Dispatched before a component is closed (collapsed). */
  208. CLOSE: 'close',
  209. /** Dispatched after a component is moused over. */
  210. ENTER: 'enter',
  211. /** Dispatched after a component is moused out of. */
  212. LEAVE: 'leave',
  213. /** Dispatched after the user activates the component. */
  214. ACTION: 'action',
  215. /** Dispatched after the external-facing state of a component is changed. */
  216. CHANGE: 'change'
  217. };
  218. /**
  219. * Errors thrown by the component.
  220. * @enum {string}
  221. */
  222. goog.ui.Component.Error = {
  223. /**
  224. * Error when a method is not supported.
  225. */
  226. NOT_SUPPORTED: 'Method not supported',
  227. /**
  228. * Error when the given element can not be decorated.
  229. */
  230. DECORATE_INVALID: 'Invalid element to decorate',
  231. /**
  232. * Error when the component is already rendered and another render attempt is
  233. * made.
  234. */
  235. ALREADY_RENDERED: 'Component already rendered',
  236. /**
  237. * Error when an attempt is made to set the parent of a component in a way
  238. * that would result in an inconsistent object graph.
  239. */
  240. PARENT_UNABLE_TO_BE_SET: 'Unable to set parent component',
  241. /**
  242. * Error when an attempt is made to add a child component at an out-of-bounds
  243. * index. We don't support sparse child arrays.
  244. */
  245. CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds',
  246. /**
  247. * Error when an attempt is made to remove a child component from a component
  248. * other than its parent.
  249. */
  250. NOT_OUR_CHILD: 'Child is not in parent component',
  251. /**
  252. * Error when an operation requiring DOM interaction is made when the
  253. * component is not in the document
  254. */
  255. NOT_IN_DOCUMENT: 'Operation not supported while component is not in document',
  256. /**
  257. * Error when an invalid component state is encountered.
  258. */
  259. STATE_INVALID: 'Invalid component state'
  260. };
  261. /**
  262. * Common component states. Components may have distinct appearance depending
  263. * on what state(s) apply to them. Not all components are expected to support
  264. * all states.
  265. * @enum {number}
  266. */
  267. goog.ui.Component.State = {
  268. /**
  269. * Union of all supported component states.
  270. */
  271. ALL: 0xFF,
  272. /**
  273. * Component is disabled.
  274. * @see goog.ui.Component.EventType.DISABLE
  275. * @see goog.ui.Component.EventType.ENABLE
  276. */
  277. DISABLED: 0x01,
  278. /**
  279. * Component is highlighted.
  280. * @see goog.ui.Component.EventType.HIGHLIGHT
  281. * @see goog.ui.Component.EventType.UNHIGHLIGHT
  282. */
  283. HOVER: 0x02,
  284. /**
  285. * Component is active (or "pressed").
  286. * @see goog.ui.Component.EventType.ACTIVATE
  287. * @see goog.ui.Component.EventType.DEACTIVATE
  288. */
  289. ACTIVE: 0x04,
  290. /**
  291. * Component is selected.
  292. * @see goog.ui.Component.EventType.SELECT
  293. * @see goog.ui.Component.EventType.UNSELECT
  294. */
  295. SELECTED: 0x08,
  296. /**
  297. * Component is checked.
  298. * @see goog.ui.Component.EventType.CHECK
  299. * @see goog.ui.Component.EventType.UNCHECK
  300. */
  301. CHECKED: 0x10,
  302. /**
  303. * Component has focus.
  304. * @see goog.ui.Component.EventType.FOCUS
  305. * @see goog.ui.Component.EventType.BLUR
  306. */
  307. FOCUSED: 0x20,
  308. /**
  309. * Component is opened (expanded). Applies to tree nodes, menu buttons,
  310. * submenus, zippys (zippies?), etc.
  311. * @see goog.ui.Component.EventType.OPEN
  312. * @see goog.ui.Component.EventType.CLOSE
  313. */
  314. OPENED: 0x40
  315. };
  316. /**
  317. * Static helper method; returns the type of event components are expected to
  318. * dispatch when transitioning to or from the given state.
  319. * @param {goog.ui.Component.State} state State to/from which the component
  320. * is transitioning.
  321. * @param {boolean} isEntering Whether the component is entering or leaving the
  322. * state.
  323. * @return {goog.ui.Component.EventType} Event type to dispatch.
  324. */
  325. goog.ui.Component.getStateTransitionEvent = function(state, isEntering) {
  326. switch (state) {
  327. case goog.ui.Component.State.DISABLED:
  328. return isEntering ? goog.ui.Component.EventType.DISABLE :
  329. goog.ui.Component.EventType.ENABLE;
  330. case goog.ui.Component.State.HOVER:
  331. return isEntering ? goog.ui.Component.EventType.HIGHLIGHT :
  332. goog.ui.Component.EventType.UNHIGHLIGHT;
  333. case goog.ui.Component.State.ACTIVE:
  334. return isEntering ? goog.ui.Component.EventType.ACTIVATE :
  335. goog.ui.Component.EventType.DEACTIVATE;
  336. case goog.ui.Component.State.SELECTED:
  337. return isEntering ? goog.ui.Component.EventType.SELECT :
  338. goog.ui.Component.EventType.UNSELECT;
  339. case goog.ui.Component.State.CHECKED:
  340. return isEntering ? goog.ui.Component.EventType.CHECK :
  341. goog.ui.Component.EventType.UNCHECK;
  342. case goog.ui.Component.State.FOCUSED:
  343. return isEntering ? goog.ui.Component.EventType.FOCUS :
  344. goog.ui.Component.EventType.BLUR;
  345. case goog.ui.Component.State.OPENED:
  346. return isEntering ? goog.ui.Component.EventType.OPEN :
  347. goog.ui.Component.EventType.CLOSE;
  348. default:
  349. // Fall through.
  350. }
  351. // Invalid state.
  352. throw Error(goog.ui.Component.Error.STATE_INVALID);
  353. };
  354. /**
  355. * Set the default right-to-left value. This causes all component's created from
  356. * this point forward to have the given value. This is useful for cases where
  357. * a given page is always in one directionality, avoiding unnecessary
  358. * right to left determinations.
  359. * @param {?boolean} rightToLeft Whether the components should be rendered
  360. * right-to-left. Null iff components should determine their directionality.
  361. */
  362. goog.ui.Component.setDefaultRightToLeft = function(rightToLeft) {
  363. goog.ui.Component.defaultRightToLeft_ = rightToLeft;
  364. };
  365. /**
  366. * Gets the unique ID for the instance of this component. If the instance
  367. * doesn't already have an ID, generates one on the fly.
  368. * @return {string} Unique component ID.
  369. */
  370. goog.ui.Component.prototype.getId = function() {
  371. return this.id_ || (this.id_ = this.idGenerator_.getNextUniqueId());
  372. };
  373. /**
  374. * Assigns an ID to this component instance. It is the caller's responsibility
  375. * to guarantee that the ID is unique. If the component is a child of a parent
  376. * component, then the parent component's child index is updated to reflect the
  377. * new ID; this may throw an error if the parent already has a child with an ID
  378. * that conflicts with the new ID.
  379. * @param {string} id Unique component ID.
  380. */
  381. goog.ui.Component.prototype.setId = function(id) {
  382. if (this.parent_ && this.parent_.childIndex_) {
  383. // Update the parent's child index.
  384. goog.object.remove(this.parent_.childIndex_, this.id_);
  385. goog.object.add(this.parent_.childIndex_, id, this);
  386. }
  387. // Update the component ID.
  388. this.id_ = id;
  389. };
  390. /**
  391. * Gets the component's element.
  392. * @return {Element} The element for the component.
  393. */
  394. goog.ui.Component.prototype.getElement = function() {
  395. return this.element_;
  396. };
  397. /**
  398. * Gets the component's element. This differs from getElement in that
  399. * it assumes that the element exists (i.e. the component has been
  400. * rendered/decorated) and will cause an assertion error otherwise (if
  401. * assertion is enabled).
  402. * @return {!Element} The element for the component.
  403. */
  404. goog.ui.Component.prototype.getElementStrict = function() {
  405. var el = this.element_;
  406. goog.asserts.assert(
  407. el, 'Can not call getElementStrict before rendering/decorating.');
  408. return el;
  409. };
  410. /**
  411. * Sets the component's root element to the given element. Considered
  412. * protected and final.
  413. *
  414. * This should generally only be called during createDom. Setting the element
  415. * does not actually change which element is rendered, only the element that is
  416. * associated with this UI component.
  417. *
  418. * This should only be used by subclasses and its associated renderers.
  419. *
  420. * @param {Element} element Root element for the component.
  421. */
  422. goog.ui.Component.prototype.setElementInternal = function(element) {
  423. this.element_ = element;
  424. };
  425. /**
  426. * Returns an array of all the elements in this component's DOM with the
  427. * provided className.
  428. * @param {string} className The name of the class to look for.
  429. * @return {!IArrayLike<!Element>} The items found with the class name provided.
  430. */
  431. goog.ui.Component.prototype.getElementsByClass = function(className) {
  432. return this.element_ ?
  433. this.dom_.getElementsByClass(className, this.element_) :
  434. [];
  435. };
  436. /**
  437. * Returns the first element in this component's DOM with the provided
  438. * className.
  439. * @param {string} className The name of the class to look for.
  440. * @return {Element} The first item with the class name provided.
  441. */
  442. goog.ui.Component.prototype.getElementByClass = function(className) {
  443. return this.element_ ? this.dom_.getElementByClass(className, this.element_) :
  444. null;
  445. };
  446. /**
  447. * Similar to {@code getElementByClass} except that it expects the
  448. * element to be present in the dom thus returning a required value. Otherwise,
  449. * will assert.
  450. * @param {string} className The name of the class to look for.
  451. * @return {!Element} The first item with the class name provided.
  452. */
  453. goog.ui.Component.prototype.getRequiredElementByClass = function(className) {
  454. var el = this.getElementByClass(className);
  455. goog.asserts.assert(
  456. el, 'Expected element in component with class: %s', className);
  457. return el;
  458. };
  459. /**
  460. * Returns the event handler for this component, lazily created the first time
  461. * this method is called.
  462. * @return {!goog.events.EventHandler<T>} Event handler for this component.
  463. * @protected
  464. * @this {T}
  465. * @template T
  466. */
  467. goog.ui.Component.prototype.getHandler = function() {
  468. // TODO(user): templated "this" values currently result in "this" being
  469. // "unknown" in the body of the function.
  470. var self = /** @type {goog.ui.Component} */ (this);
  471. if (!self.googUiComponentHandler_) {
  472. self.googUiComponentHandler_ = new goog.events.EventHandler(self);
  473. }
  474. return self.googUiComponentHandler_;
  475. };
  476. /**
  477. * Sets the parent of this component to use for event bubbling. Throws an error
  478. * if the component already has a parent or if an attempt is made to add a
  479. * component to itself as a child. Callers must use {@code removeChild}
  480. * or {@code removeChildAt} to remove components from their containers before
  481. * calling this method.
  482. * @see goog.ui.Component#removeChild
  483. * @see goog.ui.Component#removeChildAt
  484. * @param {goog.ui.Component} parent The parent component.
  485. */
  486. goog.ui.Component.prototype.setParent = function(parent) {
  487. if (this == parent) {
  488. // Attempting to add a child to itself is an error.
  489. throw Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);
  490. }
  491. if (parent && this.parent_ && this.id_ && this.parent_.getChild(this.id_) &&
  492. this.parent_ != parent) {
  493. // This component is already the child of some parent, so it should be
  494. // removed using removeChild/removeChildAt first.
  495. throw Error(goog.ui.Component.Error.PARENT_UNABLE_TO_BE_SET);
  496. }
  497. this.parent_ = parent;
  498. goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);
  499. };
  500. /**
  501. * Returns the component's parent, if any.
  502. * @return {goog.ui.Component?} The parent component.
  503. */
  504. goog.ui.Component.prototype.getParent = function() {
  505. return this.parent_;
  506. };
  507. /**
  508. * Overrides {@link goog.events.EventTarget#setParentEventTarget} to throw an
  509. * error if the parent component is set, and the argument is not the parent.
  510. * @override
  511. */
  512. goog.ui.Component.prototype.setParentEventTarget = function(parent) {
  513. if (this.parent_ && this.parent_ != parent) {
  514. throw Error(goog.ui.Component.Error.NOT_SUPPORTED);
  515. }
  516. goog.ui.Component.superClass_.setParentEventTarget.call(this, parent);
  517. };
  518. /**
  519. * Returns the dom helper that is being used on this component.
  520. * @return {!goog.dom.DomHelper} The dom helper used on this component.
  521. */
  522. goog.ui.Component.prototype.getDomHelper = function() {
  523. return this.dom_;
  524. };
  525. /**
  526. * Determines whether the component has been added to the document.
  527. * @return {boolean} TRUE if rendered. Otherwise, FALSE.
  528. */
  529. goog.ui.Component.prototype.isInDocument = function() {
  530. return this.inDocument_;
  531. };
  532. /**
  533. * Creates the initial DOM representation for the component. The default
  534. * implementation is to set this.element_ = div.
  535. */
  536. goog.ui.Component.prototype.createDom = function() {
  537. this.element_ = this.dom_.createElement(goog.dom.TagName.DIV);
  538. };
  539. /**
  540. * Renders the component. If a parent element is supplied, the component's
  541. * element will be appended to it. If there is no optional parent element and
  542. * the element doesn't have a parentNode then it will be appended to the
  543. * document body.
  544. *
  545. * If this component has a parent component, and the parent component is
  546. * not in the document already, then this will not call {@code enterDocument}
  547. * on this component.
  548. *
  549. * Throws an Error if the component is already rendered.
  550. *
  551. * @param {Element=} opt_parentElement Optional parent element to render the
  552. * component into.
  553. */
  554. goog.ui.Component.prototype.render = function(opt_parentElement) {
  555. this.render_(opt_parentElement);
  556. };
  557. /**
  558. * Renders the component before another element. The other element should be in
  559. * the document already.
  560. *
  561. * Throws an Error if the component is already rendered.
  562. *
  563. * @param {Node} sibling Node to render the component before.
  564. */
  565. goog.ui.Component.prototype.renderBefore = function(sibling) {
  566. this.render_(/** @type {Element} */ (sibling.parentNode), sibling);
  567. };
  568. /**
  569. * Renders the component. If a parent element is supplied, the component's
  570. * element will be appended to it. If there is no optional parent element and
  571. * the element doesn't have a parentNode then it will be appended to the
  572. * document body.
  573. *
  574. * If this component has a parent component, and the parent component is
  575. * not in the document already, then this will not call {@code enterDocument}
  576. * on this component.
  577. *
  578. * Throws an Error if the component is already rendered.
  579. *
  580. * @param {Element=} opt_parentElement Optional parent element to render the
  581. * component into.
  582. * @param {Node=} opt_beforeNode Node before which the component is to
  583. * be rendered. If left out the node is appended to the parent element.
  584. * @private
  585. */
  586. goog.ui.Component.prototype.render_ = function(
  587. opt_parentElement, opt_beforeNode) {
  588. if (this.inDocument_) {
  589. throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
  590. }
  591. if (!this.element_) {
  592. this.createDom();
  593. }
  594. if (opt_parentElement) {
  595. opt_parentElement.insertBefore(this.element_, opt_beforeNode || null);
  596. } else {
  597. this.dom_.getDocument().body.appendChild(this.element_);
  598. }
  599. // If this component has a parent component that isn't in the document yet,
  600. // we don't call enterDocument() here. Instead, when the parent component
  601. // enters the document, the enterDocument() call will propagate to its
  602. // children, including this one. If the component doesn't have a parent
  603. // or if the parent is already in the document, we call enterDocument().
  604. if (!this.parent_ || this.parent_.isInDocument()) {
  605. this.enterDocument();
  606. }
  607. };
  608. /**
  609. * Decorates the element for the UI component. If the element is in the
  610. * document, the enterDocument method will be called.
  611. *
  612. * If goog.ui.Component.ALLOW_DETACHED_DECORATION is false, the caller must
  613. * pass an element that is in the document.
  614. *
  615. * @param {Element} element Element to decorate.
  616. */
  617. goog.ui.Component.prototype.decorate = function(element) {
  618. if (this.inDocument_) {
  619. throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
  620. } else if (element && this.canDecorate(element)) {
  621. this.wasDecorated_ = true;
  622. // Set the DOM helper of the component to match the decorated element.
  623. var doc = goog.dom.getOwnerDocument(element);
  624. if (!this.dom_ || this.dom_.getDocument() != doc) {
  625. this.dom_ = goog.dom.getDomHelper(element);
  626. }
  627. // Call specific component decorate logic.
  628. this.decorateInternal(element);
  629. // If supporting detached decoration, check that element is in doc.
  630. if (!goog.ui.Component.ALLOW_DETACHED_DECORATION ||
  631. goog.dom.contains(doc, element)) {
  632. this.enterDocument();
  633. }
  634. } else {
  635. throw Error(goog.ui.Component.Error.DECORATE_INVALID);
  636. }
  637. };
  638. /**
  639. * Determines if a given element can be decorated by this type of component.
  640. * This method should be overridden by inheriting objects.
  641. * @param {Element} element Element to decorate.
  642. * @return {boolean} True if the element can be decorated, false otherwise.
  643. */
  644. goog.ui.Component.prototype.canDecorate = function(element) {
  645. return true;
  646. };
  647. /**
  648. * @return {boolean} Whether the component was decorated.
  649. */
  650. goog.ui.Component.prototype.wasDecorated = function() {
  651. return this.wasDecorated_;
  652. };
  653. /**
  654. * Actually decorates the element. Should be overridden by inheriting objects.
  655. * This method can assume there are checks to ensure the component has not
  656. * already been rendered have occurred and that enter document will be called
  657. * afterwards. This method is considered protected.
  658. * @param {Element} element Element to decorate.
  659. * @protected
  660. */
  661. goog.ui.Component.prototype.decorateInternal = function(element) {
  662. this.element_ = element;
  663. };
  664. /**
  665. * Called when the component's element is known to be in the document. Anything
  666. * using document.getElementById etc. should be done at this stage.
  667. *
  668. * If the component contains child components, this call is propagated to its
  669. * children.
  670. */
  671. goog.ui.Component.prototype.enterDocument = function() {
  672. this.inDocument_ = true;
  673. // Propagate enterDocument to child components that have a DOM, if any.
  674. // If a child was decorated before entering the document (permitted when
  675. // goog.ui.Component.ALLOW_DETACHED_DECORATION is true), its enterDocument
  676. // will be called here.
  677. this.forEachChild(function(child) {
  678. if (!child.isInDocument() && child.getElement()) {
  679. child.enterDocument();
  680. }
  681. });
  682. };
  683. /**
  684. * Called by dispose to clean up the elements and listeners created by a
  685. * component, or by a parent component/application who has removed the
  686. * component from the document but wants to reuse it later.
  687. *
  688. * If the component contains child components, this call is propagated to its
  689. * children.
  690. *
  691. * It should be possible for the component to be rendered again once this method
  692. * has been called.
  693. */
  694. goog.ui.Component.prototype.exitDocument = function() {
  695. // Propagate exitDocument to child components that have been rendered, if any.
  696. this.forEachChild(function(child) {
  697. if (child.isInDocument()) {
  698. child.exitDocument();
  699. }
  700. });
  701. if (this.googUiComponentHandler_) {
  702. this.googUiComponentHandler_.removeAll();
  703. }
  704. this.inDocument_ = false;
  705. };
  706. /**
  707. * Disposes of the component. Calls {@code exitDocument}, which is expected to
  708. * remove event handlers and clean up the component. Propagates the call to
  709. * the component's children, if any. Removes the component's DOM from the
  710. * document unless it was decorated.
  711. * @override
  712. * @protected
  713. */
  714. goog.ui.Component.prototype.disposeInternal = function() {
  715. if (this.inDocument_) {
  716. this.exitDocument();
  717. }
  718. if (this.googUiComponentHandler_) {
  719. this.googUiComponentHandler_.dispose();
  720. delete this.googUiComponentHandler_;
  721. }
  722. // Disposes of the component's children, if any.
  723. this.forEachChild(function(child) { child.dispose(); });
  724. // Detach the component's element from the DOM, unless it was decorated.
  725. if (!this.wasDecorated_ && this.element_) {
  726. goog.dom.removeNode(this.element_);
  727. }
  728. this.children_ = null;
  729. this.childIndex_ = null;
  730. this.element_ = null;
  731. this.model_ = null;
  732. this.parent_ = null;
  733. goog.ui.Component.superClass_.disposeInternal.call(this);
  734. };
  735. /**
  736. * Helper function for subclasses that gets a unique id for a given fragment,
  737. * this can be used by components to generate unique string ids for DOM
  738. * elements.
  739. * @param {string} idFragment A partial id.
  740. * @return {string} Unique element id.
  741. */
  742. goog.ui.Component.prototype.makeId = function(idFragment) {
  743. return this.getId() + '.' + idFragment;
  744. };
  745. /**
  746. * Makes a collection of ids. This is a convenience method for makeId. The
  747. * object's values are the id fragments and the new values are the generated
  748. * ids. The key will remain the same.
  749. * @param {Object} object The object that will be used to create the ids.
  750. * @return {!Object<string, string>} An object of id keys to generated ids.
  751. */
  752. goog.ui.Component.prototype.makeIds = function(object) {
  753. var ids = {};
  754. for (var key in object) {
  755. ids[key] = this.makeId(object[key]);
  756. }
  757. return ids;
  758. };
  759. /**
  760. * Returns the model associated with the UI component.
  761. * @return {*} The model.
  762. */
  763. goog.ui.Component.prototype.getModel = function() {
  764. return this.model_;
  765. };
  766. /**
  767. * Sets the model associated with the UI component.
  768. * @param {*} obj The model.
  769. */
  770. goog.ui.Component.prototype.setModel = function(obj) {
  771. this.model_ = obj;
  772. };
  773. /**
  774. * Helper function for returning the fragment portion of an id generated using
  775. * makeId().
  776. * @param {string} id Id generated with makeId().
  777. * @return {string} Fragment.
  778. */
  779. goog.ui.Component.prototype.getFragmentFromId = function(id) {
  780. return id.substring(this.getId().length + 1);
  781. };
  782. /**
  783. * Helper function for returning an element in the document with a unique id
  784. * generated using makeId().
  785. * @param {string} idFragment The partial id.
  786. * @return {Element} The element with the unique id, or null if it cannot be
  787. * found.
  788. */
  789. goog.ui.Component.prototype.getElementByFragment = function(idFragment) {
  790. if (!this.inDocument_) {
  791. throw Error(goog.ui.Component.Error.NOT_IN_DOCUMENT);
  792. }
  793. return this.dom_.getElement(this.makeId(idFragment));
  794. };
  795. /**
  796. * Adds the specified component as the last child of this component. See
  797. * {@link goog.ui.Component#addChildAt} for detailed semantics.
  798. *
  799. * @see goog.ui.Component#addChildAt
  800. * @param {goog.ui.Component} child The new child component.
  801. * @param {boolean=} opt_render If true, the child component will be rendered
  802. * into the parent.
  803. */
  804. goog.ui.Component.prototype.addChild = function(child, opt_render) {
  805. // TODO(gboyer): addChildAt(child, this.getChildCount(), false) will
  806. // reposition any already-rendered child to the end. Instead, perhaps
  807. // addChild(child, false) should never reposition the child; instead, clients
  808. // that need the repositioning will use addChildAt explicitly. Right now,
  809. // clients can get around this by calling addChild before calling decorate.
  810. this.addChildAt(child, this.getChildCount(), opt_render);
  811. };
  812. /**
  813. * Adds the specified component as a child of this component at the given
  814. * 0-based index.
  815. *
  816. * Both {@code addChild} and {@code addChildAt} assume the following contract
  817. * between parent and child components:
  818. * <ul>
  819. * <li>the child component's element must be a descendant of the parent
  820. * component's element, and
  821. * <li>the DOM state of the child component must be consistent with the DOM
  822. * state of the parent component (see {@code isInDocument}) in the
  823. * steady state -- the exception is to addChildAt(child, i, false) and
  824. * then immediately decorate/render the child.
  825. * </ul>
  826. *
  827. * In particular, {@code parent.addChild(child)} will throw an error if the
  828. * child component is already in the document, but the parent isn't.
  829. *
  830. * Clients of this API may call {@code addChild} and {@code addChildAt} with
  831. * {@code opt_render} set to true. If {@code opt_render} is true, calling these
  832. * methods will automatically render the child component's element into the
  833. * parent component's element. If the parent does not yet have an element, then
  834. * {@code createDom} will automatically be invoked on the parent before
  835. * rendering the child.
  836. *
  837. * Invoking {@code parent.addChild(child, true)} will throw an error if the
  838. * child component is already in the document, regardless of the parent's DOM
  839. * state.
  840. *
  841. * If {@code opt_render} is true and the parent component is not already
  842. * in the document, {@code enterDocument} will not be called on this component
  843. * at this point.
  844. *
  845. * Finally, this method also throws an error if the new child already has a
  846. * different parent, or the given index is out of bounds.
  847. *
  848. * @see goog.ui.Component#addChild
  849. * @param {goog.ui.Component} child The new child component.
  850. * @param {number} index 0-based index at which the new child component is to be
  851. * added; must be between 0 and the current child count (inclusive).
  852. * @param {boolean=} opt_render If true, the child component will be rendered
  853. * into the parent.
  854. * @return {void} Nada.
  855. */
  856. goog.ui.Component.prototype.addChildAt = function(child, index, opt_render) {
  857. goog.asserts.assert(!!child, 'Provided element must not be null.');
  858. if (child.inDocument_ && (opt_render || !this.inDocument_)) {
  859. // Adding a child that's already in the document is an error, except if the
  860. // parent is also in the document and opt_render is false (e.g. decorate()).
  861. throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
  862. }
  863. if (index < 0 || index > this.getChildCount()) {
  864. // Allowing sparse child arrays would lead to strange behavior, so we don't.
  865. throw Error(goog.ui.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS);
  866. }
  867. // Create the index and the child array on first use.
  868. if (!this.childIndex_ || !this.children_) {
  869. this.childIndex_ = {};
  870. this.children_ = [];
  871. }
  872. // Moving child within component, remove old reference.
  873. if (child.getParent() == this) {
  874. goog.object.set(this.childIndex_, child.getId(), child);
  875. goog.array.remove(this.children_, child);
  876. // Add the child to this component. goog.object.add() throws an error if
  877. // a child with the same ID already exists.
  878. } else {
  879. goog.object.add(this.childIndex_, child.getId(), child);
  880. }
  881. // Set the parent of the child to this component. This throws an error if
  882. // the child is already contained by another component.
  883. child.setParent(this);
  884. goog.array.insertAt(this.children_, child, index);
  885. if (child.inDocument_ && this.inDocument_ && child.getParent() == this) {
  886. // Changing the position of an existing child, move the DOM node (if
  887. // necessary).
  888. var contentElement = this.getContentElement();
  889. var insertBeforeElement = contentElement.childNodes[index] || null;
  890. if (insertBeforeElement != child.getElement()) {
  891. contentElement.insertBefore(child.getElement(), insertBeforeElement);
  892. }
  893. } else if (opt_render) {
  894. // If this (parent) component doesn't have a DOM yet, call createDom now
  895. // to make sure we render the child component's element into the correct
  896. // parent element (otherwise render_ with a null first argument would
  897. // render the child into the document body, which is almost certainly not
  898. // what we want).
  899. if (!this.element_) {
  900. this.createDom();
  901. }
  902. // Render the child into the parent at the appropriate location. Note that
  903. // getChildAt(index + 1) returns undefined if inserting at the end.
  904. // TODO(attila): We should have a renderer with a renderChildAt API.
  905. var sibling = this.getChildAt(index + 1);
  906. // render_() calls enterDocument() if the parent is already in the document.
  907. child.render_(this.getContentElement(), sibling ? sibling.element_ : null);
  908. } else if (
  909. this.inDocument_ && !child.inDocument_ && child.element_ &&
  910. child.element_.parentNode &&
  911. // Under some circumstances, IE8 implicitly creates a Document Fragment
  912. // for detached nodes, so ensure the parent is an Element as it should be.
  913. child.element_.parentNode.nodeType == goog.dom.NodeType.ELEMENT) {
  914. // We don't touch the DOM, but if the parent is in the document, and the
  915. // child element is in the document but not marked as such, then we call
  916. // enterDocument on the child.
  917. // TODO(gboyer): It would be nice to move this condition entirely, but
  918. // there's a large risk of breaking existing applications that manually
  919. // append the child to the DOM and then call addChild.
  920. child.enterDocument();
  921. }
  922. };
  923. /**
  924. * Returns the DOM element into which child components are to be rendered,
  925. * or null if the component itself hasn't been rendered yet. This default
  926. * implementation returns the component's root element. Subclasses with
  927. * complex DOM structures must override this method.
  928. * @return {Element} Element to contain child elements (null if none).
  929. */
  930. goog.ui.Component.prototype.getContentElement = function() {
  931. return this.element_;
  932. };
  933. /**
  934. * Returns true if the component is rendered right-to-left, false otherwise.
  935. * The first time this function is invoked, the right-to-left rendering property
  936. * is set if it has not been already.
  937. * @return {boolean} Whether the control is rendered right-to-left.
  938. */
  939. goog.ui.Component.prototype.isRightToLeft = function() {
  940. if (this.rightToLeft_ == null) {
  941. this.rightToLeft_ = goog.style.isRightToLeft(
  942. this.inDocument_ ? this.element_ : this.dom_.getDocument().body);
  943. }
  944. return this.rightToLeft_;
  945. };
  946. /**
  947. * Set is right-to-left. This function should be used if the component needs
  948. * to know the rendering direction during dom creation (i.e. before
  949. * {@link #enterDocument} is called and is right-to-left is set).
  950. * @param {boolean} rightToLeft Whether the component is rendered
  951. * right-to-left.
  952. */
  953. goog.ui.Component.prototype.setRightToLeft = function(rightToLeft) {
  954. if (this.inDocument_) {
  955. throw Error(goog.ui.Component.Error.ALREADY_RENDERED);
  956. }
  957. this.rightToLeft_ = rightToLeft;
  958. };
  959. /**
  960. * Returns true if the component has children.
  961. * @return {boolean} True if the component has children.
  962. */
  963. goog.ui.Component.prototype.hasChildren = function() {
  964. return !!this.children_ && this.children_.length != 0;
  965. };
  966. /**
  967. * Returns the number of children of this component.
  968. * @return {number} The number of children.
  969. */
  970. goog.ui.Component.prototype.getChildCount = function() {
  971. return this.children_ ? this.children_.length : 0;
  972. };
  973. /**
  974. * Returns an array containing the IDs of the children of this component, or an
  975. * empty array if the component has no children.
  976. * @return {!Array<string>} Child component IDs.
  977. */
  978. goog.ui.Component.prototype.getChildIds = function() {
  979. var ids = [];
  980. // We don't use goog.object.getKeys(this.childIndex_) because we want to
  981. // return the IDs in the correct order as determined by this.children_.
  982. this.forEachChild(function(child) {
  983. // addChild()/addChildAt() guarantee that the child array isn't sparse.
  984. ids.push(child.getId());
  985. });
  986. return ids;
  987. };
  988. /**
  989. * Returns the child with the given ID, or null if no such child exists.
  990. * @param {string} id Child component ID.
  991. * @return {goog.ui.Component?} The child with the given ID; null if none.
  992. */
  993. goog.ui.Component.prototype.getChild = function(id) {
  994. // Use childIndex_ for O(1) access by ID.
  995. return (this.childIndex_ && id) ?
  996. /** @type {goog.ui.Component} */ (
  997. goog.object.get(this.childIndex_, id)) ||
  998. null :
  999. null;
  1000. };
  1001. /**
  1002. * Returns the child at the given index, or null if the index is out of bounds.
  1003. * @param {number} index 0-based index.
  1004. * @return {goog.ui.Component?} The child at the given index; null if none.
  1005. */
  1006. goog.ui.Component.prototype.getChildAt = function(index) {
  1007. // Use children_ for access by index.
  1008. return this.children_ ? this.children_[index] || null : null;
  1009. };
  1010. /**
  1011. * Calls the given function on each of this component's children in order. If
  1012. * {@code opt_obj} is provided, it will be used as the 'this' object in the
  1013. * function when called. The function should take two arguments: the child
  1014. * component and its 0-based index. The return value is ignored.
  1015. * @param {function(this:T,?,number):?} f The function to call for every
  1016. * child component; should take 2 arguments (the child and its index).
  1017. * @param {T=} opt_obj Used as the 'this' object in f when called.
  1018. * @template T
  1019. */
  1020. goog.ui.Component.prototype.forEachChild = function(f, opt_obj) {
  1021. if (this.children_) {
  1022. goog.array.forEach(this.children_, f, opt_obj);
  1023. }
  1024. };
  1025. /**
  1026. * Returns the 0-based index of the given child component, or -1 if no such
  1027. * child is found.
  1028. * @param {goog.ui.Component?} child The child component.
  1029. * @return {number} 0-based index of the child component; -1 if not found.
  1030. */
  1031. goog.ui.Component.prototype.indexOfChild = function(child) {
  1032. return (this.children_ && child) ? goog.array.indexOf(this.children_, child) :
  1033. -1;
  1034. };
  1035. /**
  1036. * Removes the given child from this component, and returns it. Throws an error
  1037. * if the argument is invalid or if the specified child isn't found in the
  1038. * parent component. The argument can either be a string (interpreted as the
  1039. * ID of the child component to remove) or the child component itself.
  1040. *
  1041. * If {@code opt_unrender} is true, calls {@link goog.ui.component#exitDocument}
  1042. * on the removed child, and subsequently detaches the child's DOM from the
  1043. * document. Otherwise it is the caller's responsibility to clean up the child
  1044. * component's DOM.
  1045. *
  1046. * @see goog.ui.Component#removeChildAt
  1047. * @param {string|goog.ui.Component|null} child The ID of the child to remove,
  1048. * or the child component itself.
  1049. * @param {boolean=} opt_unrender If true, calls {@code exitDocument} on the
  1050. * removed child component, and detaches its DOM from the document.
  1051. * @return {goog.ui.Component} The removed component, if any.
  1052. */
  1053. goog.ui.Component.prototype.removeChild = function(child, opt_unrender) {
  1054. if (child) {
  1055. // Normalize child to be the object and id to be the ID string. This also
  1056. // ensures that the child is really ours.
  1057. var id = goog.isString(child) ? child : child.getId();
  1058. child = this.getChild(id);
  1059. if (id && child) {
  1060. goog.object.remove(this.childIndex_, id);
  1061. goog.array.remove(this.children_, child);
  1062. if (opt_unrender) {
  1063. // Remove the child component's DOM from the document. We have to call
  1064. // exitDocument first (see documentation).
  1065. child.exitDocument();
  1066. if (child.element_) {
  1067. goog.dom.removeNode(child.element_);
  1068. }
  1069. }
  1070. // Child's parent must be set to null after exitDocument is called
  1071. // so that the child can unlisten to its parent if required.
  1072. child.setParent(null);
  1073. }
  1074. }
  1075. if (!child) {
  1076. throw Error(goog.ui.Component.Error.NOT_OUR_CHILD);
  1077. }
  1078. return /** @type {!goog.ui.Component} */ (child);
  1079. };
  1080. /**
  1081. * Removes the child at the given index from this component, and returns it.
  1082. * Throws an error if the argument is out of bounds, or if the specified child
  1083. * isn't found in the parent. See {@link goog.ui.Component#removeChild} for
  1084. * detailed semantics.
  1085. *
  1086. * @see goog.ui.Component#removeChild
  1087. * @param {number} index 0-based index of the child to remove.
  1088. * @param {boolean=} opt_unrender If true, calls {@code exitDocument} on the
  1089. * removed child component, and detaches its DOM from the document.
  1090. * @return {goog.ui.Component} The removed component, if any.
  1091. */
  1092. goog.ui.Component.prototype.removeChildAt = function(index, opt_unrender) {
  1093. // removeChild(null) will throw error.
  1094. return this.removeChild(this.getChildAt(index), opt_unrender);
  1095. };
  1096. /**
  1097. * Removes every child component attached to this one and returns them.
  1098. *
  1099. * @see goog.ui.Component#removeChild
  1100. * @param {boolean=} opt_unrender If true, calls {@link #exitDocument} on the
  1101. * removed child components, and detaches their DOM from the document.
  1102. * @return {!Array<goog.ui.Component>} The removed components if any.
  1103. */
  1104. goog.ui.Component.prototype.removeChildren = function(opt_unrender) {
  1105. var removedChildren = [];
  1106. while (this.hasChildren()) {
  1107. removedChildren.push(this.removeChildAt(0, opt_unrender));
  1108. }
  1109. return removedChildren;
  1110. };