live_cable 0.0.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +1275 -0
- data/app/assets/javascript/controllers/live_controller.js +79 -44
- data/app/assets/javascript/dom.js +161 -0
- data/app/assets/javascript/live_cable.js +50 -0
- data/app/assets/javascript/live_cable_blessing.js +1 -1
- data/app/assets/javascript/observer.js +74 -0
- data/app/assets/javascript/subscriptions.js +396 -37
- data/app/channels/live_channel.rb +17 -14
- data/app/helpers/live_cable_helper.rb +28 -39
- data/config/importmap.rb +9 -3
- data/lib/generators/live_cable/component/component_generator.rb +58 -0
- data/lib/generators/live_cable/component/templates/component.rb.tt +29 -0
- data/lib/generators/live_cable/component/templates/view.html.live.erb.tt +2 -0
- data/lib/live_cable/component/broadcasting.rb +30 -0
- data/lib/live_cable/component/identification.rb +31 -0
- data/lib/live_cable/component/lifecycle.rb +67 -0
- data/lib/live_cable/component/method_dependency_tracking.rb +22 -0
- data/lib/live_cable/component/reactive_variables.rb +125 -0
- data/lib/live_cable/component/rendering.rb +177 -0
- data/lib/live_cable/component/streaming.rb +43 -0
- data/lib/live_cable/component.rb +21 -236
- data/lib/live_cable/configuration.rb +29 -0
- data/lib/live_cable/connection/broadcasting.rb +33 -0
- data/lib/live_cable/connection/channel_management.rb +13 -0
- data/lib/live_cable/connection/component_management.rb +38 -0
- data/lib/live_cable/connection/error_handling.rb +40 -0
- data/lib/live_cable/connection/messaging.rb +84 -0
- data/lib/live_cable/connection/state_management.rb +56 -0
- data/lib/live_cable/connection.rb +11 -180
- data/lib/live_cable/container.rb +25 -0
- data/lib/live_cable/delegation/array.rb +1 -0
- data/lib/live_cable/delegator.rb +0 -7
- data/lib/live_cable/engine.rb +15 -3
- data/lib/live_cable/observer.rb +5 -1
- data/lib/live_cable/observer_tracking.rb +20 -0
- data/lib/live_cable/render_context.rb +55 -8
- data/lib/live_cable/rendering/compiler.rb +80 -0
- data/lib/live_cable/rendering/dependency_visitor.rb +100 -0
- data/lib/live_cable/rendering/handler.rb +19 -0
- data/lib/live_cable/rendering/method_analyzer.rb +94 -0
- data/lib/live_cable/rendering/method_collector.rb +51 -0
- data/lib/live_cable/rendering/method_dependency_visitor.rb +51 -0
- data/lib/live_cable/rendering/partial.rb +93 -0
- data/lib/live_cable/rendering/partial_renderer.rb +145 -0
- data/lib/live_cable/rendering/render_result.rb +38 -0
- data/lib/live_cable/rendering/renderer.rb +150 -0
- data/lib/live_cable/version.rb +5 -0
- data/lib/live_cable.rb +15 -15
- metadata +124 -4
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import SubscriptionManager from "
|
|
2
|
+
import SubscriptionManager from "@isometriks/live_cable/subscriptions"
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
5
|
static values = {
|
|
6
6
|
defaults: Object,
|
|
7
7
|
status: String,
|
|
8
8
|
component: String,
|
|
9
|
-
|
|
9
|
+
actions: Array,
|
|
10
|
+
id: String,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
#subscription
|
|
13
|
-
#
|
|
14
|
-
#reactiveDebounce
|
|
15
|
-
#reactiveDebouncedMessage
|
|
14
|
+
#debounces = new Map()
|
|
16
15
|
|
|
17
16
|
#callActionCallback = (event) => {
|
|
18
17
|
event.stopPropagation()
|
|
@@ -26,24 +25,31 @@ export default class extends Controller {
|
|
|
26
25
|
this.element.addEventListener("call", this.#callActionCallback)
|
|
27
26
|
|
|
28
27
|
this.#subscription = SubscriptionManager.subscribe(
|
|
29
|
-
this.
|
|
28
|
+
this.idValue,
|
|
30
29
|
this.componentValue,
|
|
31
30
|
this.defaultsValue,
|
|
32
31
|
this
|
|
33
32
|
)
|
|
33
|
+
|
|
34
|
+
// Create callbacks for each action or form
|
|
35
|
+
this.actionsValue.forEach((action) => {
|
|
36
|
+
this[`action_$${action}`] = ({ params }) => {
|
|
37
|
+
this.sendCall(action, this.#convertKeysToSnakeCase(params))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this[`form_$${action}`] = (event) => {
|
|
41
|
+
this.#form(action, event)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
disconnect() {
|
|
37
47
|
this.element.removeEventListener("call", this.#callActionCallback)
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
call({ params }) {
|
|
41
|
-
this.sendCall(params.action, params)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
50
|
sendCall(action, params = {}) {
|
|
45
51
|
this.#subscription.send(
|
|
46
|
-
this.#
|
|
52
|
+
this.#flushDebounced(this.#callMessage(params, action))
|
|
47
53
|
)
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -54,13 +60,31 @@ export default class extends Controller {
|
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
#convertKeysToSnakeCase(params) {
|
|
64
|
+
return Object.fromEntries(
|
|
65
|
+
Object.entries(params).map(([key, value]) => [
|
|
66
|
+
key.replace(/([A-Z])/g, '_$1').toLowerCase(),
|
|
67
|
+
value
|
|
68
|
+
])
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reactive({ target, params }) {
|
|
73
|
+
const debounce = params?.debounce
|
|
74
|
+
|
|
75
|
+
if (debounce) {
|
|
76
|
+
this.#setDebounce(target, debounce, () => {
|
|
77
|
+
this.sendReactive(target)
|
|
78
|
+
}, this.#reactiveMessage(target))
|
|
79
|
+
} else {
|
|
80
|
+
this.sendReactive(target)
|
|
81
|
+
}
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
sendReactive(target) {
|
|
85
|
+
this.#clearDebounce(target)
|
|
62
86
|
this.#subscription.send(
|
|
63
|
-
this.#
|
|
87
|
+
this.#flushDebounced(this.#reactiveMessage(target))
|
|
64
88
|
)
|
|
65
89
|
}
|
|
66
90
|
|
|
@@ -72,49 +96,60 @@ export default class extends Controller {
|
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
|
|
75
|
-
#
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (this.#reactiveDebouncedMessage) {
|
|
79
|
-
messages.unshift(this.#reactiveDebouncedMessage)
|
|
80
|
-
this.#reactiveDebouncedMessage = null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { messages, _csrf_token: this.#csrfToken }
|
|
84
|
-
}
|
|
99
|
+
#form(action, { currentTarget, params }) {
|
|
100
|
+
const debounce = params.debounce
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
if (debounce) {
|
|
103
|
+
const formData = new FormData(currentTarget)
|
|
104
|
+
const formParams = new URLSearchParams(formData).toString()
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
form({ currentTarget, params: { action } }) {
|
|
97
|
-
this.sendForm(action, currentTarget)
|
|
106
|
+
this.#setDebounce(currentTarget, debounce, () => {
|
|
107
|
+
this.sendForm(action, currentTarget)
|
|
108
|
+
}, this.#callMessage(formParams, action))
|
|
109
|
+
} else {
|
|
110
|
+
this.sendForm(action, currentTarget)
|
|
111
|
+
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
sendForm(action, formEl) {
|
|
101
|
-
|
|
102
|
-
clearTimeout(this.#reactiveDebounce)
|
|
103
|
-
clearTimeout(this.#formDebounce)
|
|
115
|
+
this.#clearDebounce(formEl)
|
|
104
116
|
|
|
105
117
|
const formData = new FormData(formEl)
|
|
106
118
|
const params = new URLSearchParams(formData).toString()
|
|
107
119
|
|
|
108
120
|
this.#subscription.send(
|
|
109
|
-
this.#
|
|
121
|
+
this.#flushDebounced(this.#callMessage(params, action))
|
|
110
122
|
)
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.#
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
#setDebounce(source, delay, callback, message) {
|
|
126
|
+
// Clear existing debounce for this source
|
|
127
|
+
this.#clearDebounce(source)
|
|
128
|
+
|
|
129
|
+
// Set new debounce
|
|
130
|
+
const timeout = setTimeout(callback, delay)
|
|
131
|
+
this.#debounces.set(source, { timeout, message })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#clearDebounce(source) {
|
|
135
|
+
const debounce = this.#debounces.get(source)
|
|
136
|
+
if (debounce) {
|
|
137
|
+
clearTimeout(debounce.timeout)
|
|
138
|
+
this.#debounces.delete(source)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#flushDebounced(message) {
|
|
143
|
+
const messages = [message]
|
|
144
|
+
|
|
145
|
+
// Add all pending debounced messages to be sent immediately
|
|
146
|
+
for (const [source, { timeout, message: debouncedMessage }] of this.#debounces) {
|
|
147
|
+
clearTimeout(timeout)
|
|
148
|
+
messages.unshift(debouncedMessage)
|
|
149
|
+
}
|
|
150
|
+
this.#debounces.clear()
|
|
151
|
+
|
|
152
|
+
return { messages, _csrf_token: this.#csrfToken }
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
get #csrfToken() {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM transformation module for LiveCable
|
|
3
|
+
*
|
|
4
|
+
* Converts live-* HTML attributes into Stimulus data-* attributes at runtime.
|
|
5
|
+
* This allows for a cleaner HTML syntax while maintaining full Stimulus compatibility.
|
|
6
|
+
*/
|
|
7
|
+
class DOM {
|
|
8
|
+
// All live-* attributes that trigger DOM processing
|
|
9
|
+
static LIVE_ATTRIBUTES = [
|
|
10
|
+
'live-id',
|
|
11
|
+
'live-component',
|
|
12
|
+
'live-defaults',
|
|
13
|
+
'live-actions',
|
|
14
|
+
'live-form',
|
|
15
|
+
'live-action',
|
|
16
|
+
'live-reactive'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Transforms all live-* attributes on an element and its descendants
|
|
21
|
+
* @param {HTMLElement} element - The root element to process
|
|
22
|
+
*/
|
|
23
|
+
mutate(element) {
|
|
24
|
+
if (!element || !element.querySelectorAll) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build selector for all live-* attributes we care about
|
|
29
|
+
const selector = DOM.LIVE_ATTRIBUTES.map(attr => `[${attr}]`).join(', ')
|
|
30
|
+
|
|
31
|
+
// Get all elements with live-* attributes (including the root element if it matches)
|
|
32
|
+
const liveElements = []
|
|
33
|
+
if (element.matches && element.matches(selector)) {
|
|
34
|
+
liveElements.push(element)
|
|
35
|
+
}
|
|
36
|
+
liveElements.push(...element.querySelectorAll(selector))
|
|
37
|
+
|
|
38
|
+
liveElements.forEach(el => {
|
|
39
|
+
// Process component metadata attributes first
|
|
40
|
+
this.#processMetadataAttributes(el)
|
|
41
|
+
|
|
42
|
+
// Process interactive attributes (actions, forms, reactive)
|
|
43
|
+
this.#processInteractiveAttributes(el)
|
|
44
|
+
|
|
45
|
+
// Process live-id last so all other attributes are converted before
|
|
46
|
+
// the Stimulus controller is attached
|
|
47
|
+
if (el.hasAttribute('live-id')) {
|
|
48
|
+
this.#replaceLiveId(el)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#processMetadataAttributes(element) {
|
|
54
|
+
if (element.hasAttribute('live-component')) {
|
|
55
|
+
this.#replaceAttribute(element, 'live-component', 'data-live-component-value')
|
|
56
|
+
}
|
|
57
|
+
if (element.hasAttribute('live-defaults')) {
|
|
58
|
+
this.#replaceAttribute(element, 'live-defaults', 'data-live-defaults-value')
|
|
59
|
+
}
|
|
60
|
+
if (element.hasAttribute('live-actions')) {
|
|
61
|
+
this.#replaceAttribute(element, 'live-actions', 'data-live-actions-value')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#processInteractiveAttributes(element) {
|
|
66
|
+
// Note: convertValues and convertDebounce are called for each type as needed
|
|
67
|
+
if (element.hasAttribute('live-form')) {
|
|
68
|
+
this.#convertValues(element)
|
|
69
|
+
this.#convertDebounce(element)
|
|
70
|
+
this.#addActions(element, 'form', 'live-form', ':prevent')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (element.hasAttribute('live-action')) {
|
|
74
|
+
this.#convertValues(element)
|
|
75
|
+
this.#convertDebounce(element)
|
|
76
|
+
this.#addActions(element, 'action', 'live-action')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (element.hasAttribute('live-reactive')) {
|
|
80
|
+
this.#convertDebounce(element)
|
|
81
|
+
this.#convertReactive(element)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#replaceAttribute(element, oldAttr, newAttr) {
|
|
86
|
+
const value = element.getAttribute(oldAttr)
|
|
87
|
+
if (value !== null) {
|
|
88
|
+
element.removeAttribute(oldAttr)
|
|
89
|
+
element.setAttribute(newAttr, value)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#addActions(element, type, attribute, eventModifier = '') {
|
|
94
|
+
const attributeValue = element.getAttribute(attribute)
|
|
95
|
+
if (!attributeValue) return
|
|
96
|
+
|
|
97
|
+
const actions = attributeValue.trim().split(/\s+/).map(actionString => {
|
|
98
|
+
const parts = actionString.split(/->/)
|
|
99
|
+
const [event, action] = parts.length === 2 ? parts : [null, parts[0]]
|
|
100
|
+
const stimulusEvent = event ? `${event}->` : ''
|
|
101
|
+
|
|
102
|
+
return `${stimulusEvent}live#${type}_$${action}${eventModifier}`
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
element.removeAttribute(attribute)
|
|
106
|
+
this.#appendToAttribute(element, 'data-action', actions.join(' '))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#convertValues(element) {
|
|
110
|
+
// Get all attributes that start with 'live-value-'
|
|
111
|
+
Array.from(element.attributes).forEach(attr => {
|
|
112
|
+
if (attr.name.startsWith('live-value-')) {
|
|
113
|
+
const paramName = attr.name.substring('live-value-'.length)
|
|
114
|
+
element.removeAttribute(attr.name)
|
|
115
|
+
element.setAttribute(`data-live-${paramName}-param`, attr.value)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#convertDebounce(element) {
|
|
121
|
+
if (element.hasAttribute('live-debounce')) {
|
|
122
|
+
const value = element.getAttribute('live-debounce')
|
|
123
|
+
element.removeAttribute('live-debounce')
|
|
124
|
+
element.setAttribute('data-live-debounce-param', value)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#convertReactive(element) {
|
|
129
|
+
const value = element.getAttribute('live-reactive')
|
|
130
|
+
element.removeAttribute('live-reactive')
|
|
131
|
+
|
|
132
|
+
if (!value || value.trim() === '') {
|
|
133
|
+
// No events specified, use default Stimulus event
|
|
134
|
+
this.#appendToAttribute(element, 'data-action', 'live#reactive')
|
|
135
|
+
} else {
|
|
136
|
+
// Multiple events specified, convert each to event->live#reactive
|
|
137
|
+
const actions = value.trim().split(/\s+/).map(event => `${event}->live#reactive`)
|
|
138
|
+
this.#appendToAttribute(element, 'data-action', actions.join(' '))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#replaceLiveId(element) {
|
|
143
|
+
this.#replaceAttribute(element, 'live-id', 'data-live-id-value')
|
|
144
|
+
this.#appendToAttribute(element, 'data-controller', 'live')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#appendToAttribute(element, attributeName, value) {
|
|
148
|
+
const existing = element.getAttribute(attributeName)
|
|
149
|
+
|
|
150
|
+
if (existing) {
|
|
151
|
+
const values = existing.split(/\s+/)
|
|
152
|
+
if (!values.includes(value)) {
|
|
153
|
+
element.setAttribute(attributeName, `${existing} ${value}`)
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
element.setAttribute(attributeName, value)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default new DOM()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import LiveObserver from '@isometriks/live_cable/observer'
|
|
2
|
+
import SubscriptionManager from '@isometriks/live_cable/subscriptions'
|
|
3
|
+
import DOM from '@isometriks/live_cable/dom'
|
|
4
|
+
|
|
5
|
+
const observer = new LiveObserver()
|
|
6
|
+
observer.start()
|
|
7
|
+
|
|
8
|
+
document.addEventListener('turbo:before-render', (event) => {
|
|
9
|
+
SubscriptionManager.prune(event.detail.newBody)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
document.addEventListener('turbo:load', () => {
|
|
13
|
+
DOM.mutate(document.documentElement)
|
|
14
|
+
updateCacheControl()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure pages with live components are never stored in Turbo's page cache.
|
|
19
|
+
*
|
|
20
|
+
* Turbo's back/forward cache restores a snapshot of the page taken at
|
|
21
|
+
* navigate time. A restored snapshot would reconnect Stimulus controllers
|
|
22
|
+
* to subscriptions that were already closed server-side, causing a cold
|
|
23
|
+
* re-render that may fail. Preventing caching forces a fresh server fetch
|
|
24
|
+
* on back/forward, so components are always pre-rendered before their
|
|
25
|
+
* ActionCable subscription connects.
|
|
26
|
+
*
|
|
27
|
+
* If the developer has already set a turbo-cache-control meta tag (e.g.
|
|
28
|
+
* "no-store"), that value is left untouched so they can override this
|
|
29
|
+
* behaviour per-page.
|
|
30
|
+
*/
|
|
31
|
+
function updateCacheControl() {
|
|
32
|
+
// A meta tag without our marker was placed by the developer — leave it alone
|
|
33
|
+
const devMeta = document.querySelector('meta[name="turbo-cache-control"]:not([data-live-cable])')
|
|
34
|
+
if (devMeta) return
|
|
35
|
+
|
|
36
|
+
const ourMeta = document.querySelector('meta[name="turbo-cache-control"][data-live-cable]')
|
|
37
|
+
const hasLiveComponents = !!document.querySelector('[data-controller~="live"]')
|
|
38
|
+
|
|
39
|
+
if (hasLiveComponents && !ourMeta) {
|
|
40
|
+
const meta = document.createElement('meta')
|
|
41
|
+
meta.name = 'turbo-cache-control'
|
|
42
|
+
meta.content = 'no-cache'
|
|
43
|
+
meta.dataset.liveCable = ''
|
|
44
|
+
document.head.appendChild(meta)
|
|
45
|
+
} else if (!hasLiveComponents && ourMeta) {
|
|
46
|
+
ourMeta.remove()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default observer
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import DOM from "@isometriks/live_cable/dom"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LiveCable DOM Observer
|
|
5
|
+
*
|
|
6
|
+
* Observes the DOM for elements with the `live-id` attribute
|
|
7
|
+
*/
|
|
8
|
+
export default class LiveObserver {
|
|
9
|
+
#observer
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Start observing the DOM for live components
|
|
13
|
+
*/
|
|
14
|
+
start() {
|
|
15
|
+
if (this.#observer) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
20
|
+
mutations.forEach((mutation) => {
|
|
21
|
+
// Handle added nodes
|
|
22
|
+
mutation.addedNodes.forEach((node) => {
|
|
23
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
24
|
+
this.checkElement(node)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Handle attribute changes
|
|
29
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'live-id') {
|
|
30
|
+
const element = mutation.target
|
|
31
|
+
if (element.hasAttribute('live-id')) {
|
|
32
|
+
DOM.mutate(element)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
this.#observer.observe(document.documentElement, {
|
|
39
|
+
childList: true,
|
|
40
|
+
subtree: true,
|
|
41
|
+
attributes: true,
|
|
42
|
+
attributeFilter: ['live-id'],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Check the DOM as well when we start
|
|
46
|
+
this.checkElement(document.documentElement)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stop observing the DOM
|
|
51
|
+
*/
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.#observer) {
|
|
54
|
+
this.#observer.disconnect()
|
|
55
|
+
this.#observer = null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if an element or its descendants have live-id attribute
|
|
61
|
+
*/
|
|
62
|
+
checkElement(element) {
|
|
63
|
+
if (element.hasAttribute && element.hasAttribute('live-id')) {
|
|
64
|
+
DOM.mutate(element)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (element.querySelectorAll) {
|
|
68
|
+
const liveElements = element.querySelectorAll('[live-id]')
|
|
69
|
+
liveElements.forEach((liveElement) => {
|
|
70
|
+
DOM.mutate(liveElement)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|