Browse Source
- Add `CURRENT_TERM` to `SystemSettingKey` with dynamic selection using `SelectTermQuery`. - Refactor Alpine plugins (`chart`, `quill`, `select2`) to improve configurability, compatibility with Livewire, and dynamic updates. - Clean up Blade views for streamlined script injection.master
11 changed files with 542 additions and 298 deletions
@ -1,25 +1,212 @@ |
|||||
import '../plugin/apexchart.js' |
|
||||
|
|
||||
export default function AlpineChart(Alpine) { |
export default function AlpineChart(Alpine) { |
||||
Alpine.directive('chart', function (el, { expression }, { evaluate, cleanup }) { |
Alpine.directive( |
||||
let config = evaluate(expression); |
"chart", |
||||
|
function (el, { expression }, { evaluateLater, cleanup }) { |
||||
|
const getContent = evaluateLater(expression); |
||||
|
|
||||
const chart = new window.ApexCharts(el, config); |
import("apexcharts").then(({ default: ApexCharts }) => { |
||||
chart.render(); |
getContent((userConfig) => { |
||||
|
if (!userConfig || typeof userConfig !== "object") return; |
||||
|
|
||||
|
// 1. Fetch live computed CSS variables directly from :root (document.documentElement)
|
||||
|
const getCoreUIColors = () => { |
||||
|
const style = getComputedStyle(document.body); |
||||
|
|
||||
|
// CoreUI v4/v5 & Bootstrap 5 theme variable names
|
||||
|
const bodyColor = |
||||
|
style.getPropertyValue("--cui-body-color").trim() || |
||||
|
style.getPropertyValue("--bs-body-color").trim() || |
||||
|
"#a6b0cf"; |
||||
|
|
||||
|
const secondaryColor = |
||||
|
style |
||||
|
.getPropertyValue("--cui-secondary-color") |
||||
|
.trim() || |
||||
|
style |
||||
|
.getPropertyValue("--bs-secondary-color") |
||||
|
.trim() || |
||||
|
"#8a93a2"; |
||||
|
|
||||
|
const borderColor = |
||||
|
style |
||||
|
.getPropertyValue("--cui-border-color") |
||||
|
.trim() || |
||||
|
style |
||||
|
.getPropertyValue( |
||||
|
"--bs-border-color-translucent", |
||||
|
) |
||||
|
.trim() || |
||||
|
style |
||||
|
.getPropertyValue("--bs-border-color") |
||||
|
.trim() || |
||||
|
"rgba(255, 255, 255, 0.08)"; |
||||
|
|
||||
|
const primaryColor = |
||||
|
style.getPropertyValue("--cui-primary").trim() || |
||||
|
style.getPropertyValue("--bs-primary").trim() || |
||||
|
"#6366f1"; |
||||
|
|
||||
|
const theme = |
||||
|
document.documentElement.getAttribute( |
||||
|
"data-coreui-theme", |
||||
|
) || |
||||
|
document.documentElement.getAttribute( |
||||
|
"data-bs-theme", |
||||
|
) || |
||||
|
"dark"; |
||||
|
|
||||
|
return { |
||||
|
bodyColor, |
||||
|
secondaryColor, |
||||
|
borderColor, |
||||
|
primaryColor, |
||||
|
theme, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const colors = getCoreUIColors(); |
||||
|
|
||||
|
// 2. Base Configuration explicitly overriding ApexCharts SVG text fills
|
||||
|
const baseConfig = { |
||||
|
chart: { |
||||
|
type: "line", |
||||
|
height: 360, |
||||
|
foreColor: colors.secondaryColor, // Sets default text/legend colors
|
||||
|
background: "transparent", |
||||
|
}, |
||||
|
colors: [colors.primaryColor], |
||||
|
theme: { |
||||
|
mode: colors.theme, |
||||
|
}, |
||||
|
tooltip: { |
||||
|
theme: colors.theme, |
||||
|
}, |
||||
|
grid: { |
||||
|
borderColor: colors.borderColor, |
||||
|
}, |
||||
|
xaxis: { |
||||
|
labels: { |
||||
|
style: { |
||||
|
colors: colors.secondaryColor, |
||||
|
fontSize: "12px", |
||||
|
}, |
||||
|
}, |
||||
|
axisBorder: { color: colors.borderColor }, |
||||
|
axisTicks: { color: colors.borderColor }, |
||||
|
}, |
||||
|
yaxis: { |
||||
|
labels: { |
||||
|
style: { |
||||
|
colors: colors.secondaryColor, |
||||
|
fontSize: "12px", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// Deep merge user options with base configuration
|
||||
|
const options = Object.assign({}, baseConfig, userConfig); |
||||
|
options.chart = Object.assign( |
||||
|
{}, |
||||
|
baseConfig.chart, |
||||
|
userConfig.chart, |
||||
|
); |
||||
|
options.theme = Object.assign( |
||||
|
{}, |
||||
|
baseConfig.theme, |
||||
|
userConfig.theme, |
||||
|
); |
||||
|
options.tooltip = Object.assign( |
||||
|
{}, |
||||
|
baseConfig.tooltip, |
||||
|
userConfig.tooltip, |
||||
|
); |
||||
|
|
||||
const event = function(event) { |
// Preserve user primary colors if explicitly provided in PHP, otherwise use CoreUI's
|
||||
const newData = event.detail |
if (!userConfig.colors) { |
||||
chart.updateSeries(newData.series, true, true); |
options.colors = baseConfig.colors; |
||||
chart.updateOptions(newData.options, true, true); |
|
||||
} |
} |
||||
|
|
||||
name = el.getAttribute('id'); |
requestAnimationFrame(() => { |
||||
|
const chart = new ApexCharts(el, options); |
||||
|
chart.render(); |
||||
|
|
||||
|
// 3. Theme switch handler
|
||||
|
const observer = new MutationObserver(() => { |
||||
|
const updated = getCoreUIColors(); |
||||
|
|
||||
|
chart.updateOptions( |
||||
|
{ |
||||
|
chart: { |
||||
|
foreColor: updated.secondaryColor, |
||||
|
}, |
||||
|
theme: { mode: updated.theme }, |
||||
|
tooltip: { theme: updated.theme }, |
||||
|
grid: { borderColor: updated.borderColor }, |
||||
|
xaxis: { |
||||
|
labels: { |
||||
|
style: { |
||||
|
colors: updated.secondaryColor, |
||||
|
}, |
||||
|
}, |
||||
|
axisBorder: { |
||||
|
color: updated.borderColor, |
||||
|
}, |
||||
|
axisTicks: { |
||||
|
color: updated.borderColor, |
||||
|
}, |
||||
|
}, |
||||
|
yaxis: { |
||||
|
labels: { |
||||
|
style: { |
||||
|
colors: updated.secondaryColor, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
false, |
||||
|
true, |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
observer.observe(document.documentElement, { |
||||
|
attributes: true, |
||||
|
attributeFilter: [ |
||||
|
"data-coreui-theme", |
||||
|
"data-bs-theme", |
||||
|
"class", |
||||
|
], |
||||
|
}); |
||||
|
|
||||
window.addEventListener(`chart-${name}-update`, event) |
// 4. Dynamic update event listener
|
||||
|
const name = el.getAttribute("id"); |
||||
|
const eventHandler = (event) => { |
||||
|
const detail = event.detail || event; |
||||
|
if (detail.series) |
||||
|
chart.updateSeries(detail.series, true); |
||||
|
if (detail.options) |
||||
|
chart.updateOptions(detail.options, true); |
||||
|
}; |
||||
|
|
||||
|
if (name) { |
||||
|
window.addEventListener( |
||||
|
`chart-${name}-update`, |
||||
|
eventHandler, |
||||
|
); |
||||
|
} |
||||
|
|
||||
cleanup(() => { |
cleanup(() => { |
||||
|
observer.disconnect(); |
||||
|
if (name) |
||||
|
window.removeEventListener( |
||||
|
`chart-${name}-update`, |
||||
|
eventHandler, |
||||
|
); |
||||
chart.destroy(); |
chart.destroy(); |
||||
window.removeEventListener(`chart-${name}-update`, event) |
|
||||
}); |
}); |
||||
}) |
}); |
||||
|
}); |
||||
|
}); |
||||
|
}, |
||||
|
); |
||||
} |
} |
||||
|
|||||
@ -1,46 +1,94 @@ |
|||||
|
import Quill from "quill"; |
||||
|
import "quill/dist/quill.snow.css"; |
||||
|
|
||||
export default function alpineQuill(Alpine) { |
export default function alpineQuill(Alpine) { |
||||
Alpine.directive('quill', function (el, { expression }, { evaluate, cleanup }) { |
Alpine.directive( |
||||
const options = expression ? evaluate(expression) : {}; |
"quill", |
||||
const defaultOptions = { theme: 'snow', ...options }; |
(el, { expression }, { evaluateLater, cleanup }) => { |
||||
|
// 1. Fallback gracefully if x-quill has no parameters
|
||||
|
const getContent = expression |
||||
|
? evaluateLater(expression) |
||||
|
: (cb) => cb({}); |
||||
|
|
||||
|
getContent((userOptions) => { |
||||
|
const options = Object.assign( |
||||
|
{ |
||||
|
theme: "snow", |
||||
|
placeholder: "Write something...", |
||||
|
}, |
||||
|
typeof userOptions === "object" ? userOptions : {}, |
||||
|
); |
||||
|
|
||||
|
// 2. Identify or create container DIV (Quill crashes on <textarea>)
|
||||
|
let editorContainer; |
||||
|
let targetInput = el; |
||||
|
|
||||
|
if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") { |
||||
|
// Create a container DIV for Quill right before the textarea
|
||||
|
editorContainer = document.createElement("div"); |
||||
|
el.parentNode.insertBefore(editorContainer, el); |
||||
|
|
||||
if (typeof window.Quill === 'undefined') { |
// Hide the textarea without breaking Livewire bindings
|
||||
throw new Error('Quill must be loaded'); |
el.style.display = "none"; |
||||
|
} else { |
||||
|
editorContainer = el; |
||||
} |
} |
||||
|
|
||||
const container = document.createElement('div'); |
// 3. Instantiate Quill on the container DIV
|
||||
el.appendChild(container); |
const quill = new Quill(editorContainer, options); |
||||
const quill = new window.Quill(container, defaultOptions); |
|
||||
|
// Populate initial content from textarea / Alpine model
|
||||
|
if (targetInput.value && targetInput.value !== "<p><br></p>") { |
||||
|
quill.root.innerHTML = targetInput.value; |
||||
|
} |
||||
|
|
||||
let isUpdating = false; |
let isUpdating = false; |
||||
|
|
||||
// 1. Text changes handler
|
// 4. Sync Quill edits -> hidden input -> Livewire
|
||||
quill.on('text-change', () => { |
quill.on("text-change", () => { |
||||
if (isUpdating) return; |
if (isUpdating) return; |
||||
isUpdating = true; |
isUpdating = true; |
||||
const htmlContent = quill.root.innerHTML; |
|
||||
el.value = htmlContent === '<p><br></p>' ? '' : htmlContent; |
const html = quill.root.innerHTML; |
||||
el.dispatchEvent(new Event('input', { bubbles: true })); |
const value = html === "<p><br></p>" ? "" : html; |
||||
|
|
||||
|
targetInput.value = value; |
||||
|
|
||||
|
// Dispatch input & change events on the target input so Livewire detects it
|
||||
|
targetInput.dispatchEvent( |
||||
|
new Event("input", { bubbles: true }), |
||||
|
); |
||||
|
targetInput.dispatchEvent( |
||||
|
new Event("change", { bubbles: true }), |
||||
|
); |
||||
|
|
||||
isUpdating = false; |
isUpdating = false; |
||||
}); |
}); |
||||
|
|
||||
// 2. Element Value Getter/Setter
|
// 5. Sync Livewire dynamic resets -> Quill
|
||||
Object.defineProperty(el, 'value', { |
const handleExternalUpdate = () => { |
||||
get() { return quill.root.innerHTML; }, |
if (isUpdating) return; |
||||
set(newValue) { |
if (targetInput.value !== quill.root.innerHTML) { |
||||
if (isUpdating || newValue === quill.root.innerHTML) return; |
|
||||
isUpdating = true; |
isUpdating = true; |
||||
quill.root.innerHTML = newValue || ''; |
quill.root.innerHTML = targetInput.value || ""; |
||||
isUpdating = false; |
isUpdating = false; |
||||
}, |
} |
||||
configurable: true |
}; |
||||
}); |
|
||||
|
|
||||
quill.on('selection-change', (range) => { |
targetInput.addEventListener("input", handleExternalUpdate); |
||||
if (range == null) { |
|
||||
el.dispatchEvent(new Event('blur', { bubbles: true })); |
// 6. Cleanup DOM when Livewire removes the element
|
||||
|
cleanup(() => { |
||||
|
targetInput.removeEventListener( |
||||
|
"input", |
||||
|
handleExternalUpdate, |
||||
|
); |
||||
|
if (editorContainer !== el) { |
||||
|
editorContainer.remove(); |
||||
|
el.style.display = ""; |
||||
} |
} |
||||
}); |
}); |
||||
|
}); |
||||
cleanup(() => el.innerHTML = '') |
}, |
||||
}) |
); |
||||
} |
} |
||||
|
|||||
@ -1,23 +1,23 @@ |
|||||
// resources/js/app.js
|
// resources/js/app.js
|
||||
import { Alpine, Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm'; |
import { Alpine, Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm'; |
||||
import bs from './alpine/bs'; |
import bs from './alpine/bs'; |
||||
import alpineAxios from './alpine/axios'; |
import alpineAxios from './alpine/axios'; |
||||
import alpineSelect2 from "./alpine/select2.js"; |
import alpineSelect2 from "./alpine/select2.js"; |
||||
import alpineQuill from "./alpine/quill.js"; |
import alpineQuill from "./alpine/quill.js"; |
||||
import alpineAsk from "./alpine/ask.js"; |
import alpineAsk from "./alpine/ask.js"; |
||||
import AlpineChart from "./alpine/chart.js"; |
import AlpineChart from "./alpine/chart.js"; |
||||
|
|
||||
// 1. Expose both instances globally so Blade directives can see them
|
// 1. Expose both instances globally so Blade directives can see them
|
||||
window.Alpine = Alpine; |
window.Alpine = Alpine; |
||||
window.Livewire = Livewire; |
window.Livewire = Livewire; |
||||
|
|
||||
// 2. Wrap plugin registration inside the initialization event listener
|
// 2. Wrap plugin registration inside the initialization event listener
|
||||
Alpine.plugin(bs); |
Alpine.plugin(bs); |
||||
Alpine.plugin(alpineAxios); |
Alpine.plugin(alpineAxios); |
||||
Alpine.plugin(alpineAsk); |
Alpine.plugin(alpineAsk); |
||||
Alpine.plugin(alpineSelect2); |
Alpine.plugin(alpineSelect2); |
||||
Alpine.plugin(alpineQuill); |
Alpine.plugin(alpineQuill); |
||||
Alpine.plugin(AlpineChart); |
Alpine.plugin(AlpineChart); |
||||
|
|
||||
// 3. Boot Livewire
|
// 3. Boot Livewire
|
||||
Livewire.start(); |
Livewire.start(); |
||||
|
|||||
Loading…
Reference in new issue