1 | /**
|
2 | * @license
|
3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
4 | * This code may only be used under the BSD style license found at
|
5 | * http://polymer.github.io/LICENSE.txt
|
6 | * The complete set of authors may be found at
|
7 | * http://polymer.github.io/AUTHORS.txt
|
8 | * The complete set of contributors may be found at
|
9 | * http://polymer.github.io/CONTRIBUTORS.txt
|
10 | * Code distributed by Google as part of the polymer project is also
|
11 | * subject to an additional IP rights grant found at
|
12 | * http://polymer.github.io/PATENTS.txt
|
13 | */
|
14 | /**
|
15 | * When using Closure Compiler, JSCompiler_renameProperty(property, object) is
|
16 | * replaced at compile time by the munged name for object[property]. We cannot
|
17 | * alias this function, so we have to use a small shim that has the same
|
18 | * behavior when not compiling.
|
19 | */
|
20 | const JSCompiler_renameProperty = (prop, _obj) => prop;
|
21 | /**
|
22 | * Returns the property descriptor for a property on this prototype by walking
|
23 | * up the prototype chain. Note that we stop just before Object.prototype, which
|
24 | * also avoids issues with Symbol polyfills (core-js, get-own-property-symbols),
|
25 | * which create accessors for the symbols on Object.prototype.
|
26 | */
|
27 | const descriptorFromPrototype = (name, proto) => {
|
28 | if (name in proto) {
|
29 | while (proto !== Object.prototype) {
|
30 | if (proto.hasOwnProperty(name)) {
|
31 | return Object.getOwnPropertyDescriptor(proto, name);
|
32 | }
|
33 | proto = Object.getPrototypeOf(proto);
|
34 | }
|
35 | }
|
36 | return undefined;
|
37 | };
|
38 | export const defaultConverter = {
|
39 | toAttribute(value, type) {
|
40 | switch (type) {
|
41 | case Boolean:
|
42 | return value ? '' : null;
|
43 | case Object:
|
44 | case Array:
|
45 | // if the value is `null` or `undefined` pass this through
|
46 | // to allow removing/no change behavior.
|
47 | return value == null ? value : JSON.stringify(value);
|
48 | }
|
49 | return value;
|
50 | },
|
51 | fromAttribute(value, type) {
|
52 | switch (type) {
|
53 | case Boolean:
|
54 | return value !== null;
|
55 | case Number:
|
56 | return value === null ? null : Number(value);
|
57 | case Object:
|
58 | case Array:
|
59 | return JSON.parse(value);
|
60 | }
|
61 | return value;
|
62 | }
|
63 | };
|
64 | /**
|
65 | * Change function that returns true if `value` is different from `oldValue`.
|
66 | * This method is used as the default for a property's `hasChanged` function.
|
67 | */
|
68 | export const notEqual = (value, old) => {
|
69 | // This ensures (old==NaN, value==NaN) always returns false
|
70 | return old !== value && (old === old || value === value);
|
71 | };
|
72 | const defaultPropertyDeclaration = {
|
73 | attribute: true,
|
74 | type: String,
|
75 | converter: defaultConverter,
|
76 | reflect: false,
|
77 | hasChanged: notEqual
|
78 | };
|
79 | const microtaskPromise = Promise.resolve(true);
|
80 | const STATE_HAS_UPDATED = 1;
|
81 | const STATE_UPDATE_REQUESTED = 1 << 2;
|
82 | const STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3;
|
83 | const STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4;
|
84 | const STATE_HAS_CONNECTED = 1 << 5;
|
85 | /**
|
86 | * Base element class which manages element properties and attributes. When
|
87 | * properties change, the `update` method is asynchronously called. This method
|
88 | * should be supplied by subclassers to render updates as desired.
|
89 | */
|
90 | export class UpdatingElement extends HTMLElement {
|
91 | constructor() {
|
92 | super();
|
93 | this._updateState = 0;
|
94 | this._instanceProperties = undefined;
|
95 | this._updatePromise = microtaskPromise;
|
96 | this._hasConnectedResolver = undefined;
|
97 | /**
|
98 | * Map with keys for any properties that have changed since the last
|
99 | * update cycle with previous values.
|
100 | */
|
101 | this._changedProperties = new Map();
|
102 | /**
|
103 | * Map with keys of properties that should be reflected when updated.
|
104 | */
|
105 | this._reflectingProperties = undefined;
|
106 | this.initialize();
|
107 | }
|
108 | /**
|
109 | * Returns a list of attributes corresponding to the registered properties.
|
110 | * @nocollapse
|
111 | */
|
112 | static get observedAttributes() {
|
113 | // note: piggy backing on this to ensure we're _finalized.
|
114 | this._finalize();
|
115 | const attributes = [];
|
116 | for (const [p, v] of this._classProperties) {
|
117 | const attr = this._attributeNameForProperty(p, v);
|
118 | if (attr !== undefined) {
|
119 | this._attributeToPropertyMap.set(attr, p);
|
120 | attributes.push(attr);
|
121 | }
|
122 | }
|
123 | return attributes;
|
124 | }
|
125 | /**
|
126 | * Ensures the private `_classProperties` property metadata is created.
|
127 | * In addition to `_finalize` this is also called in `createProperty` to
|
128 | * ensure the `@property` decorator can add property metadata.
|
129 | */
|
130 | /** @nocollapse */
|
131 | static _ensureClassProperties() {
|
132 | // ensure private storage for property declarations.
|
133 | if (!this.hasOwnProperty(JSCompiler_renameProperty('_classProperties', this))) {
|
134 | this._classProperties = new Map();
|
135 | // NOTE: Workaround IE11 not supporting Map constructor argument.
|
136 | const superProperties = Object.getPrototypeOf(this)._classProperties;
|
137 | if (superProperties !== undefined) {
|
138 | superProperties.forEach((v, k) => this._classProperties.set(k, v));
|
139 | }
|
140 | }
|
141 | }
|
142 | /**
|
143 | * Creates a property accessor on the element prototype if one does not exist.
|
144 | * The property setter calls the property's `hasChanged` property option
|
145 | * or uses a strict identity check to determine whether or not to request
|
146 | * an update.
|
147 | * @nocollapse
|
148 | */
|
149 | static createProperty(name, options = defaultPropertyDeclaration) {
|
150 | // Note, since this can be called by the `@property` decorator which
|
151 | // is called before `_finalize`, we ensure storage exists for property
|
152 | // metadata.
|
153 | this._ensureClassProperties();
|
154 | this._classProperties.set(name, options);
|
155 | if (!options.noAccessor) {
|
156 | const superDesc = descriptorFromPrototype(name, this.prototype);
|
157 | let desc;
|
158 | // If there is a super accessor, capture it and "super" to it
|
159 | if (superDesc !== undefined && (superDesc.set && superDesc.get)) {
|
160 | const { set, get } = superDesc;
|
161 | desc = {
|
162 | get() { return get.call(this); },
|
163 | set(value) {
|
164 | const oldValue = this[name];
|
165 | set.call(this, value);
|
166 | this.requestUpdate(name, oldValue);
|
167 | },
|
168 | configurable: true,
|
169 | enumerable: true
|
170 | };
|
171 | }
|
172 | else {
|
173 | const key = typeof name === 'symbol' ? Symbol() : `__${name}`;
|
174 | desc = {
|
175 | get() { return this[key]; },
|
176 | set(value) {
|
177 | const oldValue = this[name];
|
178 | this[key] = value;
|
179 | this.requestUpdate(name, oldValue);
|
180 | },
|
181 | configurable: true,
|
182 | enumerable: true
|
183 | };
|
184 | }
|
185 | Object.defineProperty(this.prototype, name, desc);
|
186 | }
|
187 | }
|
188 | /**
|
189 | * Creates property accessors for registered properties and ensures
|
190 | * any superclasses are also finalized.
|
191 | * @nocollapse
|
192 | */
|
193 | static _finalize() {
|
194 | if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this)) &&
|
195 | this.finalized) {
|
196 | return;
|
197 | }
|
198 | // finalize any superclasses
|
199 | const superCtor = Object.getPrototypeOf(this);
|
200 | if (typeof superCtor._finalize === 'function') {
|
201 | superCtor._finalize();
|
202 | }
|
203 | this.finalized = true;
|
204 | this._ensureClassProperties();
|
205 | // initialize Map populated in observedAttributes
|
206 | this._attributeToPropertyMap = new Map();
|
207 | // make any properties
|
208 | // Note, only process "own" properties since this element will inherit
|
209 | // any properties defined on the superClass, and finalization ensures
|
210 | // the entire prototype chain is finalized.
|
211 | if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) {
|
212 | const props = this.properties;
|
213 | // support symbols in properties (IE11 does not support this)
|
214 | const propKeys = [
|
215 | ...Object.getOwnPropertyNames(props),
|
216 | ...(typeof Object.getOwnPropertySymbols === 'function')
|
217 | ? Object.getOwnPropertySymbols(props)
|
218 | : []
|
219 | ];
|
220 | for (const p of propKeys) {
|
221 | // note, use of `any` is due to TypeSript lack of support for symbol in
|
222 | // index types
|
223 | this.createProperty(p, props[p]);
|
224 | }
|
225 | }
|
226 | }
|
227 | /**
|
228 | * Returns the property name for the given attribute `name`.
|
229 | * @nocollapse
|
230 | */
|
231 | static _attributeNameForProperty(name, options) {
|
232 | const attribute = options.attribute;
|
233 | return attribute === false
|
234 | ? undefined
|
235 | : (typeof attribute === 'string'
|
236 | ? attribute
|
237 | : (typeof name === 'string' ? name.toLowerCase()
|
238 | : undefined));
|
239 | }
|
240 | /**
|
241 | * Returns true if a property should request an update.
|
242 | * Called when a property value is set and uses the `hasChanged`
|
243 | * option for the property if present or a strict identity check.
|
244 | * @nocollapse
|
245 | */
|
246 | static _valueHasChanged(value, old, hasChanged = notEqual) {
|
247 | return hasChanged(value, old);
|
248 | }
|
249 | /**
|
250 | * Returns the property value for the given attribute value.
|
251 | * Called via the `attributeChangedCallback` and uses the property's
|
252 | * `converter` or `converter.fromAttribute` property option.
|
253 | * @nocollapse
|
254 | */
|
255 | static _propertyValueFromAttribute(value, options) {
|
256 | const type = options.type;
|
257 | const converter = options.converter || defaultConverter;
|
258 | const fromAttribute = (typeof converter === 'function' ? converter : converter.fromAttribute);
|
259 | return fromAttribute ? fromAttribute(value, type) : value;
|
260 | }
|
261 | /**
|
262 | * Returns the attribute value for the given property value. If this
|
263 | * returns undefined, the property will *not* be reflected to an attribute.
|
264 | * If this returns null, the attribute will be removed, otherwise the
|
265 | * attribute will be set to the value.
|
266 | * This uses the property's `reflect` and `type.toAttribute` property options.
|
267 | * @nocollapse
|
268 | */
|
269 | static _propertyValueToAttribute(value, options) {
|
270 | if (options.reflect === undefined) {
|
271 | return;
|
272 | }
|
273 | const type = options.type;
|
274 | const converter = options.converter;
|
275 | const toAttribute = converter && converter.toAttribute ||
|
276 | defaultConverter.toAttribute;
|
277 | return toAttribute(value, type);
|
278 | }
|
279 | /**
|
280 | * Performs element initialization. By default captures any pre-set values for
|
281 | * registered properties.
|
282 | */
|
283 | initialize() { this._saveInstanceProperties(); }
|
284 | /**
|
285 | * Fixes any properties set on the instance before upgrade time.
|
286 | * Otherwise these would shadow the accessor and break these properties.
|
287 | * The properties are stored in a Map which is played back after the
|
288 | * constructor runs. Note, on very old versions of Safari (<=9) or Chrome
|
289 | * (<=41), properties created for native platform properties like (`id` or
|
290 | * `name`) may not have default values set in the element constructor. On
|
291 | * these browsers native properties appear on instances and therefore their
|
292 | * default value will overwrite any element default (e.g. if the element sets
|
293 | * this.id = 'id' in the constructor, the 'id' will become '' since this is
|
294 | * the native platform default).
|
295 | */
|
296 | _saveInstanceProperties() {
|
297 | for (const [p] of this.constructor
|
298 | ._classProperties) {
|
299 | if (this.hasOwnProperty(p)) {
|
300 | const value = this[p];
|
301 | delete this[p];
|
302 | if (!this._instanceProperties) {
|
303 | this._instanceProperties = new Map();
|
304 | }
|
305 | this._instanceProperties.set(p, value);
|
306 | }
|
307 | }
|
308 | }
|
309 | /**
|
310 | * Applies previously saved instance properties.
|
311 | */
|
312 | _applyInstanceProperties() {
|
313 | for (const [p, v] of this._instanceProperties) {
|
314 | this[p] = v;
|
315 | }
|
316 | this._instanceProperties = undefined;
|
317 | }
|
318 | connectedCallback() {
|
319 | this._updateState = this._updateState | STATE_HAS_CONNECTED;
|
320 | // Ensure connection triggers an update. Updates cannot complete before
|
321 | // connection and if one is pending connection the `_hasConnectionResolver`
|
322 | // will exist. If so, resolve it to complete the update, otherwise
|
323 | // requestUpdate.
|
324 | if (this._hasConnectedResolver) {
|
325 | this._hasConnectedResolver();
|
326 | this._hasConnectedResolver = undefined;
|
327 | }
|
328 | else {
|
329 | this.requestUpdate();
|
330 | }
|
331 | }
|
332 | /**
|
333 | * Allows for `super.disconnectedCallback()` in extensions while
|
334 | * reserving the possibility of making non-breaking feature additions
|
335 | * when disconnecting at some point in the future.
|
336 | */
|
337 | disconnectedCallback() { }
|
338 | /**
|
339 | * Synchronizes property values when attributes change.
|
340 | */
|
341 | attributeChangedCallback(name, old, value) {
|
342 | if (old !== value) {
|
343 | this._attributeToProperty(name, value);
|
344 | }
|
345 | }
|
346 | _propertyToAttribute(name, value, options = defaultPropertyDeclaration) {
|
347 | const ctor = this.constructor;
|
348 | const attr = ctor._attributeNameForProperty(name, options);
|
349 | if (attr !== undefined) {
|
350 | const attrValue = ctor._propertyValueToAttribute(value, options);
|
351 | // an undefined value does not change the attribute.
|
352 | if (attrValue === undefined) {
|
353 | return;
|
354 | }
|
355 | // Track if the property is being reflected to avoid
|
356 | // setting the property again via `attributeChangedCallback`. Note:
|
357 | // 1. this takes advantage of the fact that the callback is synchronous.
|
358 | // 2. will behave incorrectly if multiple attributes are in the reaction
|
359 | // stack at time of calling. However, since we process attributes
|
360 | // in `update` this should not be possible (or an extreme corner case
|
361 | // that we'd like to discover).
|
362 | // mark state reflecting
|
363 | this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE;
|
364 | if (attrValue == null) {
|
365 | this.removeAttribute(attr);
|
366 | }
|
367 | else {
|
368 | this.setAttribute(attr, attrValue);
|
369 | }
|
370 | // mark state not reflecting
|
371 | this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE;
|
372 | }
|
373 | }
|
374 | _attributeToProperty(name, value) {
|
375 | // Use tracking info to avoid deserializing attribute value if it was
|
376 | // just set from a property setter.
|
377 | if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) {
|
378 | return;
|
379 | }
|
380 | const ctor = this.constructor;
|
381 | const propName = ctor._attributeToPropertyMap.get(name);
|
382 | if (propName !== undefined) {
|
383 | const options = ctor._classProperties.get(propName) || defaultPropertyDeclaration;
|
384 | // mark state reflecting
|
385 | this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY;
|
386 | this[propName] =
|
387 | ctor._propertyValueFromAttribute(value, options);
|
388 | // mark state not reflecting
|
389 | this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY;
|
390 | }
|
391 | }
|
392 | /**
|
393 | * Requests an update which is processed asynchronously. This should
|
394 | * be called when an element should update based on some state not triggered
|
395 | * by setting a property. In this case, pass no arguments. It should also be
|
396 | * called when manually implementing a property setter. In this case, pass the
|
397 | * property `name` and `oldValue` to ensure that any configured property
|
398 | * options are honored. Returns the `updateComplete` Promise which is resolved
|
399 | * when the update completes.
|
400 | *
|
401 | * @param name {PropertyKey} (optional) name of requesting property
|
402 | * @param oldValue {any} (optional) old value of requesting property
|
403 | * @returns {Promise} A Promise that is resolved when the update completes.
|
404 | */
|
405 | requestUpdate(name, oldValue) {
|
406 | let shouldRequestUpdate = true;
|
407 | // if we have a property key, perform property update steps.
|
408 | if (name !== undefined && !this._changedProperties.has(name)) {
|
409 | const ctor = this.constructor;
|
410 | const options = ctor._classProperties.get(name) || defaultPropertyDeclaration;
|
411 | if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) {
|
412 | // track old value when changing.
|
413 | this._changedProperties.set(name, oldValue);
|
414 | // add to reflecting properties set
|
415 | if (options.reflect === true &&
|
416 | !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {
|
417 | if (this._reflectingProperties === undefined) {
|
418 | this._reflectingProperties = new Map();
|
419 | }
|
420 | this._reflectingProperties.set(name, options);
|
421 | }
|
422 | // abort the request if the property should not be considered changed.
|
423 | }
|
424 | else {
|
425 | shouldRequestUpdate = false;
|
426 | }
|
427 | }
|
428 | if (!this._hasRequestedUpdate && shouldRequestUpdate) {
|
429 | this._enqueueUpdate();
|
430 | }
|
431 | return this.updateComplete;
|
432 | }
|
433 | /**
|
434 | * Sets up the element to asynchronously update.
|
435 | */
|
436 | async _enqueueUpdate() {
|
437 | // Mark state updating...
|
438 | this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
|
439 | let resolve;
|
440 | const previousUpdatePromise = this._updatePromise;
|
441 | this._updatePromise = new Promise((res) => resolve = res);
|
442 | // Ensure any previous update has resolved before updating.
|
443 | // This `await` also ensures that property changes are batched.
|
444 | await previousUpdatePromise;
|
445 | // Make sure the element has connected before updating.
|
446 | if (!this._hasConnected) {
|
447 | await new Promise((res) => this._hasConnectedResolver = res);
|
448 | }
|
449 | // Allow `performUpdate` to be asynchronous to enable scheduling of updates.
|
450 | const result = this.performUpdate();
|
451 | // Note, this is to avoid delaying an additional microtask unless we need
|
452 | // to.
|
453 | if (result != null &&
|
454 | typeof result.then === 'function') {
|
455 | await result;
|
456 | }
|
457 | resolve(!this._hasRequestedUpdate);
|
458 | }
|
459 | get _hasConnected() {
|
460 | return (this._updateState & STATE_HAS_CONNECTED);
|
461 | }
|
462 | get _hasRequestedUpdate() {
|
463 | return (this._updateState & STATE_UPDATE_REQUESTED);
|
464 | }
|
465 | get hasUpdated() { return (this._updateState & STATE_HAS_UPDATED); }
|
466 | /**
|
467 | * Performs an element update.
|
468 | *
|
469 | * You can override this method to change the timing of updates. For instance,
|
470 | * to schedule updates to occur just before the next frame:
|
471 | *
|
472 | * ```
|
473 | * protected async performUpdate(): Promise<unknown> {
|
474 | * await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
475 | * super.performUpdate();
|
476 | * }
|
477 | * ```
|
478 | */
|
479 | performUpdate() {
|
480 | // Mixin instance properties once, if they exist.
|
481 | if (this._instanceProperties) {
|
482 | this._applyInstanceProperties();
|
483 | }
|
484 | if (this.shouldUpdate(this._changedProperties)) {
|
485 | const changedProperties = this._changedProperties;
|
486 | this.update(changedProperties);
|
487 | this._markUpdated();
|
488 | if (!(this._updateState & STATE_HAS_UPDATED)) {
|
489 | this._updateState = this._updateState | STATE_HAS_UPDATED;
|
490 | this.firstUpdated(changedProperties);
|
491 | }
|
492 | this.updated(changedProperties);
|
493 | }
|
494 | else {
|
495 | this._markUpdated();
|
496 | }
|
497 | }
|
498 | _markUpdated() {
|
499 | this._changedProperties = new Map();
|
500 | this._updateState = this._updateState & ~STATE_UPDATE_REQUESTED;
|
501 | }
|
502 | /**
|
503 | * Returns a Promise that resolves when the element has completed updating.
|
504 | * The Promise value is a boolean that is `true` if the element completed the
|
505 | * update without triggering another update. The Promise result is `false` if
|
506 | * a property was set inside `updated()`. This getter can be implemented to
|
507 | * await additional state. For example, it is sometimes useful to await a
|
508 | * rendered element before fulfilling this Promise. To do this, first await
|
509 | * `super.updateComplete` then any subsequent state.
|
510 | *
|
511 | * @returns {Promise} The Promise returns a boolean that indicates if the
|
512 | * update resolved without triggering another update.
|
513 | */
|
514 | get updateComplete() { return this._updatePromise; }
|
515 | /**
|
516 | * Controls whether or not `update` should be called when the element requests
|
517 | * an update. By default, this method always returns `true`, but this can be
|
518 | * customized to control when to update.
|
519 | *
|
520 | * * @param _changedProperties Map of changed properties with old values
|
521 | */
|
522 | shouldUpdate(_changedProperties) {
|
523 | return true;
|
524 | }
|
525 | /**
|
526 | * Updates the element. This method reflects property values to attributes.
|
527 | * It can be overridden to render and keep updated element DOM.
|
528 | * Setting properties inside this method will *not* trigger
|
529 | * another update.
|
530 | *
|
531 | * * @param _changedProperties Map of changed properties with old values
|
532 | */
|
533 | update(_changedProperties) {
|
534 | if (this._reflectingProperties !== undefined &&
|
535 | this._reflectingProperties.size > 0) {
|
536 | for (const [k, v] of this._reflectingProperties) {
|
537 | this._propertyToAttribute(k, this[k], v);
|
538 | }
|
539 | this._reflectingProperties = undefined;
|
540 | }
|
541 | }
|
542 | /**
|
543 | * Invoked whenever the element is updated. Implement to perform
|
544 | * post-updating tasks via DOM APIs, for example, focusing an element.
|
545 | *
|
546 | * Setting properties inside this method will trigger the element to update
|
547 | * again after this update cycle completes.
|
548 | *
|
549 | * * @param _changedProperties Map of changed properties with old values
|
550 | */
|
551 | updated(_changedProperties) { }
|
552 | /**
|
553 | * Invoked when the element is first updated. Implement to perform one time
|
554 | * work on the element after update.
|
555 | *
|
556 | * Setting properties inside this method will trigger the element to update
|
557 | * again after this update cycle completes.
|
558 | *
|
559 | * * @param _changedProperties Map of changed properties with old values
|
560 | */
|
561 | firstUpdated(_changedProperties) { }
|
562 | }
|
563 | /**
|
564 | * Marks class as having finished creating properties.
|
565 | */
|
566 | UpdatingElement.finalized = true;
|
567 | //# sourceMappingURL=updating-element.js.map |
\ | No newline at end of file |