replace admin rail with unified bottom sheet editor
All checks were successful
deploy / deploy (push) Successful in 1m30s
All checks were successful
deploy / deploy (push) Successful in 1m30s
- add editor sheet component anchored bottom (mobile) / right (desktop) - admin cog moves to header, always visible for admins - remove Done button from editor header, keep only Save - add editor_at_defaults tracking to disable Reset when at defaults - sheet collapses on click outside or Escape, stays in edit mode - dirty indicator + beforeunload warning for unsaved changes - keyboard shortcuts: Ctrl+Z undo, Ctrl+Shift+Z redo - WCAG compliant: aria-expanded, live region, focus management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
105
assets/js/app.js
105
assets/js/app.js
@@ -478,6 +478,56 @@ const SearchModal = {
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile nav drawer - slides in from the left
|
||||
const MobileNavDrawer = {
|
||||
mounted() {
|
||||
this.el.addEventListener("open-mobile-nav", () => this.open())
|
||||
this.el.addEventListener("close-mobile-nav", () => this.close())
|
||||
|
||||
// Close on Escape key
|
||||
this._keydown = (e) => {
|
||||
if (e.key === "Escape" && this.isOpen()) {
|
||||
e.preventDefault()
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", this._keydown)
|
||||
|
||||
// Prevent hamburger button navigation on no-JS fallback
|
||||
this._hamburger = document.querySelector('.header-hamburger')
|
||||
if (this._hamburger) {
|
||||
this._preventNav = (e) => e.preventDefault()
|
||||
this._hamburger.addEventListener("click", this._preventNav)
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.removeEventListener("keydown", this._keydown)
|
||||
if (this._hamburger && this._preventNav) {
|
||||
this._hamburger.removeEventListener("click", this._preventNav)
|
||||
}
|
||||
},
|
||||
|
||||
isOpen() {
|
||||
return this.el.classList.contains("is-open")
|
||||
},
|
||||
|
||||
open() {
|
||||
this.el.classList.add("is-open")
|
||||
document.body.style.overflow = "hidden"
|
||||
// Focus the close button for accessibility
|
||||
const closeBtn = this.el.querySelector(".mobile-nav-close")
|
||||
if (closeBtn) closeBtn.focus()
|
||||
},
|
||||
|
||||
close() {
|
||||
this.el.classList.remove("is-open")
|
||||
document.body.style.overflow = ""
|
||||
// Return focus to hamburger button
|
||||
if (this._hamburger) this._hamburger.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Flex-wrap base → horizontal scroll enhancement for collection category pills.
|
||||
// If the pills wrap past 2 rows on mobile, switches to single-row scroll
|
||||
// and scrolls the active pill into view.
|
||||
@@ -629,6 +679,53 @@ const DirtyGuard = {
|
||||
}
|
||||
}
|
||||
|
||||
// EditorSheet: simple open/collapse sheet for page editing
|
||||
// Positioning is handled by CSS - JS just handles click-outside and Escape
|
||||
const EditorSheet = {
|
||||
mounted() {
|
||||
// Click outside to collapse (works in any mode for preview)
|
||||
// Use mousedown instead of click to avoid race with LiveView re-renders
|
||||
this._onDocMousedown = (e) => {
|
||||
if (!this.el.contains(e.target) && this._getState() !== "collapsed") {
|
||||
this._setState("collapsed")
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", this._onDocMousedown)
|
||||
|
||||
// Escape key to collapse
|
||||
this._onKeydown = (e) => {
|
||||
if (e.key === "Escape" && this._getState() !== "collapsed") {
|
||||
e.preventDefault()
|
||||
this._setState("collapsed")
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", this._onKeydown)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
document.removeEventListener("mousedown", this._onDocMousedown)
|
||||
document.removeEventListener("keydown", this._onKeydown)
|
||||
},
|
||||
|
||||
_getState() {
|
||||
return this.el.dataset.state || "collapsed"
|
||||
},
|
||||
|
||||
_setState(state) {
|
||||
this.el.dataset.state = state
|
||||
this.el.setAttribute("aria-expanded", state !== "collapsed")
|
||||
this.pushEvent("editor_set_sheet_state", { state })
|
||||
this._announce(state === "collapsed" ? "Editor collapsed" : "Editor expanded")
|
||||
},
|
||||
|
||||
_announce(message) {
|
||||
const region = document.getElementById("editor-live-region")
|
||||
if (region) {
|
||||
region.textContent = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DirtyGuard + Ctrl+Z / Ctrl+Shift+Z undo/redo for page editors
|
||||
const EditorKeyboard = {
|
||||
mounted() {
|
||||
@@ -682,7 +779,7 @@ const EditorKeyboard = {
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
params: {_csrf_token: csrfToken, screen_width: window.innerWidth},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard},
|
||||
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, MobileNavDrawer, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard, EditorSheet},
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
@@ -701,6 +798,12 @@ window.addEventListener("phx:scroll-preview-top", (e) => {
|
||||
// connect if there are any LiveViews on the page
|
||||
liveSocket.connect()
|
||||
|
||||
// Suppress connection error banners during intentional navigation (refresh, link click).
|
||||
// beforeunload fires on navigation but NOT on genuine connection drops.
|
||||
window.addEventListener("beforeunload", () => {
|
||||
document.body.classList.add("navigating-away")
|
||||
})
|
||||
|
||||
// expose liveSocket on window for web console debug logs and latency simulation:
|
||||
// >> liveSocket.enableDebug()
|
||||
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
||||
|
||||
Reference in New Issue
Block a user