1 |
|
2 | /**
|
3 | * @license
|
4 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
5 | * This code may only be used under the BSD style license found at
|
6 | * http://polymer.github.io/LICENSE.txt
|
7 | * The complete set of authors may be found at
|
8 | * http://polymer.github.io/AUTHORS.txt
|
9 | * The complete set of contributors may be found at
|
10 | * http://polymer.github.io/CONTRIBUTORS.txt
|
11 | * Code distributed by Google as part of the polymer project is also
|
12 | * subject to an additional IP rights grant found at
|
13 | * http://polymer.github.io/PATENTS.txt
|
14 | */
|
15 |
|
16 | import {LitElement} from '../lit-element.js';
|
17 |
|
18 | import {PropertyDeclaration, UpdatingElement} from './updating-element.js';
|
19 |
|
20 | export type Constructor<T> = {
|
21 | new (...args: any[]): T
|
22 | };
|
23 |
|
24 | // From the TC39 Decorators proposal
|
25 | interface ClassDescriptor {
|
26 | kind: 'class';
|
27 | elements: ClassElement[];
|
28 | finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
|
29 | }
|
30 |
|
31 | // From the TC39 Decorators proposal
|
32 | interface ClassElement {
|
33 | kind: 'field'|'method';
|
34 | key: PropertyKey;
|
35 | placement: 'static'|'prototype'|'own';
|
36 | initializer?: Function;
|
37 | extras?: ClassElement[];
|
38 | finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
|
39 | descriptor?: PropertyDescriptor;
|
40 | }
|
41 |
|
42 | const legacyCustomElement =
|
43 | (tagName: string, clazz: Constructor<HTMLElement>) => {
|
44 | window.customElements.define(tagName, clazz);
|
45 | // Cast as any because TS doesn't recognize the return type as being a
|
46 | // subtype of the decorated class when clazz is typed as
|
47 | // `Constructor<HTMLElement>` for some reason.
|
48 | // `Constructor<HTMLElement>` is helpful to make sure the decorator is
|
49 | // applied to elements however.
|
50 | return clazz as any;
|
51 | };
|
52 |
|
53 | const standardCustomElement =
|
54 | (tagName: string, descriptor: ClassDescriptor) => {
|
55 | const {kind, elements} = descriptor;
|
56 | return {
|
57 | kind,
|
58 | elements,
|
59 | // This callback is called once the class is otherwise fully defined
|
60 | finisher(clazz: Constructor<HTMLElement>) {
|
61 | window.customElements.define(tagName, clazz);
|
62 | }
|
63 | };
|
64 | };
|
65 |
|
66 | /**
|
67 | * Class decorator factory that defines the decorated class as a custom element.
|
68 | *
|
69 | * @param tagName the name of the custom element to define
|
70 | *
|
71 | * In TypeScript, the `tagName` passed to `customElement` should be a key of the
|
72 | * `HTMLElementTagNameMap` interface. To add your element to the interface,
|
73 | * declare the interface in this module:
|
74 | *
|
75 | * @customElement('my-element')
|
76 | * export class MyElement extends LitElement {}
|
77 | *
|
78 | * declare global {
|
79 | * interface HTMLElementTagNameMap {
|
80 | * 'my-element': MyElement;
|
81 | * }
|
82 | * }
|
83 | *
|
84 | */
|
85 | export const customElement = (tagName: string) => (
|
86 | classOrDescriptor: Constructor<HTMLElement>|ClassDescriptor) =>
|
87 | (typeof classOrDescriptor === 'function')
|
88 | ? legacyCustomElement(tagName,
|
89 | classOrDescriptor as Constructor<HTMLElement>)
|
90 | : standardCustomElement(tagName, classOrDescriptor as ClassDescriptor);
|
91 |
|
92 | const standardProperty =
|
93 | (options: PropertyDeclaration, element: ClassElement) => {
|
94 | // createProperty() takes care of defining the property, but we still must
|
95 | // return some kind of descriptor, so return a descriptor for an unused
|
96 | // prototype field. The finisher calls createProperty().
|
97 | return {
|
98 | kind : 'field',
|
99 | key : Symbol(),
|
100 | placement : 'own',
|
101 | descriptor : {},
|
102 | // When @babel/plugin-proposal-decorators implements initializers,
|
103 | // do this instead of the initializer below. See:
|
104 | // https://github.com/babel/babel/issues/9260 extras: [
|
105 | // {
|
106 | // kind: 'initializer',
|
107 | // placement: 'own',
|
108 | // initializer: descriptor.initializer,
|
109 | // }
|
110 | // ],
|
111 | initializer(this: any) {
|
112 | if (typeof element.initializer === 'function') {
|
113 | this[element.key] = element.initializer!.call(this);
|
114 | }
|
115 | },
|
116 | finisher(clazz: typeof UpdatingElement) {
|
117 | clazz.createProperty(element.key, options);
|
118 | }
|
119 | };
|
120 | };
|
121 |
|
122 | const legacyProperty = (options: PropertyDeclaration, proto: Object,
|
123 | name: PropertyKey) => {
|
124 | (proto.constructor as typeof UpdatingElement).createProperty(name!, options);
|
125 | };
|
126 |
|
127 | /**
|
128 | * A property decorator which creates a LitElement property which reflects a
|
129 | * corresponding attribute value. A `PropertyDeclaration` may optionally be
|
130 | * supplied to configure property features.
|
131 | */
|
132 | export const property = (options?: PropertyDeclaration) =>
|
133 | (protoOrDescriptor: Object|ClassElement, name?: PropertyKey): any =>
|
134 | (name !== undefined)
|
135 | ? legacyProperty(options!, protoOrDescriptor as Object, name)
|
136 | : standardProperty(options!, protoOrDescriptor as ClassElement);
|
137 |
|
138 | /**
|
139 | * A property decorator that converts a class property into a getter that
|
140 | * executes a querySelector on the element's renderRoot.
|
141 | */
|
142 | export const query = _query((target: NodeSelector, selector: string) =>
|
143 | target.querySelector(selector));
|
144 |
|
145 | /**
|
146 | * A property decorator that converts a class property into a getter
|
147 | * that executes a querySelectorAll on the element's renderRoot.
|
148 | */
|
149 | export const queryAll = _query((target: NodeSelector, selector: string) =>
|
150 | target.querySelectorAll(selector));
|
151 |
|
152 | const legacyQuery =
|
153 | (descriptor: PropertyDescriptor, proto: Object,
|
154 | name: PropertyKey) => { Object.defineProperty(proto, name, descriptor); };
|
155 |
|
156 | const standardQuery = (descriptor: PropertyDescriptor, element: ClassElement) =>
|
157 | ({
|
158 | kind : 'method',
|
159 | placement : 'prototype',
|
160 | key : element.key,
|
161 | descriptor,
|
162 | });
|
163 |
|
164 | /**
|
165 | * Base-implementation of `@query` and `@queryAll` decorators.
|
166 | *
|
167 | * @param queryFn exectute a `selector` (ie, querySelector or querySelectorAll)
|
168 | * against `target`.
|
169 | */
|
170 | function _query<T>(queryFn: (target: NodeSelector, selector: string) => T) {
|
171 | return (selector: string) => (protoOrDescriptor: Object|ClassElement,
|
172 | name?: PropertyKey): any => {
|
173 | const descriptor = {
|
174 | get(this: LitElement) { return queryFn(this.renderRoot!, selector); },
|
175 | enumerable : true,
|
176 | configurable : true,
|
177 | };
|
178 | return (name !== undefined)
|
179 | ? legacyQuery(descriptor, protoOrDescriptor as Object, name)
|
180 | : standardQuery(descriptor, protoOrDescriptor as ClassElement);
|
181 | };
|
182 | }
|
183 |
|
184 | const standardEventOptions =
|
185 | (options: AddEventListenerOptions, element: ClassElement) => {
|
186 | return {
|
187 | ...element,
|
188 | finisher(clazz: typeof UpdatingElement) {
|
189 | Object.assign(clazz.prototype[element.key as keyof UpdatingElement],
|
190 | options);
|
191 | }
|
192 | };
|
193 | };
|
194 |
|
195 | const legacyEventOptions =
|
196 | (options: AddEventListenerOptions, proto: any,
|
197 | name: PropertyKey) => { Object.assign(proto[name], options); };
|
198 |
|
199 | /**
|
200 | * Adds event listener options to a method used as an event listener in a
|
201 | * lit-html template.
|
202 | *
|
203 | * @param options An object that specifis event listener options as accepted by
|
204 | * `EventTarget#addEventListener` and `EventTarget#removeEventListener`.
|
205 | *
|
206 | * Current browsers support the `capture`, `passive`, and `once` options. See:
|
207 | * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters
|
208 | *
|
209 | * @example
|
210 | *
|
211 | * class MyElement {
|
212 | *
|
213 | * clicked = false;
|
214 | *
|
215 | * render() {
|
216 | * return html`<div @click=${this._onClick}`><button></button></div>`;
|
217 | * }
|
218 | *
|
219 | * @eventOptions({capture: true})
|
220 | * _onClick(e) {
|
221 | * this.clicked = true;
|
222 | * }
|
223 | * }
|
224 | */
|
225 | export const eventOptions = (options: AddEventListenerOptions) =>
|
226 | // Return value typed as any to prevent TypeScript from complaining that
|
227 | // standard decorator function signature does not match TypeScript decorator
|
228 | // signature
|
229 | // TODO(kschaaf): unclear why it was only failing on this decorator and not
|
230 | // the others
|
231 | ((protoOrDescriptor: Object|ClassElement, name?: string) =>
|
232 | (name !== undefined)
|
233 | ? legacyEventOptions(options, protoOrDescriptor as Object, name)
|
234 | : standardEventOptions(options,
|
235 | protoOrDescriptor as ClassElement)) as any;
|