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) { |
|||
Alpine.directive('chart', function (el, { expression }, { evaluate, cleanup }) { |
|||
let config = evaluate(expression); |
|||
Alpine.directive( |
|||
"chart", |
|||
function (el, { expression }, { evaluateLater, cleanup }) { |
|||
const getContent = evaluateLater(expression); |
|||
|
|||
const chart = new window.ApexCharts(el, config); |
|||
chart.render(); |
|||
import("apexcharts").then(({ default: ApexCharts }) => { |
|||
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) { |
|||
const newData = event.detail |
|||
chart.updateSeries(newData.series, true, true); |
|||
chart.updateOptions(newData.options, true, true); |
|||
// Preserve user primary colors if explicitly provided in PHP, otherwise use CoreUI's
|
|||
if (!userConfig.colors) { |
|||
options.colors = baseConfig.colors; |
|||
} |
|||
|
|||
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(() => { |
|||
observer.disconnect(); |
|||
if (name) |
|||
window.removeEventListener( |
|||
`chart-${name}-update`, |
|||
eventHandler, |
|||
); |
|||
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) { |
|||
Alpine.directive('quill', function (el, { expression }, { evaluate, cleanup }) { |
|||
const options = expression ? evaluate(expression) : {}; |
|||
const defaultOptions = { theme: 'snow', ...options }; |
|||
Alpine.directive( |
|||
"quill", |
|||
(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') { |
|||
throw new Error('Quill must be loaded'); |
|||
// Hide the textarea without breaking Livewire bindings
|
|||
el.style.display = "none"; |
|||
} else { |
|||
editorContainer = el; |
|||
} |
|||
|
|||
const container = document.createElement('div'); |
|||
el.appendChild(container); |
|||
const quill = new window.Quill(container, defaultOptions); |
|||
// 3. Instantiate Quill on the container DIV
|
|||
const quill = new Quill(editorContainer, options); |
|||
|
|||
// Populate initial content from textarea / Alpine model
|
|||
if (targetInput.value && targetInput.value !== "<p><br></p>") { |
|||
quill.root.innerHTML = targetInput.value; |
|||
} |
|||
|
|||
let isUpdating = false; |
|||
|
|||
// 1. Text changes handler
|
|||
quill.on('text-change', () => { |
|||
// 4. Sync Quill edits -> hidden input -> Livewire
|
|||
quill.on("text-change", () => { |
|||
if (isUpdating) return; |
|||
isUpdating = true; |
|||
const htmlContent = quill.root.innerHTML; |
|||
el.value = htmlContent === '<p><br></p>' ? '' : htmlContent; |
|||
el.dispatchEvent(new Event('input', { bubbles: true })); |
|||
|
|||
const html = quill.root.innerHTML; |
|||
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; |
|||
}); |
|||
|
|||
// 2. Element Value Getter/Setter
|
|||
Object.defineProperty(el, 'value', { |
|||
get() { return quill.root.innerHTML; }, |
|||
set(newValue) { |
|||
if (isUpdating || newValue === quill.root.innerHTML) return; |
|||
// 5. Sync Livewire dynamic resets -> Quill
|
|||
const handleExternalUpdate = () => { |
|||
if (isUpdating) return; |
|||
if (targetInput.value !== quill.root.innerHTML) { |
|||
isUpdating = true; |
|||
quill.root.innerHTML = newValue || ''; |
|||
quill.root.innerHTML = targetInput.value || ""; |
|||
isUpdating = false; |
|||
}, |
|||
configurable: true |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
quill.on('selection-change', (range) => { |
|||
if (range == null) { |
|||
el.dispatchEvent(new Event('blur', { bubbles: true })); |
|||
targetInput.addEventListener("input", handleExternalUpdate); |
|||
|
|||
// 6. Cleanup DOM when Livewire removes the element
|
|||
cleanup(() => { |
|||
targetInput.removeEventListener( |
|||
"input", |
|||
handleExternalUpdate, |
|||
); |
|||
if (editorContainer !== el) { |
|||
editorContainer.remove(); |
|||
el.style.display = ""; |
|||
} |
|||
}); |
|||
|
|||
cleanup(() => el.innerHTML = '') |
|||
}) |
|||
}); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
Loading…
Reference in new issue