-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathattrable.ts
More file actions
130 lines (120 loc) · 4.27 KB
/
attrable.ts
File metadata and controls
130 lines (120 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import type {CustomElementClass} from './custom-element.js'
import type {ControllableClass} from './controllable.js'
import {controllable} from './controllable.js'
import {dasherize, mustDasherize} from './dasherize.js'
import {createMark} from './mark.js'
import {createAbility} from './ability.js'
const attrChangedCallback = Symbol()
const serializeAttributeName = Symbol()
export interface Attrable {
[key: PropertyKey]: unknown
[serializeAttributeName](name: PropertyKey): string
[attrChangedCallback](changed: Map<PropertyKey, unknown>): void
}
export interface AttrableClass {
new (): Attrable
}
export const deprecatedDataPrefixedAttrs = createAbility(
<T extends CustomElementClass>(Class: T): T =>
class extends controllable(Class) {
[serializeAttributeName](name: PropertyKey) {
return `data-${dasherize(name)}`
}
}
)
const Identity = (v: unknown) => v
let setFromMutation = false
const attrs = new WeakMap<Element, Map<string, PropertyKey>>()
const handleMutations = (mutations: MutationRecord[]) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes') {
const name = mutation.attributeName!
const el = mutation.target as Element & {[key: PropertyKey]: unknown}
const key = attrs.get(el)?.get(name)
if (key) {
setFromMutation = true
el[key] = el.getAttribute(name)
setFromMutation = false
}
}
}
}
const observer = new MutationObserver(handleMutations)
const [attr, getAttr, initializeAttrs] = createMark<Element & Attrable>(
({name}) => mustDasherize(name, '@attr'),
(instance: Element & Attrable, {name, kind, access}) => {
let cast: typeof Identity | typeof Boolean | typeof Number | typeof String = Identity
let initialValue: unknown
if (access.get) {
initialValue = access.get.call(instance)
} else if ('value' in access && kind !== 'method') {
initialValue = access.value
}
let value = initialValue
const attributeName = instance[serializeAttributeName](name)
const setCallback = (kind === 'method' ? access.value : access.set) || Identity
const getCallback = access.get || (() => value)
if (!attrs.get(instance)) attrs.set(instance, new Map())
attrs.get(instance)!.set(attributeName, name)
if (typeof value === 'number') {
cast = Number
} else if (typeof value === 'boolean') {
cast = Boolean
} else if (typeof value === 'string') {
cast = String
}
const queue = new Map()
const requestAttrChanged = async (newValue: unknown) => {
queue.set(name, newValue)
if (queue.size > 1) return
await Promise.resolve()
const changed = new Map(queue)
queue.clear()
instance[attrChangedCallback](changed)
}
return {
get() {
const has = instance.hasAttribute(attributeName)
if (has) {
return cast === Boolean ? has : cast(instance.getAttribute(attributeName))
}
return cast(getCallback.call(instance))
},
set(newValue: unknown) {
const isInitial = newValue === null
if (isInitial) newValue = initialValue
const same = Object.is(value, newValue)
value = newValue
setCallback.call(instance, value)
if (setFromMutation || same || isInitial) return
requestAttrChanged(newValue)
}
}
}
)
export {attr, getAttr, attrChangedCallback}
export const attrable = createAbility(
<T extends CustomElementClass>(Class: T): T & ControllableClass & AttrableClass =>
class extends controllable(Class) {
[key: PropertyKey]: unknown
constructor() {
super()
initializeAttrs(this)
const attributeFilter = Array.from(getAttr(this)).map(name => this[serializeAttributeName](name))
observer.observe(this, {attributeFilter})
}
[serializeAttributeName](name: PropertyKey) {
return dasherize(name)
}
[attrChangedCallback](changed: Map<PropertyKey, unknown>) {
if (!this.isConnected) return
for (const [name, value] of changed) {
if (typeof value === 'boolean') {
this.toggleAttribute(this[serializeAttributeName](name), value)
} else {
this.setAttribute(this[serializeAttributeName](name), String(value))
}
}
}
}
)