turbo_boost-elements 0.0.7 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 () {