Skip to content
Open
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
124 changes: 124 additions & 0 deletions assets/controllers/responsive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Controller } from '@hotwired/stimulus'
import '../styles/responsive.css'

/* stimulusFetch: 'eager' */
export default class extends Controller {
static targets = ['toggleButton', 'collapsibleRow']

static values = {
breakpoints: { type: Object, default: {} },
currentBreakpoint: { type: String, default: '' },
}

#resizeObserver = null
#debounceTimeout = null

connect() {
const frame = this.element.closest('turbo-frame')

if (!frame) {
this.#reveal()
return
}

// Synchronous check before first paint: does the server breakpoint match reality?
const width = Math.round(frame.getBoundingClientRect().width)

if (width > 0) {
const breakpoint = this.#resolveBreakpoint(width)

if (breakpoint !== this.currentBreakpointValue) {
// Mismatch: keep hidden (CSS class), reload with correct breakpoint
this.currentBreakpointValue = breakpoint
this.#reloadFrame(breakpoint)
} else {
// Match: reveal immediately
this.#reveal()
}
} else {
this.#reveal()
}

this.#resizeObserver = new ResizeObserver((entries) => {
this.#onResize(Math.round(entries[0].contentRect.width))
})
this.#resizeObserver.observe(frame)
}

disconnect() {
if (this.#resizeObserver) {
this.#resizeObserver.disconnect()
this.#resizeObserver = null
}

if (this.#debounceTimeout) {
clearTimeout(this.#debounceTimeout)
this.#debounceTimeout = null
}
}

toggle(event) {
const button = event.currentTarget
const index = button.dataset.rowIndex
const row = this.collapsibleRowTargets.find(r => r.dataset.rowIndex === index)

if (!row) {
return
}

row.hidden = !row.hidden
button.setAttribute('aria-expanded', String(!row.hidden))
button.querySelector('.kreyu-dt-toggle-icon').textContent = row.hidden ? '+' : '\u2212'
}

#reveal() {
this.element.classList.remove('kreyu-dt-responsive-pending')
}

#onResize(width) {
if (this.#debounceTimeout) {
clearTimeout(this.#debounceTimeout)
}

this.#debounceTimeout = setTimeout(() => this.#detectAndUpdate(width), 250)
}

#detectAndUpdate(width) {
const breakpoint = this.#resolveBreakpoint(width)

if (breakpoint === this.currentBreakpointValue) {
return
}

this.currentBreakpointValue = breakpoint
this.#reloadFrame(breakpoint)
}

#resolveBreakpoint(width) {
const breakpoints = this.breakpointsValue

for (const [name, maxWidth] of Object.entries(breakpoints)) {
if (width <= maxWidth) {
return name
}
}

// Above all breakpoints: return the largest one
const names = Object.keys(breakpoints)
return names.length > 0 ? names[names.length - 1] : ''
}

#reloadFrame(breakpoint) {
const frame = this.element.closest('turbo-frame')

if (!frame) {
return
}

const baseUrl = frame.getAttribute('src') || window.location.href
const url = new URL(baseUrl, window.location.origin)
url.searchParams.set('_breakpoint', breakpoint)

frame.src = url.toString()
}
}
5 changes: 5 additions & 0 deletions assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"main": "controllers/bootstrap/modal.js",
"fetch": "eager",
"enabled": false
},
"responsive": {
"main": "controllers/responsive.js",
"fetch": "eager",
"enabled": true
}
},
"importmap": {
Expand Down
39 changes: 39 additions & 0 deletions assets/styles/responsive.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.kreyu-dt-responsive-pending {
visibility: hidden;
}

.kreyu-dt-collapsible-row td {
padding: 0 !important;
}

.kreyu-dt-collapsible-row dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
margin: 0;
padding: 0.5rem 0.75rem;
}

.kreyu-dt-collapsible-row dt {
font-weight: 600;
white-space: nowrap;
}

.kreyu-dt-collapsible-row dd {
margin: 0;
}

.kreyu-dt-toggle-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem 0.5rem;
line-height: 1;
font-size: 1.1rem;
color: inherit;
opacity: 0.6;
}

.kreyu-dt-toggle-btn:hover {
opacity: 1;
}
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default defineConfig({
{ text: 'Exporting', link: '/docs/features/exporting' },
{ text: 'Pagination', link: '/docs/features/pagination' },
{ text: 'Personalization', link: '/docs/features/personalization' },
{ text: 'Responsive', link: '/docs/features/responsive' },
{ text: 'Persistence', link: '/docs/features/persistence' },
{ text: 'Theming', link: '/docs/features/theming' },
{ text: 'Asynchronicity', link: '/docs/features/asynchronicity' },
Expand Down
Loading
Loading