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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/builds/@turbo-boost/elements.js +65 -26
- data/app/assets/builds/@turbo-boost/elements.js.map +4 -4
- data/app/commands/turbo_boost/elements/toggle_command.rb +0 -1
- data/app/javascript/devtools/elements/tooltip_element.js +15 -7
- data/app/javascript/devtools/supervisor.js +1 -0
- data/app/javascript/elements/index.js +6 -2
- data/app/javascript/elements/{toggle_target_focus.js → toggle_elements/target_element/focus.js} +0 -0
- data/app/javascript/elements/toggle_elements/target_element/index.js +128 -0
- data/app/javascript/elements/toggle_elements/toggle_element/index.js +83 -0
- data/app/javascript/{devtools/toggle.js → elements/toggle_elements/trigger_element/devtool.js} +74 -26
- data/app/javascript/elements/toggle_elements/trigger_element/index.js +172 -0
- data/app/javascript/elements/{turbo_boost_element.js → turbo_boost_element/index.js} +2 -2
- data/app/javascript/utils/dom.js +7 -6
- data/lib/turbo_boost/elements/engine.rb +1 -0
- data/lib/turbo_boost/elements/tag_builders/base_tag_builder.rb +18 -2
- data/lib/turbo_boost/elements/tag_builders/toggle_tags_builder.rb +16 -12
- data/lib/turbo_boost/elements/version.rb +1 -1
- metadata +10 -9
- data/app/javascript/elements/toggle_target_element.js +0 -84
- data/app/javascript/elements/toggle_trigger_element.js +0 -122
@@ -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:
|
88
|
-
margin-top:
|
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
|
-
|
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
|
2
|
-
import
|
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)
|
data/app/javascript/elements/{toggle_target_focus.js → toggle_elements/target_element/focus.js}
RENAMED
File without changes
|
@@ -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
|
+
}
|
data/app/javascript/{devtools/toggle.js → elements/toggle_elements/trigger_element/devtool.js}
RENAMED
@@ -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 '
|
7
|
-
import supervisor from '
|
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
|
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
|
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(
|
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 (
|
75
|
-
|
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
|
-
|
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 (
|
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
|
-
|
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 =
|
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
|
-
|
147
|
-
|
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 & 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 =
|
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
|
181
|
-
labeled
|
207
|
+
<b>id</b>: ${this.targetElement.id}<br>
|
208
|
+
<b>aria-labeled-by</b>: ${this.targetElement.labeledBy}<br>
|
182
209
|
`
|
183
|
-
|
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
|
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 =
|
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
|
218
|
-
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
|
-
|
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
|
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
|
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 () {
|