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