// If you want to use Phoenix channels, run `mix help phx.gen.channel` // to get started and then uncomment the line below. // import "./user_socket.js" // You can include dependencies in two ways. // // The simplest option is to put them in assets/vendor and // import them using relative paths: // // import "../vendor/some-package.js" // // Alternatively, you can `npm install some-package --prefix assets` and import // them using a path starting with the package name: // // import "some-package" // // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") // Hooks for LiveView components let Hooks = {} // CopyToClipboard hook: Copies text to clipboard when triggered by server event Hooks.CopyToClipboard = { mounted() { this.handleEvent("copy_to_clipboard", ({text}) => { if (navigator.clipboard) { navigator.clipboard.writeText(text).catch(err => { console.error("Clipboard write failed:", err) }) } else { // Fallback for older browsers const textArea = document.createElement("textarea") textArea.value = text textArea.style.position = "fixed" textArea.style.left = "-999999px" document.body.appendChild(textArea) textArea.select() try { document.execCommand("copy") } catch (err) { console.error("Fallback clipboard copy failed:", err) } document.body.removeChild(textArea) } }) } } // ComboBox hook: Prevents form submission when Enter is pressed in dropdown Hooks.ComboBox = { mounted() { this.handleKeyDown = (e) => { const isDropdownOpen = this.el.getAttribute("aria-expanded") === "true" if (e.key === "Enter" && isDropdownOpen) { e.preventDefault() } } this.el.addEventListener("keydown", this.handleKeyDown) }, destroyed() { this.el.removeEventListener("keydown", this.handleKeyDown) } } let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, hooks: Hooks }) // Listen for custom events from LiveView window.addEventListener("phx:set-input-value", (e) => { const {id, value} = e.detail const input = document.getElementById(id) if (input) { input.value = value } }) // Show progress bar on live navigation and form submits topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket // Sidebar accessibility improvements document.addEventListener("DOMContentLoaded", () => { const drawerToggle = document.getElementById("main-drawer") const sidebarToggle = document.getElementById("sidebar-toggle") const sidebar = document.getElementById("main-sidebar") if (!drawerToggle || !sidebarToggle || !sidebar) return // Manage tabindex for sidebar elements based on open/closed state const updateSidebarTabIndex = (isOpen) => { // Find all potentially focusable elements (including those with tabindex="-1") const allFocusableElements = sidebar.querySelectorAll( 'a[href], button, select, input:not([type="hidden"]), [tabindex]' ) allFocusableElements.forEach(el => { // Skip the overlay button if (el.closest('.drawer-overlay')) return if (isOpen) { // Remove tabindex="-1" to make focusable when open if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') === '-1') { el.removeAttribute('tabindex') } } else { // Set tabindex="-1" to remove from tab order when closed if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '-1') } else if (el.getAttribute('tabindex') !== '-1') { // Store original tabindex in data attribute before setting to -1 if (!el.hasAttribute('data-original-tabindex')) { el.setAttribute('data-original-tabindex', el.getAttribute('tabindex')) } el.setAttribute('tabindex', '-1') } } }) } // Find first focusable element in sidebar // Priority: first navigation link (menuitem) > other links > other focusable elements const getFirstFocusableElement = () => { // First, try to find the first navigation link (menuitem) const firstNavLink = sidebar.querySelector('a[href][role="menuitem"]:not([tabindex="-1"])') if (firstNavLink && !firstNavLink.closest('.drawer-overlay')) { return firstNavLink } // Fallback: any navigation link const firstLink = sidebar.querySelector('a[href]:not([tabindex="-1"])') if (firstLink && !firstLink.closest('.drawer-overlay')) { return firstLink } // Last resort: any other focusable element const focusableSelectors = [ 'button:not([tabindex="-1"]):not([disabled])', 'select:not([tabindex="-1"]):not([disabled])', 'input:not([tabindex="-1"]):not([disabled]):not([type="hidden"])', '[tabindex]:not([tabindex="-1"])' ] for (const selector of focusableSelectors) { const element = sidebar.querySelector(selector) if (element && !element.closest('.drawer-overlay')) { return element } } return null } // Update aria-expanded when drawer state changes const updateAriaExpanded = () => { const isOpen = drawerToggle.checked sidebarToggle.setAttribute("aria-expanded", isOpen.toString()) // Update dropdown aria-expanded if present const userMenuButton = sidebar.querySelector('button[aria-haspopup="true"]') if (userMenuButton) { const dropdown = userMenuButton.closest('.dropdown') const isDropdownOpen = dropdown?.classList.contains('dropdown-open') if (userMenuButton) { userMenuButton.setAttribute("aria-expanded", (isDropdownOpen || false).toString()) } } } // Listen for changes to the drawer checkbox drawerToggle.addEventListener("change", () => { const isOpen = drawerToggle.checked updateAriaExpanded() updateSidebarTabIndex(isOpen) if (!isOpen) { // When closing, return focus to toggle button sidebarToggle.focus() } }) // Update on initial load updateAriaExpanded() updateSidebarTabIndex(drawerToggle.checked) // Close sidebar with ESC key document.addEventListener("keydown", (e) => { if (e.key === "Escape" && drawerToggle.checked) { drawerToggle.checked = false updateAriaExpanded() updateSidebarTabIndex(false) // Return focus to toggle button sidebarToggle.focus() } }) // Improve keyboard navigation for sidebar toggle sidebarToggle.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() const wasOpen = drawerToggle.checked drawerToggle.checked = !drawerToggle.checked updateAriaExpanded() // If opening, move focus to first element in sidebar if (!wasOpen && drawerToggle.checked) { updateSidebarTabIndex(true) // Use setTimeout to ensure DOM is updated setTimeout(() => { const firstElement = getFirstFocusableElement() if (firstElement) { firstElement.focus() } }, 50) } else if (wasOpen && !drawerToggle.checked) { updateSidebarTabIndex(false) } } }) // Also handle click events to update tabindex and focus sidebarToggle.addEventListener("click", () => { setTimeout(() => { const isOpen = drawerToggle.checked updateSidebarTabIndex(isOpen) if (isOpen) { const firstElement = getFirstFocusableElement() if (firstElement) { firstElement.focus() } } }, 50) }) // Handle dropdown keyboard navigation const userMenuButton = sidebar?.querySelector('button[aria-haspopup="true"]') if (userMenuButton) { userMenuButton.addEventListener("click", () => { setTimeout(updateAriaExpanded, 0) }) userMenuButton.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() userMenuButton.click() } }) } })