@rhtml/custom-attributes 0.0.112 → 0.0.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ import { Attribute } from './attribute';
2
+ import { observe } from './helpers';
3
+ import { Constructor } from './types';
4
+
5
+ export class CustomAttributeRegistry {
6
+ private _attrMap: Map<string, Constructor<Attribute>> = new Map();
7
+ private _elementMap: Map<HTMLElement, Map<string, Attribute>> = new Map();
8
+ private observer: MutationObserver;
9
+
10
+ constructor(private parent: HTMLElement) {
11
+ if (!parent) {
12
+ throw new Error('Must be given a parent element');
13
+ }
14
+ this.observe();
15
+ }
16
+
17
+ define(attrName: string, Constructor: Constructor<Attribute>) {
18
+ this._attrMap.set(attrName.toLowerCase(), Constructor);
19
+ this.upgradeAttribute(attrName);
20
+ }
21
+
22
+ get(element: HTMLElement, attrName: string) {
23
+ const map = this._elementMap.get(element);
24
+ if (!map) return;
25
+ return map.get(attrName.toLowerCase());
26
+ }
27
+
28
+ private getConstructor(attrName: string) {
29
+ return this._attrMap.get(attrName.toLowerCase());
30
+ }
31
+
32
+ private observe() {
33
+ this.observer = new MutationObserver(mutations => {
34
+ for (const mutation of mutations) {
35
+ if (mutation.type === 'attributes') {
36
+ const attr = this.getConstructor(mutation.attributeName);
37
+ if (attr) {
38
+ this.found(
39
+ mutation.attributeName,
40
+ mutation.target as never,
41
+ mutation.oldValue
42
+ );
43
+ }
44
+ } else {
45
+ for (const node of mutation.removedNodes) {
46
+ this.downgrade(node as never);
47
+ }
48
+ for (const node of mutation.addedNodes) {
49
+ this.upgradeElement(node as never);
50
+ }
51
+ }
52
+ }
53
+ });
54
+ this.observer.observe(this.parent?.['shadowRoot'] ?? this.parent, {
55
+ childList: true,
56
+ subtree: true,
57
+ attributes: true,
58
+ attributeOldValue: true
59
+ });
60
+ }
61
+
62
+ unsubscribe() {
63
+ this.observer?.disconnect();
64
+ const values = [...this._elementMap.values()];
65
+ for (const elModifiers of values) {
66
+ const modifiers = [...elModifiers.values()];
67
+ for (const modifier of modifiers) {
68
+ modifier.OnDestroy();
69
+ if (modifier.observer) {
70
+ modifier.observer.disconnect();
71
+ }
72
+ }
73
+ elModifiers.clear();
74
+ }
75
+ this._elementMap.clear();
76
+ }
77
+
78
+ private upgradeAttribute(attrName: string, doc?: HTMLElement) {
79
+ const parent = doc || this.parent;
80
+
81
+ const matches = parent.querySelectorAll('[' + attrName + ']');
82
+
83
+ for (const match of [...matches]) {
84
+ this.found(attrName, match as never);
85
+ }
86
+ }
87
+
88
+ private upgradeElement(element: HTMLElement) {
89
+ if (element.nodeType !== 1) {
90
+ return;
91
+ }
92
+ for (const attr of element.attributes) {
93
+ if (this.getConstructor(attr.name)) {
94
+ this.found(attr.name, element);
95
+ }
96
+ }
97
+ for (const [attr] of this._attrMap) {
98
+ this.upgradeAttribute(attr, element);
99
+ }
100
+ }
101
+
102
+ private downgrade(element: HTMLElement) {
103
+ const map = this._elementMap.get(element);
104
+ if (!map) {
105
+ return;
106
+ }
107
+ for (const [, instance] of map) {
108
+ if (instance.OnDestroy) {
109
+ instance.OnDestroy();
110
+ }
111
+ }
112
+ this._elementMap.delete(element);
113
+ }
114
+
115
+ private found(attributeName: string, el: HTMLElement, oldVal?: string) {
116
+ let map = this._elementMap.get(el);
117
+ if (!map) {
118
+ map = new Map();
119
+ this._elementMap.set(el, map);
120
+ }
121
+
122
+ let modifier = map.get(attributeName);
123
+ const attribute = el.getAttribute(attributeName);
124
+
125
+ if (!modifier) {
126
+ const Modifier = this.getConstructor(attributeName);
127
+ modifier = new Modifier();
128
+ if (Modifier.options?.observedAttributes?.length) {
129
+ for (const observedAttribute of Modifier.options.observedAttributes) {
130
+ observe(modifier, observedAttribute);
131
+ }
132
+ }
133
+ modifier.element = el;
134
+ modifier.selector = attributeName;
135
+
136
+ modifier.value = attribute || modifier.value;
137
+ modifier.parent = this.parent;
138
+
139
+ if (modifier.OnInit) {
140
+ modifier.OnInit();
141
+ }
142
+ if (Modifier.options.observe) {
143
+ modifier.observer = new MutationObserver(records =>
144
+ modifier.OnChange(records)
145
+ );
146
+ modifier.observer.observe(modifier.element, Modifier.options.observe);
147
+ }
148
+ map.set(attributeName, modifier);
149
+ return;
150
+ }
151
+
152
+ if (attribute == null && !!modifier.value) {
153
+ modifier.value = attribute;
154
+ if (modifier.OnDestroy) {
155
+ modifier.OnDestroy();
156
+ }
157
+ if (modifier.observer) {
158
+ modifier.observer.disconnect();
159
+ }
160
+ map.delete(attributeName);
161
+ return;
162
+ }
163
+ if (attribute !== modifier.value) {
164
+ modifier.value = attribute;
165
+ if (modifier.OnUpdate) {
166
+ modifier.OnUpdate(oldVal, attribute);
167
+ }
168
+ return;
169
+ }
170
+ }
171
+ }
@@ -0,0 +1,58 @@
1
+ import { noop, observe } from '../helpers';
2
+ import { InputOptions, ModifierOptions } from '../types';
3
+
4
+ /**
5
+ * Decorator @Input
6
+ * Used to get attribute from element
7
+ */
8
+ export const Input = (options?: InputOptions) => (
9
+ target,
10
+ memberName: string
11
+ ) => {
12
+ const OnInit = target.OnInit || noop;
13
+ Object.defineProperty(target, 'OnInit', {
14
+ value() {
15
+ let originalValue = this[memberName];
16
+
17
+ const element = this.element ?? this;
18
+ Object.defineProperty(this, memberName, {
19
+ get: function() {
20
+ originalValue = element.getAttribute(memberName.toLowerCase());
21
+ return originalValue;
22
+ },
23
+ set(value) {
24
+ element.setAttribute(memberName.toLowerCase(), value);
25
+ originalValue = value;
26
+ },
27
+ configurable: true
28
+ });
29
+ return OnInit.call(this);
30
+ },
31
+ configurable: true
32
+ });
33
+
34
+ if (options?.observe) {
35
+ observe(target, memberName);
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Decorator @Modifier
41
+ * Accepts parameter options with selector and registry
42
+ */
43
+ export const Modifier = (options: ModifierOptions) => {
44
+ return (target: Function) => {
45
+ target['options'] = options;
46
+ };
47
+ };
48
+
49
+ /**
50
+ * Decorator @CustomAttribute
51
+ * Accepts parameter options with selector and registry
52
+ */
53
+ export const CustomAttribute = Modifier;
54
+ /**
55
+ * Decorator @Directive
56
+ * Accepts parameter options with selector and registry
57
+ */
58
+ export const Directive = Modifier;
@@ -0,0 +1,35 @@
1
+ export const noop = function() {
2
+ /* */
3
+ };
4
+
5
+ export const observe = (target: unknown, memberName: string) => {
6
+ const prototype = target.constructor.prototype;
7
+ const OnInit = prototype.OnInit || noop;
8
+ const OnDestroy = prototype.OnDestroy || noop;
9
+ const OnUpdateAttribute = prototype.OnUpdateAttribute || noop;
10
+
11
+ let observer: MutationObserver;
12
+ prototype.OnInit = function() {
13
+ const element = this.element ?? this;
14
+ if (observer) {
15
+ observer.disconnect();
16
+ }
17
+ observer = new MutationObserver(() => {
18
+ OnUpdateAttribute.call(
19
+ this,
20
+ memberName,
21
+ element.getAttribute(memberName)
22
+ );
23
+ target[memberName] = element.getAttribute(memberName);
24
+ });
25
+ observer.observe(element, {
26
+ attributeFilter: [memberName],
27
+ attributes: true
28
+ });
29
+ return OnInit.call(this);
30
+ };
31
+ prototype.OnDestroy = function() {
32
+ observer.disconnect();
33
+ return OnDestroy.call(this);
34
+ };
35
+ };
package/src/index.ts CHANGED
@@ -1,388 +1,6 @@
1
- export type C<T> = new (...args: never[]) => T;
2
-
3
- export interface Constructor<T> extends C<T> {
4
- options: ModifierOptions;
5
- }
6
-
7
- const noop = function() {
8
- /* */
9
- };
10
-
11
- interface ModifierOptions {
12
- selector: string;
13
- registry?(this: HTMLElement): CustomAttributeRegistry;
14
- observedAttributes?: string[];
15
- }
16
-
17
- interface InputOptions {
18
- /**
19
- * If enabled will trigger OnUpdate method on the Attribute
20
- * */
21
- observe: true;
22
- }
23
-
24
- const observe = (target: unknown, memberName: string) => {
25
- const prototype = target.constructor.prototype;
26
- const OnInit = prototype.OnInit || noop;
27
- const OnDestroy = prototype.OnDestroy || noop;
28
- const OnUpdateAttribute = prototype.OnUpdateAttribute || noop;
29
-
30
- let observer: MutationObserver;
31
- prototype.OnInit = function() {
32
- const element = this.element ?? this;
33
- if (observer) {
34
- observer.disconnect();
35
- }
36
- observer = new MutationObserver(() => {
37
- OnUpdateAttribute.call(
38
- this,
39
- memberName,
40
- element.getAttribute(memberName)
41
- );
42
- target[memberName] = element.getAttribute(memberName);
43
- });
44
- observer.observe(element, {
45
- attributeFilter: [memberName],
46
- attributes: true
47
- });
48
- return OnInit.call(this);
49
- };
50
- prototype.OnDestroy = function() {
51
- observer.disconnect();
52
- return OnDestroy.call(this);
53
- };
54
- };
55
-
56
- /**
57
- * Decorator @Input
58
- * Used to get attribute from element
59
- */
60
- export const Input = (options?: InputOptions) => (
61
- target,
62
- memberName: string
63
- ) => {
64
- const OnInit = target.OnInit || noop;
65
- Object.defineProperty(target, 'OnInit', {
66
- value() {
67
- let originalValue = this[memberName];
68
-
69
- const element = this.element ?? this;
70
- Object.defineProperty(this, memberName, {
71
- get: function() {
72
- originalValue = element.getAttribute(memberName.toLowerCase());
73
- return originalValue;
74
- },
75
- set(value) {
76
- element.setAttribute(memberName.toLowerCase(), value);
77
- originalValue = value;
78
- },
79
- configurable: true
80
- });
81
- return OnInit.call(this);
82
- },
83
- configurable: true
84
- });
85
-
86
- if (options?.observe) {
87
- observe(target, memberName);
88
- }
89
- };
90
-
91
- /**
92
- * Decorator @Modifier
93
- * Accepts parameter options with selector and registry
94
- */
95
- export const Modifier = (options: ModifierOptions) => {
96
- return (target: Function) => {
97
- target['options'] = options;
98
- };
99
- };
100
-
101
- /**
102
- * Decorator @CustomAttribute
103
- * Accepts parameter options with selector and registry
104
- */
105
- export const CustomAttribute = Modifier;
106
- /**
107
- * Decorator @Directive
108
- * Accepts parameter options with selector and registry
109
- */
110
- export const Directive = Modifier;
111
-
112
- /* */
113
- export abstract class Attribute<T = {}> {
114
- public static options: ModifierOptions;
115
- public element?: HTMLElement;
116
- public value?: string;
117
- public selector?: string;
118
- public parent?: HTMLElement;
119
- setStyles(keys: T) {
120
- return (div: HTMLElement | Element | HTMLDivElement) => {
121
- for (const [key, value] of Object.entries(keys)) {
122
- div['style'][key] = value;
123
- }
124
- };
125
- }
126
- OnInit(): void {
127
- /* */
128
- }
129
- OnDestroy(): void {
130
- /* */
131
- }
132
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
133
- OnUpdate(_oldValue: string, _newValue: string) {
134
- /* */
135
- }
136
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
137
- OnUpdateAttribute(_name: string, _value: string) {
138
- /* */
139
- }
140
- }
141
-
142
- /**
143
- * Media query Attribute
144
- * for performance reasons it is key value pair
145
- */
146
- export const MediaMatchers = new Map([
147
- ['screen and (max-width: 599px)', 'xs'],
148
- ['screen and (min-width: 600px) and (max-width: 959px)', 'sm'],
149
- ['screen and (min-width: 960px) and (max-width: 1279px)', 'md'],
150
- ['screen and (min-width: 1280px) and (max-width: 1919px)', 'lg'],
151
- ['screen and (min-width: 1920px) and (max-width: 5000px)', 'xl'],
152
- ['screen and (max-width: 959px)', 'lt-md'],
153
- ['screen and (max-width: 1279px)', 'lt-lg'],
154
- ['screen and (max-width: 1919px)', 'lt-xl'],
155
- ['screen and (min-width: 600px)', 'gt-xs'],
156
- ['screen and (min-width: 960px)', 'gt-sm'],
157
- ['screen and (min-width: 1280px)', 'gt-md'],
158
- ['screen and (min-width: 1920px)', 'gt-lg']
159
- ]);
160
- type MediaEvent = MediaQueryList | MediaQueryListEvent;
161
- export type EnterMediaQueryAttributes = [MediaEvent, Attr];
162
- export type ExitMediaQueryAttributes = [MediaEvent, string];
163
-
164
- export interface OnUpdateMediaQuery {
165
- OnEnterMediaQuery(tuple: [MediaEvent, Attr]): void;
166
- OnExitMediaQuery(tuple: [MediaEvent, string]): void;
167
- }
168
-
169
- export const Breakpoints = [...MediaMatchers.values()];
170
-
171
- export const createFiltersFromSelector = (selector: string) => [
172
- ...Breakpoints.map(breakpoint => `${selector}.${breakpoint}`),
173
- selector
174
- ];
175
-
176
- export abstract class MediaQueryAttribute<T> extends Attribute<T>
177
- implements OnUpdateMediaQuery {
178
- prevValue: string;
179
- originalValue: string;
180
-
181
- private matchers: Map<MediaQueryList, MediaQueryList> = new Map();
182
- private cachedAttributes: Map<string, Attr> = new Map();
183
-
184
- listener = (event: MediaQueryList | MediaQueryListEvent) => {
185
- const key = `${this.selector.toLowerCase()}.${MediaMatchers.get(
186
- event.media
187
- )}`;
188
- const attribute = this.cachedAttributes.get(key);
189
-
190
- if (event.matches && attribute) {
191
- return this.OnEnterMediaQuery([event, attribute]);
192
- }
193
- return this.OnExitMediaQuery([event, key]);
194
- };
195
-
196
- OnInit() {
197
- if (this.OnEnterMediaQuery || this.OnExitMediaQuery) {
198
- this.originalValue = this.value;
199
- for (const query of MediaMatchers.keys()) {
200
- const matcher = window.matchMedia(query);
201
-
202
- const attr = Object.values(this.element.attributes).find(
203
- v =>
204
- v.name ===
205
- `${this.selector.toLowerCase()}.${MediaMatchers.get(query)}`
206
- );
207
-
208
- if (attr) {
209
- this.cachedAttributes.set(attr.name, attr);
210
- matcher.addEventListener('change', this.listener);
211
- }
212
-
213
- if (attr && matcher.matches) {
214
- this.listener(matcher);
215
- }
216
- this.matchers.set(matcher, matcher);
217
- }
218
- }
219
- }
220
-
221
- OnDestroy() {
222
- for (const matcher of this.matchers.values()) {
223
- matcher.removeEventListener('change', this.listener);
224
- }
225
- this.cachedAttributes.clear();
226
- this.matchers.clear();
227
- }
228
-
229
- abstract OnEnterMediaQuery(tuple: [MediaEvent, Attr]): void;
230
- abstract OnExitMediaQuery(tuple: [MediaEvent, string]): void;
231
- }
232
-
233
- /* Media query Attribute END */
234
-
235
- export class CustomAttributeRegistry {
236
- private _attrMap: Map<string, Constructor<Attribute>> = new Map();
237
- private _elementMap: Map<HTMLElement, Map<string, Attribute>> = new Map();
238
- private observer: MutationObserver;
239
-
240
- constructor(private parent: HTMLElement) {
241
- if (!parent) {
242
- throw new Error('Must be given a parent element');
243
- }
244
- this.observe();
245
- }
246
-
247
- define(attrName: string, Constructor: Constructor<Attribute>) {
248
- this._attrMap.set(attrName.toLowerCase(), Constructor);
249
- this.upgradeAttribute(attrName);
250
- }
251
-
252
- get(element: HTMLElement, attrName: string) {
253
- const map = this._elementMap.get(element);
254
- if (!map) return;
255
- return map.get(attrName.toLowerCase());
256
- }
257
-
258
- private getConstructor(attrName: string) {
259
- return this._attrMap.get(attrName.toLowerCase());
260
- }
261
-
262
- private observe() {
263
- this.observer = new MutationObserver(mutations => {
264
- for (const mutation of mutations) {
265
- if (mutation.type === 'attributes') {
266
- const attr = this.getConstructor(mutation.attributeName);
267
- if (attr) {
268
- this.found(
269
- mutation.attributeName,
270
- mutation.target as never,
271
- mutation.oldValue
272
- );
273
- }
274
- } else {
275
- for (const node of mutation.removedNodes) {
276
- this.downgrade(node as never);
277
- }
278
- for (const node of mutation.addedNodes) {
279
- this.upgradeElement(node as never);
280
- }
281
- }
282
- }
283
- });
284
- this.observer.observe(this.parent?.['shadowRoot'] ?? this.parent, {
285
- childList: true,
286
- subtree: true,
287
- attributes: true,
288
- attributeOldValue: true
289
- });
290
- }
291
-
292
- unsubscribe() {
293
- this.observer?.disconnect();
294
- const values = [...this._elementMap.values()];
295
- for (const elModifiers of values) {
296
- const modifiers = [...elModifiers.values()];
297
- for (const modifier of modifiers) {
298
- modifier.OnDestroy();
299
- }
300
- elModifiers.clear();
301
- }
302
- this._elementMap.clear();
303
- }
304
-
305
- private upgradeAttribute(attrName: string, doc?: HTMLElement) {
306
- const parent = doc || this.parent;
307
-
308
- const matches = parent.querySelectorAll('[' + attrName + ']');
309
-
310
- for (const match of [...matches]) {
311
- this.found(attrName, match as never);
312
- }
313
- }
314
-
315
- private upgradeElement(element: HTMLElement) {
316
- if (element.nodeType !== 1) {
317
- return;
318
- }
319
- for (const attr of element.attributes) {
320
- if (this.getConstructor(attr.name)) {
321
- this.found(attr.name, element);
322
- }
323
- }
324
- for (const [attr] of this._attrMap) {
325
- this.upgradeAttribute(attr, element);
326
- }
327
- }
328
-
329
- private downgrade(element: HTMLElement) {
330
- const map = this._elementMap.get(element);
331
- if (!map) {
332
- return;
333
- }
334
- for (const [, instance] of map) {
335
- if (instance.OnDestroy) {
336
- instance.OnDestroy();
337
- }
338
- }
339
- this._elementMap.delete(element);
340
- }
341
-
342
- private found(attributeName: string, el: HTMLElement, oldVal?: string) {
343
- let map = this._elementMap.get(el);
344
- if (!map) {
345
- map = new Map();
346
- this._elementMap.set(el, map);
347
- }
348
-
349
- const modifier = map.get(attributeName);
350
- const attribute = el.getAttribute(attributeName);
351
-
352
- if (!modifier) {
353
- const Modifier = this.getConstructor(attributeName);
354
- const modifier = new Modifier();
355
- if (Modifier.options?.observedAttributes?.length) {
356
- for (const observedAttribute of Modifier.options.observedAttributes) {
357
- observe(modifier, observedAttribute);
358
- }
359
- }
360
- map.set(attributeName, modifier);
361
- modifier.element = el;
362
- modifier.selector = attributeName;
363
- modifier.value = attribute || modifier.value;
364
- modifier.parent = this.parent;
365
-
366
- if (modifier.OnInit) {
367
- modifier.OnInit();
368
- }
369
- return;
370
- }
371
-
372
- if (attribute == null && !!modifier.value) {
373
- modifier.value = attribute;
374
- if (modifier.OnDestroy) {
375
- modifier.OnDestroy();
376
- }
377
- map.delete(attributeName);
378
- return;
379
- }
380
- if (attribute !== modifier.value) {
381
- modifier.value = attribute;
382
- if (modifier.OnUpdate) {
383
- modifier.OnUpdate(oldVal, attribute);
384
- }
385
- return;
386
- }
387
- }
388
- }
1
+ export * from './attribute';
2
+ export * from './custom-registry';
3
+ export * from './decorators';
4
+ export * from './helpers';
5
+ export * from './media-attribute';
6
+ export * from './types';