Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional "busy" slot to show activity #30

Merged
merged 17 commits into from
Jan 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 65 additions & 26 deletions app/assets/builds/@turbo-boost/elements.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions app/assets/builds/@turbo-boost/elements.js.map

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion app/commands/turbo_boost/elements/toggle_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ def hide
end

def toggle
validate_element!
element.aria.expanded? ? hide : show
end

Expand Down
22 changes: 15 additions & 7 deletions app/javascript/devtools/elements/tooltip_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,37 @@ export default class TooltipElement extends HTMLElement {
display: block;
font-size: 0.8rem;
font-weight: lighter;
margin-bottom: 8px;
margin-top: 4px;
margin-bottom: 12px;
margin-top: 8px;
padding-bottom: 4px;
padding-top: 4px;
width: 100%;
}

slot[name="content-top"],
slot[name="content"],
slot[name="content-bottom"] {
display: block;
font-weight: normal;
}

slot[name="content-top"] {
color: ${this.color};
font-weight: normal;
margin-bottom: 8px;
}

slot[name="content"],
slot[name="content-bottom"] {
opacity: 0.7;
padding-left: 12px;
}

slot[name="content"] {
color: ${this.color};
font-weight: normal;
opacity: 0.7;
}

slot[name="content-bottom"] {
color: red;
font-weight: normal;
opacity: 0.7;
}
`
}
Expand Down
1 change: 1 addition & 0 deletions app/javascript/devtools/supervisor.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ addEventListener('turbo:load', autoRestart)
addEventListener('turbo-frame:load', autoRestart)
addEventListener(TurboBoost.Commands.events.success, autoRestart)
addEventListener(TurboBoost.Commands.events.finish, autoRestart)
addEventListener('turbo-boost:devtools-connect', autoRestart)
addEventListener('turbo-boost:devtools-close', stop)

function register (name, label) {
Expand Down
8 changes: 6 additions & 2 deletions app/javascript/elements/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import ToggleTargetElement from './toggle_target_element'
import ToggleTriggerElement from './toggle_trigger_element'
import TurboBoostElement from './turbo_boost_element'
import ToggleTargetElement from './toggle_elements/target_element'
import ToggleTriggerElement from './toggle_elements/trigger_element'

// Valid custom element names: https://html.spec.whatwg.org/#valid-custom-element-name

customElements.define('turbo-boost', TurboBoostElement)
customElements.define('turbo-boost-toggle-target', ToggleTargetElement)
customElements.define('turbo-boost-toggle-trigger', ToggleTriggerElement)
128 changes: 128 additions & 0 deletions app/javascript/elements/toggle_elements/target_element/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import ToggleElement from '../toggle_element'
import './focus'

export default class ToggleTargetElement extends ToggleElement {
connectedCallback () {
super.connectedCallback()

this.mouseenterHandler = this.onMouseenter.bind(this)
this.addEventListener('mouseenter', this.mouseenterHandler)

this.collapseHandler = this.collapse.bind(this)
this.collapseNowHandler = this.collapseNow.bind(this)

this.collapseOn.forEach(entry => {
const parts = entry.split('@')
const name = parts[0]

if (parts.length > 1) {
const target = parts[1].match(/^self|window$/) ? self : self[parts[1]]
target.addEventListener(name, this.collapseNowHandler)
} else {
this.addEventListener(name, this.collapseHandler)
}
})
}

disconnectedCallback () {
this.removeEventListener('mouseenter', this.mouseenterHandler)

this.collapseOn.forEach(entry => {
const parts = entry.split('@')
const name = parts[0]

if (parts.length > 1) {
const target = parts[1].match(/^self|window$/) ? self : self[parts[1]]
target.removeEventListener(name, this.collapseNowHandler)
} else {
this.removeEventListener(name, this.collapseHandler)
}
})
}

// TODO: get cached content working properly
// perhaps use a mechanic other than morph

// TODO: implement cache (similar to Turbo Drive restoration visit)
cacheHTML () {
// this.cachedHTML = this.innerHTML
}

// TODO: implement cache (similar to Turbo Drive restoration visit)
renderCachedHTML () {
// if (!this.cachedHTML) return
// this.innerHTML = this.cachedHTML
}

onMouseenter () {
clearTimeout(this.collapseTimeout)
}

collapse (delay = 250) {
clearTimeout(this.collapseTimeout)
if (typeof delay !== 'number') delay = 250

if (delay > 0)
return (this.collapseTimeout = setTimeout(() => this.collapse(0), delay))

this.innerHTML = ''
try {
this.expanded = false
this.currentTriggerElement.hideDevtool()
} catch {}
}

collapseNow (event) {
if (event.target.closest('turbo-boost-devtool-tooltip')) return
this.collapse(0)
}

collapseMatches () {
document.querySelectorAll(this.collapseSelector).forEach(el => {
if (el === this) return
if (el.collapse) el.collapse(0)
})
}

get collapseSelector () {
return (
this.currentTriggerElement.collapseSelector ||
this.getAttribute('collapse-selector')
)
}

focus () {
clearTimeout(this.focusTimeout)
this.focusTimeout = setTimeout(() => {
if (this.focusElement) this.focusElement.focus()
}, 50)
}

get focusSelector () {
if (this.currentTriggerElement && this.currentTriggerElement.focusSelector)
return this.currentTriggerElement.focusSelector
return this.getAttribute('focus-selector')
}

get focusElement () {
return this.querySelector(this.focusSelector)
}

get labeledBy () {
return this.getAttribute('aria-labeledby')
}

get collapseOn () {
const value = this.getAttribute('collapse-on')
if (!value) return []
return JSON.parse(value)
}

get expanded () {
return this.currentTriggerElement.expanded
}

set expanded (value) {
return (this.currentTriggerElement.expanded = value)
}
}
83 changes: 83 additions & 0 deletions app/javascript/elements/toggle_elements/toggle_element/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import TurboBoostElement from '../../turbo_boost_element'

const html = `
<turbo-boost>
<slot name="busy" hidden></slot>
<slot></slot>
</turbo-boost>
`

export const busyDelay = 100 // milliseconds - time to wait before showing busy element
export const busyDuration = 400 // milliseconds - minimum time that busy element is shown

export default class ToggleElement extends TurboBoostElement {
constructor () {
super(html)
}

// TODO: Should we timeout after a theoretical max wait time?
// The idea being that a server error occurred and the toggle failed.
showBusyElement () {
clearTimeout(this.showBusyElementTimeout)
clearTimeout(this.hideBusyElementTimeout)

if (!this.busyElement) return

this.busyStartedAt = Date.now() + busyDelay
this.showBusyElementTimeout = setTimeout(() => {
this.busySlotElement.hidden = false
this.defaultSlotElement.hidden = true
}, busyDelay)
}

hideBusyElement () {
clearTimeout(this.showBusyElementTimeout)
clearTimeout(this.hideBusyElementTimeout)

if (!this.busyElement) return

let delay = busyDuration - (Date.now() - this.busyStartedAt)
if (delay < 0) delay = 0

delete this.busyStartedAt
this.hideBusyElementTimeout = setTimeout(() => {
this.busySlotElement.hidden = true
this.defaultSlotElement.hidden = false
}, delay)
}

get busyElement () {
return this.querySelector(':scope > [slot="busy"]')
}

get busySlotElement () {
return this.shadowRoot.querySelector('slot[name="busy"]')
}

get defaultSlotElement () {
return this.shadowRoot.querySelector('slot:not([name])')
}

// indicates if an rpc call is active/busy
get busy () {
return this.getAttribute('busy') === 'true'
}

// indicates if an rpc call is active/busy
set busy (value) {
value = !!value
if (this.busy === value) return
this.setAttribute('busy', value)
if (value) this.showBusyElement()
else this.hideBusyElement()
}

get busyStartedAt () {
if (!this.dataset.busyStartedAt) return 0
return Number(this.dataset.busyStartedAt)
}

set busyStartedAt (value) {
this.dataset.busyStartedAt = value
}
}
Loading