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