turbo_boost-elements 0.0.8 → 0.0.9

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.
@@ -22,7 +22,6 @@ class TurboBoost::Elements::ToggleCommand < TurboBoost::Elements::ApplicationCom
22
22
  end
23
23
 
24
24
  def toggle
25
- validate_element!
26
25
  element.aria.expanded? ? hide : show
27
26
  end
28
27
 
@@ -84,29 +84,37 @@ export default class TooltipElement extends HTMLElement {
84
84
  display: block;
85
85
  font-size: 0.8rem;
86
86
  font-weight: lighter;
87
- margin-bottom: 8px;
88
- margin-top: 4px;
87
+ margin-bottom: 12px;
88
+ margin-top: 8px;
89
89
  padding-bottom: 4px;
90
90
  padding-top: 4px;
91
91
  width: 100%;
92
92
  }
93
93
 
94
+ slot[name="content-top"],
95
+ slot[name="content"],
96
+ slot[name="content-bottom"] {
97
+ display: block;
98
+ font-weight: normal;
99
+ }
100
+
94
101
  slot[name="content-top"] {
95
102
  color: ${this.color};
96
- font-weight: normal;
103
+ margin-bottom: 8px;
104
+ }
105
+
106
+ slot[name="content"],
107
+ slot[name="content-bottom"] {
97
108
  opacity: 0.7;
109
+ padding-left: 12px;
98
110
  }
99
111
 
100
112
  slot[name="content"] {
101
113
  color: ${this.color};
102
- font-weight: normal;
103
- opacity: 0.7;
104
114
  }
105
115
 
106
116
  slot[name="content-bottom"] {
107
117
  color: red;
108
- font-weight: normal;
109
- opacity: 0.7;
110
118
  }
111
119
  `
112
120
  }
@@ -81,6 +81,7 @@ addEventListener('turbo:load', autoRestart)
81
81
  addEventListener('turbo-frame:load', autoRestart)
82
82
  addEventListener(TurboBoost.Commands.events.success, autoRestart)
83
83
  addEventListener(TurboBoost.Commands.events.finish, autoRestart)
84
+ addEventListener('turbo-boost:devtools-connect', autoRestart)
84
85
  addEventListener('turbo-boost:devtools-close', stop)
85
86
 
86
87
  function register (name, label) {
@@ -1,5 +1,9 @@
1
- import ToggleTargetElement from './toggle_target_element'
2
- import ToggleTriggerElement from './toggle_trigger_element'
1
+ import TurboBoostElement from './turbo_boost_element'
2
+ import ToggleTargetElement from './toggle_elements/target_element'
3
+ import ToggleTriggerElement from './toggle_elements/trigger_element'
3
4
 
5
+ // Valid custom element names: https://html.spec.whatwg.org/#valid-custom-element-name
6
+
7
+ customElements.define('turbo-boost', TurboBoostElement)
4
8
  customElements.define('turbo-boost-toggle-target', ToggleTargetElement)
5
9
  customElements.define('turbo-boost-toggle-trigger', ToggleTriggerElement)
@@ -0,0 +1,128 @@
1
+ import ToggleElement from '../toggle_element'
2
+ import './focus'
3
+
4
+ export default class ToggleTargetElement extends ToggleElement {
5
+ connectedCallback () {
6
+ super.connectedCallback()
7
+
8
+ this.mouseenterHandler = this.onMouseenter.bind(this)
9
+ this.addEventListener('mouseenter', this.mouseenterHandler)
10
+
11
+ this.collapseHandler = this.collapse.bind(this)
12
+ this.collapseNowHandler = this.collapseNow.bind(this)
13
+
14
+ this.collapseOn.forEach(entry => {
15
+ const parts = entry.split('@')
16
+ const name = parts[0]
17
+
18
+ if (parts.length > 1) {
19
+ const target = parts[1].match(/^self|window$/) ? self : self[parts[1]]
20
+ target.addEventListener(name, this.collapseNowHandler)
21
+ } else {
22
+ this.addEventListener(name, this.collapseHandler)
23
+ }
24
+ })
25
+ }
26
+
27
+ disconnectedCallback () {
28
+ this.removeEventListener('mouseenter', this.mouseenterHandler)
29
+
30
+ this.collapseOn.forEach(entry => {
31
+ const parts = entry.split('@')
32
+ const name = parts[0]
33
+
34
+ if (parts.length > 1) {
35
+ const target = parts[1].match(/^self|window$/) ? self : self[parts[1]]
36
+ target.removeEventListener(name, this.collapseNowHandler)
37
+ } else {
38
+ this.removeEventListener(name, this.collapseHandler)
39
+ }
40
+ })
41
+ }
42
+
43
+ // TODO: get cached content working properly
44
+ // perhaps use a mechanic other than morph
45
+
46
+ // TODO: implement cache (similar to Turbo Drive restoration visit)
47
+ cacheHTML () {
48
+ // this.cachedHTML = this.innerHTML
49
+ }
50
+
51
+ // TODO: implement cache (similar to Turbo Drive restoration visit)
52
+ renderCachedHTML () {
53
+ // if (!this.cachedHTML) return
54
+ // this.innerHTML = this.cachedHTML
55
+ }
56
+
57
+ onMouseenter () {
58
+ clearTimeout(this.collapseTimeout)
59
+ }
60
+
61
+ collapse (delay = 250) {
62
+ clearTimeout(this.collapseTimeout)
63
+ if (typeof delay !== 'number') delay = 250
64
+
65
+ if (delay > 0)
66
+ return (this.collapseTimeout = setTimeout(() => this.collapse(0), delay))
67
+
68
+ this.innerHTML = ''
69
+ try {
70
+ this.expanded = false
71
+ this.currentTriggerElement.hideDevtool()
72
+ } catch {}
73
+ }
74
+
75
+ collapseNow (event) {
76
+ if (event.target.closest('turbo-boost-devtool-tooltip')) return
77
+ this.collapse(0)
78
+ }
79
+
80
+ collapseMatches () {
81
+ document.querySelectorAll(this.collapseSelector).forEach(el => {
82
+ if (el === this) return
83
+ if (el.collapse) el.collapse(0)
84
+ })
85
+ }
86
+
87
+ get collapseSelector () {
88
+ return (
89
+ this.currentTriggerElement.collapseSelector ||
90
+ this.getAttribute('collapse-selector')
91
+ )
92
+ }
93
+
94
+ focus () {
95
+ clearTimeout(this.focusTimeout)
96
+ this.focusTimeout = setTimeout(() => {
97
+ if (this.focusElement) this.focusElement.focus()
98
+ }, 50)
99
+ }
100
+
101
+ get focusSelector () {
102
+ if (this.currentTriggerElement && this.currentTriggerElement.focusSelector)
103
+ return this.currentTriggerElement.focusSelector
104
+ return this.getAttribute('focus-selector')
105
+ }
106
+
107
+ get focusElement () {
108
+ return this.querySelector(this.focusSelector)
109
+ }
110
+
111
+ get labeledBy () {
112
+ return this.getAttribute('aria-labeledby')
113
+ }
114
+
115
+ get collapseOn () {
116
+ const value = this.getAttribute('collapse-on')
117
+ if (!value) return []
118
+ return JSON.parse(value)
119
+ }
120
+
121
+ get expanded () {
122
+ return this.currentTriggerElement.expanded
123
+ }
124
+
125
+ set expanded (value) {
126
+ return (this.currentTriggerElement.expanded = value)
127
+ }
128
+ }
@@ -0,0 +1,83 @@
1
+ import TurboBoostElement from '../../turbo_boost_element'
2
+
3
+ const html = `
4
+ <turbo-boost>
5
+ <slot name="busy" hidden></slot>
6
+ <slot></slot>
7
+ </turbo-boost>
8
+ `
9
+
10
+ export const busyDelay = 100 // milliseconds - time to wait before showing busy element
11
+ export const busyDuration = 400 // milliseconds - minimum time that busy element is shown
12
+
13
+ export default class ToggleElement extends TurboBoostElement {
14
+ constructor () {
15
+ super(html)
16
+ }
17
+
18
+ // TODO: Should we timeout after a theoretical max wait time?
19
+ // The idea being that a server error occurred and the toggle failed.
20
+ showBusyElement () {
21
+ clearTimeout(this.showBusyElementTimeout)
22
+ clearTimeout(this.hideBusyElementTimeout)
23
+
24
+ if (!this.busyElement) return
25
+
26
+ this.busyStartedAt = Date.now() + busyDelay
27
+ this.showBusyElementTimeout = setTimeout(() => {
28
+ this.busySlotElement.hidden = false
29
+ this.defaultSlotElement.hidden = true
30
+ }, busyDelay)
31
+ }
32
+
33
+ hideBusyElement () {
34
+ clearTimeout(this.showBusyElementTimeout)
35
+ clearTimeout(this.hideBusyElementTimeout)
36
+
37
+ if (!this.busyElement) return
38
+
39
+ let delay = busyDuration - (Date.now() - this.busyStartedAt)
40
+ if (delay < 0) delay = 0
41
+
42
+ delete this.busyStartedAt
43
+ this.hideBusyElementTimeout = setTimeout(() => {
44
+ this.busySlotElement.hidden = true
45
+ this.defaultSlotElement.hidden = false
46
+ }, delay)
47
+ }
48
+
49
+ get busyElement () {
50
+ return this.querySelector(':scope > [slot="busy"]')
51
+ }
52
+
53
+ get busySlotElement () {
54
+ return this.shadowRoot.querySelector('slot[name="busy"]')
55
+ }
56
+
57
+ get defaultSlotElement () {
58
+ return this.shadowRoot.querySelector('slot:not([name])')
59
+ }
60
+
61
+ // indicates if an rpc call is active/busy
62
+ get busy () {
63
+ return this.getAttribute('busy') === 'true'
64
+ }
65
+
66
+ // indicates if an rpc call is active/busy
67
+ set busy (value) {
68
+ value = !!value
69
+ if (this.busy === value) return
70
+ this.setAttribute('busy', value)
71
+ if (value) this.showBusyElement()
72
+ else this.hideBusyElement()
73
+ }
74
+
75
+ get busyStartedAt () {
76
+ if (!this.dataset.busyStartedAt) return 0
77
+ return Number(this.dataset.busyStartedAt)
78
+ }
79
+
80
+ set busyStartedAt (value) {
81
+ this.dataset.busyStartedAt = value
82
+ }
83
+ }
@@ -1,15 +1,16 @@
1
+ // Icons courtesy of https://feathericons.com/
1
2
  import {
2
3
  appendHTML,
3
4
  addHighlight,
4
5
  coordinates,
5
6
  removeHighlight
6
- } from '../utils/dom'
7
- import supervisor from './supervisor'
7
+ } from '../../../utils/dom'
8
+ import supervisor from '../../../devtools/supervisor'
8
9
 
9
10
  let activeToggle
10
11
 
11
12
  document.addEventListener('turbo-boost:devtools-start', () =>
12
- supervisor.register('toggle', 'toggles<small>(trigger/target)</small>')
13
+ supervisor.register('toggle', 'toggles')
13
14
  )
14
15
 
15
16
  function appendTooltip (title, subtitle, content, options = {}) {
@@ -25,7 +26,7 @@ function appendTooltip (title, subtitle, content, options = {}) {
25
26
  `)
26
27
  }
27
28
 
28
- export default class ToggleDevtool {
29
+ export default class Devtool {
29
30
  constructor (triggerElement) {
30
31
  this.name = 'toggle'
31
32
  this.command = triggerElement.dataset.turboCommand
@@ -51,7 +52,7 @@ export default class ToggleDevtool {
51
52
  let hideTimeout
52
53
  const debouncedHide = () => {
53
54
  clearTimeout(hideTimeout)
54
- hideTimeout = setTimeout(this.hide(true), 25)
55
+ hideTimeout = setTimeout(this.hide({ active: false }), 25)
55
56
  }
56
57
 
57
58
  addEventListener('click', event => {
@@ -59,6 +60,13 @@ export default class ToggleDevtool {
59
60
  debouncedHide()
60
61
  })
61
62
 
63
+ addEventListener('resize', () => {
64
+ if (this.active) {
65
+ this.hide({ active: false })
66
+ this.show()
67
+ }
68
+ })
69
+
62
70
  addEventListener('turbo:load', debouncedHide)
63
71
  addEventListener('turbo-frame:load', debouncedHide)
64
72
  addEventListener(TurboBoost.Commands.events.success, debouncedHide)
@@ -69,11 +77,20 @@ export default class ToggleDevtool {
69
77
  return supervisor.enabled(this.name)
70
78
  }
71
79
 
80
+ get active () {
81
+ return activeToggle === this
82
+ }
83
+
84
+ set active (value) {
85
+ if (value) activeToggle = this
86
+ else activeToggle = null
87
+ }
88
+
72
89
  show () {
73
90
  if (!this.enabled) return
74
- if (activeToggle === this) return
75
- activeToggle = this
76
- this.hide()
91
+ if (this.active) return
92
+ this.active = true
93
+ this.hide({ active: true })
77
94
 
78
95
  addHighlight(this.targetElement, {
79
96
  outline: '3px dashed darkcyan',
@@ -115,14 +132,14 @@ export default class ToggleDevtool {
115
132
  if (this.targetElement)
116
133
  data.target = {
117
134
  partial: this.targetElement.partial,
118
- id: this.targetElement.id,
135
+ dom_id: this.targetElement.id,
119
136
  status: 'OK'
120
137
  }
121
138
 
122
139
  console.table(data)
123
140
  }
124
141
 
125
- hide (clearActiveToggle) {
142
+ hide ({ active: active = false }) {
126
143
  document.querySelectorAll('.leader-line').forEach(el => el.remove())
127
144
  document
128
145
  .querySelectorAll('turbo-boost-devtool-tooltip')
@@ -132,7 +149,7 @@ export default class ToggleDevtool {
132
149
  if (!el.tagName.match(/turbo-boost-toggle-trigger/i)) removeHighlight(el)
133
150
  })
134
151
 
135
- if (clearActiveToggle) activeToggle = null
152
+ this.active = active
136
153
  }
137
154
 
138
155
  createMorphTooltip () {
@@ -141,12 +158,19 @@ export default class ToggleDevtool {
141
158
  `Unable to create the morph tooltip! No element matches the DOM id: '${this.triggerElement.morphs}'`
142
159
  )
143
160
 
144
- const title = 'PARTIAL'
161
+ const title = `
162
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:inline-block;" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg>
163
+ RENDERING
164
+ `
145
165
  const subtitle = `
146
- id: ${this.triggerElement.morphs || 'unknown'}<br>
147
- partial: ${this.triggerElement.renders || 'unknown'}
166
+ <b>partial</b>: ${this.triggerElement.renders || 'unknown'}<br>
167
+ <b>morphs</b>: ${this.triggerElement.morphs || 'unknown'}<br>
168
+ `
169
+ const content = `
170
+ <div slot="content-top" style="font-size:85%; font-style:italic; font-weight:100;">
171
+ The <b>TRIGGER</b> toggles the <b>TARGET</b> then renders the partial &amp; morphs the element.<br>
172
+ </div>
148
173
  `
149
- const content = '<div slot="content"></div>'
150
174
  const tooltip = appendTooltip(title, subtitle, content, {
151
175
  backgroundColor: 'lightyellow',
152
176
  color: 'chocolate'
@@ -175,20 +199,31 @@ export default class ToggleDevtool {
175
199
  `Unable to create the target tooltip! No element matches the DOM id: '${this.triggerElement.controls}'`
176
200
  )
177
201
 
178
- const title = 'TARGET'
202
+ const title = `
203
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:inline-block;" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="6"></circle><circle cx="12" cy="12" r="2"></circle></svg>
204
+ TARGET
205
+ `
179
206
  const subtitle = `
180
- id: ${this.targetElement.id}<br>
181
- labeled by: ${this.targetElement.labeledBy}
207
+ <b>id</b>: ${this.targetElement.id}<br>
208
+ <b>aria-labeled-by</b>: ${this.targetElement.labeledBy}<br>
182
209
  `
183
- const content = this.targetElement.viewStack
210
+ let content = this.targetElement.viewStack
184
211
  .reverse()
185
212
  .map((view, index) => {
186
213
  return this.triggerElement.sharedViews.includes(view)
187
- ? `<div slot="content-top">${index + 1}. ${view}</div>`
214
+ ? `<div slot="content">${index + 1}. ${view}</div>`
188
215
  : `<div slot="content-bottom">${index + 1}. ${view}</div>`
189
216
  }, this)
190
217
  .join('')
191
218
 
219
+ content = `
220
+ <div slot="content-top">
221
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:inline-block;" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
222
+ <b>View Stack</b>
223
+ </div>
224
+ ${content}
225
+ `
226
+
192
227
  const tooltip = appendTooltip(title, subtitle, content, {
193
228
  backgroundColor: 'lightcyan',
194
229
  color: 'darkcyan',
@@ -212,20 +247,33 @@ export default class ToggleDevtool {
212
247
 
213
248
  createTriggerTooltip (targetTooltip, morphTooltip) {
214
249
  if (!this.triggerElement) return
215
- const title = 'TRIGGER'
250
+ const title = `
251
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:inline;" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
252
+ TRIGGER
253
+ `
216
254
  const subtitle = `
217
- id: ${this.triggerElement.id}<br>
218
- controls: ${this.triggerElement.controls}
255
+ <b>id</b>: ${this.triggerElement.id}<br>
256
+ <b>aria-controls</b>: ${this.triggerElement.controls}<br>
257
+ <b>aria-expanded</b>: ${this.triggerElement.expanded}<br>
258
+ <b>remember</b>: ${this.triggerElement.remember}<br>
219
259
  `
220
- const content = this.triggerElement.viewStack
260
+ let content = this.triggerElement.viewStack
221
261
  .reverse()
222
262
  .map((view, index) => {
223
263
  return this.triggerElement.sharedViews.includes(view)
224
- ? `<div slot="content-top">${index + 1}. ${view}</div>`
264
+ ? `<div slot="content">${index + 1}. ${view}</div>`
225
265
  : `<div slot="content-bottom">${index + 1}. ${view}</div>`
226
266
  }, this)
227
267
  .join('')
228
268
 
269
+ content = `
270
+ <div slot="content-top">
271
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:inline-block;" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
272
+ <b>View Stack</b>
273
+ </div>
274
+ ${content}
275
+ `
276
+
229
277
  const tooltip = appendTooltip(title, subtitle, content, {
230
278
  backgroundColor: 'lavender',
231
279
  color: 'blueviolet'
@@ -261,7 +309,7 @@ export default class ToggleDevtool {
261
309
  tooltip.lineToRendering = new LeaderLine(tooltip, morphTooltip, {
262
310
  ...this.leaderLineOptions,
263
311
  color: 'blueviolet',
264
- middleLabel: 'renders and morphs',
312
+ middleLabel: 'renders & morphs',
265
313
  size: 2.1
266
314
  })
267
315
 
@@ -0,0 +1,172 @@
1
+ import ToggleElement, { busyDuration } from '../toggle_element'
2
+ import Devtool from './devtool'
3
+
4
+ export default class ToggleTriggerElement extends ToggleElement {
5
+ connectedCallback () {
6
+ super.connectedCallback()
7
+
8
+ if (this.targetElement)
9
+ this.targetElement.setAttribute('aria-labeledby', this.id)
10
+
11
+ const { start: commandStartEvent } = TurboBoost.Commands.events
12
+ this.commandStartHandler = this.onCommandStart.bind(this)
13
+ this.addEventListener(commandStartEvent, this.commandStartHandler)
14
+
15
+ const { before: beforeInvokeEvent } = TurboBoost.Streams.invokeEvents
16
+ this.beforeInvokeHandler = this.onBeforeInvoke.bind(this)
17
+ addEventListener(beforeInvokeEvent, this.beforeInvokeHandler)
18
+
19
+ // fires after receiving the toggle morph Turbo Stream but before it is executed
20
+ // this.addEventListener(TurboBoost.Commands.events.success, event => {
21
+ // // TODO: imlement cache, this.targetElement.cacheHTML()
22
+ // })
23
+
24
+ this.initializeDevtool()
25
+ }
26
+
27
+ disconnectedCallback () {
28
+ const { start: commandStartEvent } = TurboBoost.Commands.events
29
+ this.removeEventListener(commandStartEvent, this.commandStartHandler)
30
+
31
+ const { before: beforeInvokeEvent } = TurboBoost.Streams.invokeEvents
32
+ removeEventListener(beforeInvokeEvent, this.beforeInvokeHandler)
33
+
34
+ this.devtool.hide({ active: false })
35
+ delete this.devtool
36
+ }
37
+
38
+ initializeDevtool () {
39
+ const mouseenter = () => this.devtool.show()
40
+
41
+ addEventListener('turbo-boost:devtools-start', () => {
42
+ this.devtool = new Devtool(this)
43
+ this.addEventListener('mouseenter', mouseenter)
44
+ })
45
+
46
+ addEventListener('turbo-boost:devtools-stop', () => {
47
+ this.removeEventListener('mouseenter', mouseenter)
48
+ delete this.devtool
49
+ })
50
+
51
+ this.dispatchEvent(
52
+ new CustomEvent('turbo-boost:devtools-connect', { bubbles: true })
53
+ )
54
+ }
55
+
56
+ hideDevtool () {
57
+ if (this.devtool) this.devtool.hide({ active: false })
58
+ }
59
+
60
+ onCommandStart (event) {
61
+ this.targetElement.currentTriggerElement = this
62
+ this.targetElement.setAttribute('aria-labeledby', this.id)
63
+ this.targetElement.collapseMatches()
64
+ this.targetElement.busy = true
65
+ this.busy = true
66
+ // TODO: implement cache - this.targetElement.renderCachedHTML()
67
+ }
68
+
69
+ onBeforeInvoke (event) {
70
+ if (event.detail.method !== 'morph') return
71
+ if (event.target.id !== this.morphs) return
72
+
73
+ // ensure the busy element is shown long enough for a good user experience
74
+ // we accomplish this by modifying the event.detail with invoke instructions i.e. { delay }
75
+ // SEE: the TurboBoost Streams library for details on how this works
76
+ const duration = Date.now() - this.busyStartedAt
77
+ let delay = busyDuration - duration
78
+ if (delay < 10) delay = 10
79
+ event.detail.invoke = { delay }
80
+
81
+ // runs before the morph is executed
82
+ setTimeout(() => {
83
+ this.busy = false
84
+ this.targetElement.busy = false
85
+ this.morphToggleElements.forEach(el => (el.busy = false))
86
+ this.expanded = !this.expanded
87
+ }, delay - 10)
88
+
89
+ // runs after the morph is executed
90
+ setTimeout(() => {
91
+ if (this.expanded) this.targetElement.focus()
92
+ }, delay + 10)
93
+ }
94
+
95
+ // a list of views shared between the trigger and target
96
+ get sharedViews () {
97
+ if (!this.targetElement) return []
98
+ if (!this.targetElement.viewStack) return []
99
+ const reducer = (memo, view) => {
100
+ if (this.targetElement.viewStack.includes(view)) memo.push(view)
101
+ return memo
102
+ }
103
+ return this.viewStack.reduce(reducer.bind(this), [])
104
+ }
105
+
106
+ // the partial to render
107
+ get renders () {
108
+ return this.getAttribute('renders')
109
+ }
110
+
111
+ // the renderered partial's top wrapping dom_id
112
+ get morphs () {
113
+ return this.getAttribute('morphs')
114
+ }
115
+
116
+ // the morph element
117
+ get morphElement () {
118
+ if (!this.morphs) return null
119
+ return document.getElementById(this.morphs)
120
+ }
121
+
122
+ // all toggle elements contained by the `morphElement`
123
+ get morphToggleElements () {
124
+ return Array.from(
125
+ this.morphElement.querySelectorAll(
126
+ 'turbo-boost-toggle-trigger,turbo-boost-toggle-target'
127
+ )
128
+ )
129
+ }
130
+
131
+ // the target's dom_id
132
+ get controls () {
133
+ return this.getAttribute('aria-controls')
134
+ }
135
+
136
+ // the target element
137
+ get targetElement () {
138
+ if (!this.controls) return null
139
+ return document.getElementById(this.controls)
140
+ }
141
+
142
+ get collapseSelector () {
143
+ return this.getAttribute('collapse-selector')
144
+ }
145
+
146
+ get focusSelector () {
147
+ return this.getAttribute('focus-selector')
148
+ }
149
+
150
+ // indicates if the toggle state should be remembered across requests
151
+ get remember () {
152
+ return this.getAttribute('remember') === 'true'
153
+ }
154
+
155
+ set remember (value) {
156
+ return this.setAttribute('remember', !!value)
157
+ }
158
+
159
+ // indicates if the target is expanded
160
+ get expanded () {
161
+ return this.getAttribute('aria-expanded') === 'true'
162
+ }
163
+
164
+ set expanded (value) {
165
+ this.setAttribute('aria-expanded', !!value)
166
+ }
167
+
168
+ // indicates if the target is expanded
169
+ get collapsed () {
170
+ return !this.expanded
171
+ }
172
+ }
@@ -1,9 +1,9 @@
1
1
  export default class TurboBoostElement extends HTMLElement {
2
- constructor () {
2
+ constructor (html) {
3
3
  super()
4
4
  this.devtool = 'unknown'
5
5
  this.attachShadow({ mode: 'open' })
6
- this.shadowRoot.innerHTML = '<slot></slot>'
6
+ this.shadowRoot.innerHTML = html || '<slot></slot>'
7
7
  }
8
8
 
9
9
  connectedCallback () {