perf(assets): lazy-load three more hooks + trim CopyToClipboard
DeviceListReorder, SortableList, MikrotikPortSync are each used on a single page (devices index, escalation/schedule show, device edit form respectively) — push them out of app.js into their own chunks via lazyHook. Also extract MikrotikPortSync into its own file under hooks/ for consistency. Compress CopyToClipboard's flash feedback by collapsing the duplicated success/error inline-SVG/setTimeout blocks into one helper, drop the debug console.logs, and remove the bulk Phoenix-template boilerplate comment block that was just generated noise. After-bundle (minified): app.js 287.6 KB (-2 KB on top of prior split) device_list_reorder 4.6 KB loaded on /devices only sortable_list 3.1 KB loaded on escalation+schedule pages mikrotik_port_sync 1.3 KB loaded on device edit form only
This commit is contained in:
parent
3bd4ed303c
commit
7ffc4ebd2b
2 changed files with 79 additions and 151 deletions
181
assets/js/app.ts
181
assets/js/app.ts
|
|
@ -1,105 +1,44 @@
|
|||
// 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"
|
||||
//
|
||||
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
|
||||
// To load it, simply add a second `<link>` to your `root.html.heex` file.
|
||||
|
||||
// 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 { hooks as colocatedHooks } from "phoenix-colocated/towerops"
|
||||
import topbar from "../vendor/topbar"
|
||||
import { DeviceListReorder } from "./device_list_reorder"
|
||||
import { SortableList } from "./sortable_list"
|
||||
import { initCookieConsent } from "./cookie_consent"
|
||||
|
||||
|
||||
// LiveView hook for copying to clipboard
|
||||
// Copy a target element's text to the clipboard, with brief inline feedback.
|
||||
const CopyToClipboard = {
|
||||
handleClick: null as ((e: Event) => void) | null,
|
||||
|
||||
mounted(this: any) {
|
||||
this.handleClick = (e: Event) => {
|
||||
e.preventDefault()
|
||||
const targetId = this.el.dataset.target
|
||||
if (!targetId) {
|
||||
console.error("No data-target attribute found on copy button")
|
||||
return
|
||||
}
|
||||
const targetEl = document.querySelector(this.el.dataset.target || '') as HTMLElement | null
|
||||
if (!targetEl) return
|
||||
|
||||
const targetEl = document.querySelector(targetId) as HTMLInputElement | HTMLTextAreaElement | HTMLElement
|
||||
if (!targetEl) {
|
||||
console.error(`Target element not found: ${targetId}`)
|
||||
return
|
||||
}
|
||||
const text = ((targetEl as HTMLInputElement | HTMLTextAreaElement).value
|
||||
?? targetEl.innerText
|
||||
?? targetEl.textContent
|
||||
?? '').trim()
|
||||
if (!text) return
|
||||
|
||||
let text = ""
|
||||
if (targetEl.tagName === "INPUT" || targetEl.tagName === "TEXTAREA") {
|
||||
text = (targetEl as HTMLInputElement | HTMLTextAreaElement).value
|
||||
} else {
|
||||
text = targetEl.innerText || targetEl.textContent || ""
|
||||
}
|
||||
|
||||
// Trim any whitespace from the text
|
||||
text = text.trim()
|
||||
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('Copied to clipboard successfully')
|
||||
// Show visual feedback
|
||||
const originalHTML = this.el.innerHTML
|
||||
const originalClasses = this.el.className
|
||||
|
||||
// Update button to show success state
|
||||
this.el.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg><span>Copied!</span>'
|
||||
this.el.className = originalClasses.replace('bg-blue-600', 'bg-green-600').replace('hover:bg-blue-700', 'hover:bg-green-700').replace('bg-blue-500', 'bg-green-500').replace('hover:bg-blue-600', 'hover:bg-green-600')
|
||||
|
||||
setTimeout(() => {
|
||||
this.el.innerHTML = originalHTML
|
||||
this.el.className = originalClasses
|
||||
}, 2000)
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err)
|
||||
// Show error state
|
||||
const originalHTML = this.el.innerHTML
|
||||
const originalClasses = this.el.className
|
||||
|
||||
this.el.innerHTML = '<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg><span>Failed</span>'
|
||||
this.el.className = originalClasses.replace('bg-blue-600', 'bg-red-600').replace('hover:bg-blue-700', 'hover:bg-red-700').replace('bg-blue-500', 'bg-red-500').replace('hover:bg-blue-600', 'hover:bg-red-600')
|
||||
|
||||
setTimeout(() => {
|
||||
this.el.innerHTML = originalHTML
|
||||
this.el.className = originalClasses
|
||||
}, 2000)
|
||||
})
|
||||
} else {
|
||||
console.error('No text found to copy')
|
||||
}
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => this.flash('Copied!', 'green'))
|
||||
.catch(() => this.flash('Failed', 'red'))
|
||||
}
|
||||
|
||||
this.el.addEventListener("click", this.handleClick)
|
||||
},
|
||||
|
||||
flash(this: any, label: string, color: 'green' | 'red') {
|
||||
const originalHTML = this.el.innerHTML
|
||||
const originalClasses = this.el.className
|
||||
this.el.innerHTML = `<span>${label}</span>`
|
||||
this.el.className = originalClasses.replace(/bg-blue-(500|600)/g, `bg-${color}-$1`).replace(/hover:bg-blue-(600|700)/g, `hover:bg-${color}-$1`)
|
||||
setTimeout(() => { this.el.innerHTML = originalHTML; this.el.className = originalClasses }, 2000)
|
||||
},
|
||||
|
||||
destroyed(this: any) {
|
||||
if (this.handleClick) {
|
||||
this.el.removeEventListener("click", this.handleClick)
|
||||
this.handleClick = null
|
||||
}
|
||||
if (this.handleClick) this.el.removeEventListener("click", this.handleClick)
|
||||
this.handleClick = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,56 +169,6 @@ const DynamicFavicon = {
|
|||
}
|
||||
}
|
||||
|
||||
// MikroTik Port Sync - automatically switches port when SSL checkbox is toggled
|
||||
const MikrotikPortSync = {
|
||||
handleSslChange: null as ((e: Event) => void) | null,
|
||||
sslCheckbox: null as HTMLInputElement | null,
|
||||
|
||||
setupListener(this: any) {
|
||||
// Clean up existing listener first
|
||||
if (this.sslCheckbox && this.handleSslChange) {
|
||||
this.sslCheckbox.removeEventListener('change', this.handleSslChange)
|
||||
}
|
||||
|
||||
const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') as HTMLInputElement
|
||||
const portInput = this.el.querySelector('[name="device[mikrotik_port]"]') as HTMLInputElement
|
||||
|
||||
if (!sslCheckbox || !portInput) return
|
||||
|
||||
this.sslCheckbox = sslCheckbox
|
||||
|
||||
this.handleSslChange = () => {
|
||||
const isChecked = sslCheckbox.checked
|
||||
const currentPort = portInput.value.trim()
|
||||
|
||||
// Only update port if it's empty or set to one of the default values
|
||||
if (currentPort === '' || currentPort === '8729' || currentPort === '8728') {
|
||||
portInput.value = isChecked ? '8729' : '8728'
|
||||
// Trigger a change event so LiveView validates the new value
|
||||
portInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
sslCheckbox.addEventListener('change', this.handleSslChange)
|
||||
},
|
||||
|
||||
mounted(this: any) {
|
||||
this.setupListener()
|
||||
},
|
||||
|
||||
updated(this: any) {
|
||||
// Re-attach listener when LiveView updates the DOM (e.g., when mikrotik_enabled changes)
|
||||
this.setupListener()
|
||||
},
|
||||
|
||||
destroyed(this: any) {
|
||||
if (this.sslCheckbox && this.handleSslChange) {
|
||||
this.sslCheckbox.removeEventListener('change', this.handleSslChange)
|
||||
this.handleSslChange = null
|
||||
this.sslCheckbox = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// lazyHook wraps a Phoenix LiveView hook so its implementation is
|
||||
|
|
@ -324,6 +213,11 @@ const LeafletMap = lazyHook(() => import("./hooks/sites_map"), "LeafletMap")
|
|||
const NetworkMap = lazyHook(() => import("./hooks/network_map"), "NetworkMap")
|
||||
const WeathermapViewer = lazyHook(() => import("./hooks/weathermap"), "WeathermapViewer")
|
||||
|
||||
// Page-specific small hooks — page count keeps app.js tiny.
|
||||
const DeviceListReorder = lazyHook(() => import("./device_list_reorder"), "DeviceListReorder")
|
||||
const SortableList = lazyHook(() => import("./sortable_list"), "SortableList")
|
||||
const MikrotikPortSync = lazyHook(() => import("./hooks/mikrotik_port_sync"), "MikrotikPortSync")
|
||||
|
||||
const csrfToken = document.querySelector<HTMLMetaElement>("meta[name='csrf-token']")?.getAttribute("content")
|
||||
if (!csrfToken) {
|
||||
throw new Error('CSRF token meta tag not found')
|
||||
|
|
@ -464,27 +358,12 @@ window.addEventListener("phx:page-loading-stop", _info => {
|
|||
|
||||
// Handle clipboard copy events
|
||||
window.addEventListener("phx:copy", (event: any) => {
|
||||
// When using JS.dispatch with 'to:', the event is dispatched to that element
|
||||
// and event.target will be that element
|
||||
const el = event.target as HTMLElement
|
||||
const text = el.tagName === "INPUT" || el.tagName === "TEXTAREA"
|
||||
? (el as HTMLInputElement | HTMLTextAreaElement).value
|
||||
: (el.innerText || el.textContent || "")
|
||||
|
||||
let text = ""
|
||||
|
||||
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
||||
text = (el as HTMLInputElement | HTMLTextAreaElement).value
|
||||
} else {
|
||||
text = el.innerText || el.textContent || ""
|
||||
}
|
||||
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
console.log('Copied to clipboard:', text.substring(0, 50) + (text.length > 50 ? '...' : ''))
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text:', err)
|
||||
})
|
||||
} else {
|
||||
console.warn('No text found to copy from element:', el)
|
||||
}
|
||||
if (text) navigator.clipboard.writeText(text).catch(err => console.error('clipboard:', err))
|
||||
})
|
||||
|
||||
// Handle file download events
|
||||
|
|
|
|||
49
assets/js/hooks/mikrotik_port_sync.ts
Normal file
49
assets/js/hooks/mikrotik_port_sync.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Auto-switches the MikroTik API port between 8728 (plain) and 8729
|
||||
// (TLS) when the SSL checkbox is toggled, but only when the port input
|
||||
// is empty or already at one of those defaults.
|
||||
|
||||
export const MikrotikPortSync = {
|
||||
handleSslChange: null as ((e: Event) => void) | null,
|
||||
sslCheckbox: null as HTMLInputElement | null,
|
||||
|
||||
setupListener(this: any) {
|
||||
if (this.sslCheckbox && this.handleSslChange) {
|
||||
this.sslCheckbox.removeEventListener('change', this.handleSslChange)
|
||||
}
|
||||
|
||||
const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') as HTMLInputElement
|
||||
const portInput = this.el.querySelector('[name="device[mikrotik_port]"]') as HTMLInputElement
|
||||
|
||||
if (!sslCheckbox || !portInput) return
|
||||
|
||||
this.sslCheckbox = sslCheckbox
|
||||
|
||||
this.handleSslChange = () => {
|
||||
const isChecked = sslCheckbox.checked
|
||||
const currentPort = portInput.value.trim()
|
||||
|
||||
if (currentPort === '' || currentPort === '8729' || currentPort === '8728') {
|
||||
portInput.value = isChecked ? '8729' : '8728'
|
||||
portInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
sslCheckbox.addEventListener('change', this.handleSslChange)
|
||||
},
|
||||
|
||||
mounted(this: any) {
|
||||
this.setupListener()
|
||||
},
|
||||
|
||||
updated(this: any) {
|
||||
this.setupListener()
|
||||
},
|
||||
|
||||
destroyed(this: any) {
|
||||
if (this.sslCheckbox && this.handleSslChange) {
|
||||
this.sslCheckbox.removeEventListener('change', this.handleSslChange)
|
||||
this.handleSslChange = null
|
||||
this.sslCheckbox = null
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue