| @ -1,13 +1,200 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import 'easy-pie-chart/dist/jquery.easypiechart.min.js'; | |||||
| import Theme from '../../utils/theme.js'; | |||||
| export default (function () { | export default (function () { | ||||
| if ($('.easy-pie-chart').length > 0) { | |||||
| $('.easy-pie-chart').easyPieChart({ | |||||
| onStep(from, to, percent) { | |||||
| this.el.children[0].innerHTML = `${Math.round(percent)} %`; | |||||
| }, | |||||
| }); | |||||
| // Vanilla JS Pie Chart implementation using SVG | |||||
| class VanillaPieChart { | |||||
| constructor(element, options = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| size: 110, | |||||
| lineWidth: 3, | |||||
| lineCap: 'round', | |||||
| trackColor: '#f2f2f2', | |||||
| barColor: '#ef1e25', | |||||
| scaleColor: false, | |||||
| animate: 1000, | |||||
| onStep: null, | |||||
| ...options, | |||||
| }; | |||||
| this.percentage = parseInt(element.dataset.percent || 0); | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.createSVG(); | |||||
| this.animate(); | |||||
| } | |||||
| createSVG() { | |||||
| const size = this.options.size; | |||||
| const lineWidth = this.options.lineWidth; | |||||
| const radius = (size - lineWidth) / 2; | |||||
| const circumference = 2 * Math.PI * radius; | |||||
| // Create SVG element | |||||
| const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |||||
| svg.setAttribute('width', size); | |||||
| svg.setAttribute('height', size); | |||||
| svg.style.transform = 'rotate(-90deg)'; | |||||
| // Create track (background circle) | |||||
| const track = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |||||
| track.setAttribute('cx', size / 2); | |||||
| track.setAttribute('cy', size / 2); | |||||
| track.setAttribute('r', radius); | |||||
| track.setAttribute('fill', 'none'); | |||||
| track.setAttribute('stroke', this.options.trackColor); | |||||
| track.setAttribute('stroke-width', lineWidth); | |||||
| // Create bar (progress circle) | |||||
| const bar = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |||||
| bar.setAttribute('cx', size / 2); | |||||
| bar.setAttribute('cy', size / 2); | |||||
| bar.setAttribute('r', radius); | |||||
| bar.setAttribute('fill', 'none'); | |||||
| bar.setAttribute('stroke', this.options.barColor); | |||||
| bar.setAttribute('stroke-width', lineWidth); | |||||
| bar.setAttribute('stroke-linecap', this.options.lineCap); | |||||
| bar.setAttribute('stroke-dasharray', circumference); | |||||
| bar.setAttribute('stroke-dashoffset', circumference); | |||||
| // Add elements to SVG | |||||
| svg.appendChild(track); | |||||
| svg.appendChild(bar); | |||||
| // Clear element and add SVG | |||||
| this.element.innerHTML = ''; | |||||
| this.element.appendChild(svg); | |||||
| // Add percentage text | |||||
| const textElement = document.createElement('div'); | |||||
| textElement.style.position = 'absolute'; | |||||
| textElement.style.top = '50%'; | |||||
| textElement.style.left = '50%'; | |||||
| textElement.style.transform = 'translate(-50%, -50%)'; | |||||
| textElement.style.fontSize = '14px'; | |||||
| textElement.style.fontWeight = 'bold'; | |||||
| textElement.style.color = Theme.getCSSVar('--c-text-base') || '#333'; | |||||
| textElement.textContent = '0%'; | |||||
| this.element.style.position = 'relative'; | |||||
| this.element.appendChild(textElement); | |||||
| // Store references | |||||
| this.svg = svg; | |||||
| this.bar = bar; | |||||
| this.textElement = textElement; | |||||
| this.circumference = circumference; | |||||
| } | |||||
| animate() { | |||||
| const targetOffset = this.circumference - (this.percentage / 100) * this.circumference; | |||||
| const duration = this.options.animate; | |||||
| const startTime = Date.now(); | |||||
| const startOffset = this.circumference; | |||||
| const animateStep = () => { | |||||
| const elapsed = Date.now() - startTime; | |||||
| const progress = Math.min(elapsed / duration, 1); | |||||
| // Easing function (easeOutCubic) | |||||
| const easeProgress = 1 - Math.pow(1 - progress, 3); | |||||
| const currentOffset = startOffset - (startOffset - targetOffset) * easeProgress; | |||||
| const currentPercent = ((this.circumference - currentOffset) / this.circumference) * 100; | |||||
| this.bar.setAttribute('stroke-dashoffset', currentOffset); | |||||
| this.textElement.textContent = `${Math.round(currentPercent)}%`; | |||||
| // Call onStep callback if provided | |||||
| if (this.options.onStep) { | |||||
| this.options.onStep.call(this, 0, this.percentage, currentPercent); | |||||
| } | |||||
| if (progress < 1) { | |||||
| requestAnimationFrame(animateStep); | |||||
| } | |||||
| }; | |||||
| requestAnimationFrame(animateStep); | |||||
| } | |||||
| update(percentage) { | |||||
| this.percentage = percentage; | |||||
| this.animate(); | |||||
| } | |||||
| destroy() { | |||||
| if (this.element) { | |||||
| this.element.innerHTML = ''; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| }()) | |||||
| // Initialize all pie charts | |||||
| const initializePieCharts = () => { | |||||
| const pieChartElements = document.querySelectorAll('.easy-pie-chart'); | |||||
| pieChartElements.forEach(element => { | |||||
| // Skip if already initialized | |||||
| if (element.pieChartInstance) { | |||||
| element.pieChartInstance.destroy(); | |||||
| } | |||||
| // Get theme colors | |||||
| const isDark = Theme.current() === 'dark'; | |||||
| const barColor = element.dataset.barColor || (isDark ? '#4f46e5' : '#ef4444'); | |||||
| const trackColor = element.dataset.trackColor || (isDark ? '#374151' : '#f3f4f6'); | |||||
| // Create pie chart instance | |||||
| const pieChart = new VanillaPieChart(element, { | |||||
| size: parseInt(element.dataset.size || 110), | |||||
| lineWidth: parseInt(element.dataset.lineWidth || 3), | |||||
| barColor, | |||||
| trackColor, | |||||
| animate: parseInt(element.dataset.animate || 1000), | |||||
| onStep(from, to, percent) { | |||||
| // Update the percentage display | |||||
| const textElement = this.element.querySelector('div'); | |||||
| if (textElement) { | |||||
| textElement.innerHTML = `${Math.round(percent)}%`; | |||||
| } | |||||
| }, | |||||
| }); | |||||
| // Store instance for cleanup | |||||
| element.pieChartInstance = pieChart; | |||||
| }); | |||||
| }; | |||||
| // Initialize on load | |||||
| initializePieCharts(); | |||||
| // Reinitialize on theme change | |||||
| window.addEventListener('adminator:themeChanged', () => { | |||||
| setTimeout(initializePieCharts, 100); | |||||
| }); | |||||
| // Reinitialize on window resize | |||||
| window.addEventListener('resize', () => { | |||||
| setTimeout(initializePieCharts, 100); | |||||
| }); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| const pieChartElements = document.querySelectorAll('.easy-pie-chart'); | |||||
| pieChartElements.forEach(element => { | |||||
| if (element.pieChartInstance) { | |||||
| element.pieChartInstance.destroy(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| // Return public API | |||||
| return { | |||||
| init: initializePieCharts, | |||||
| VanillaPieChart, | |||||
| }; | |||||
| }()); | |||||
| @ -1,254 +1,208 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import 'jquery-sparkline'; | |||||
| import { Chart, registerables } from 'chart.js'; | |||||
| import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||
| import { COLORS } from '../../constants/colors'; | import { COLORS } from '../../constants/colors'; | ||||
| import Theme from '../../utils/theme.js'; | import Theme from '../../utils/theme.js'; | ||||
| // Register Chart.js components | |||||
| Chart.register(...registerables); | |||||
| export default (function () { | export default (function () { | ||||
| // Store chart instances for cleanup | |||||
| let chartInstances = []; | |||||
| // ------------------------------------------------------ | // ------------------------------------------------------ | ||||
| // @Dashboard Sparklines | |||||
| // @Sparkline Chart Creation Helpers | |||||
| // ------------------------------------------------------ | // ------------------------------------------------------ | ||||
| const drawSparklines = () => { | |||||
| const sparkColors = Theme.getSparklineColors(); | |||||
| if ($('#sparklinedash').length > 0) { | |||||
| $('#sparklinedash').sparkline([0, 5, 6, 10, 9, 12, 4, 9], { | |||||
| type: 'bar', | |||||
| height: '20', | |||||
| barWidth: '3', | |||||
| resize: true, | |||||
| barSpacing: '3', | |||||
| barColor: sparkColors.success, | |||||
| }); | |||||
| } | |||||
| const createSparklineChart = (elementId, data, color, type = 'bar') => { | |||||
| const element = document.getElementById(elementId); | |||||
| if (!element) return null; | |||||
| if ($('#sparklinedash2').length > 0) { | |||||
| $('#sparklinedash2').sparkline([0, 5, 6, 10, 9, 12, 4, 9], { | |||||
| type: 'bar', | |||||
| height: '20', | |||||
| barWidth: '3', | |||||
| resize: true, | |||||
| barSpacing: '3', | |||||
| barColor: sparkColors.purple, | |||||
| }); | |||||
| // Clear existing chart | |||||
| const existingChart = chartInstances.find(chart => chart.canvas.id === elementId); | |||||
| if (existingChart) { | |||||
| existingChart.destroy(); | |||||
| chartInstances = chartInstances.filter(chart => chart.canvas.id !== elementId); | |||||
| } | } | ||||
| if ($('#sparklinedash3').length > 0) { | |||||
| $('#sparklinedash3').sparkline([0, 5, 6, 10, 9, 12, 4, 9], { | |||||
| type: 'bar', | |||||
| height: '20', | |||||
| barWidth: '3', | |||||
| resize: true, | |||||
| barSpacing: '3', | |||||
| barColor: sparkColors.info, | |||||
| }); | |||||
| // Create canvas if it doesn't exist | |||||
| let canvas = element.querySelector('canvas'); | |||||
| if (!canvas) { | |||||
| canvas = document.createElement('canvas'); | |||||
| canvas.id = `${elementId }-canvas`; | |||||
| element.appendChild(canvas); | |||||
| } | } | ||||
| if ($('#sparklinedash4').length > 0) { | |||||
| $('#sparklinedash4').sparkline([0, 5, 6, 10, 9, 12, 4, 9], { | |||||
| type: 'bar', | |||||
| height: '20', | |||||
| barWidth: '3', | |||||
| resize: true, | |||||
| barSpacing: '3', | |||||
| barColor: sparkColors.danger, | |||||
| }); | |||||
| } | |||||
| // Set canvas size | |||||
| canvas.width = element.offsetWidth || 100; | |||||
| canvas.height = 20; | |||||
| const ctx = canvas.getContext('2d'); | |||||
| const chartConfig = { | |||||
| type, | |||||
| data: { | |||||
| labels: data.map((_, index) => index), | |||||
| datasets: [{ | |||||
| data, | |||||
| backgroundColor: color, | |||||
| borderColor: color, | |||||
| borderWidth: type === 'line' ? 2 : 0, | |||||
| barThickness: 3, | |||||
| categoryPercentage: 1.0, | |||||
| barPercentage: 0.8, | |||||
| fill: false, | |||||
| pointRadius: 0, | |||||
| pointHoverRadius: 0, | |||||
| }], | |||||
| }, | |||||
| options: { | |||||
| responsive: true, | |||||
| maintainAspectRatio: false, | |||||
| plugins: { | |||||
| legend: { | |||||
| display: false, | |||||
| }, | |||||
| tooltip: { | |||||
| enabled: false, | |||||
| }, | |||||
| }, | |||||
| scales: { | |||||
| x: { | |||||
| display: false, | |||||
| grid: { | |||||
| display: false, | |||||
| }, | |||||
| }, | |||||
| y: { | |||||
| display: false, | |||||
| grid: { | |||||
| display: false, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| elements: { | |||||
| bar: { | |||||
| borderRadius: 0, | |||||
| }, | |||||
| line: { | |||||
| tension: 0.1, | |||||
| }, | |||||
| }, | |||||
| layout: { | |||||
| padding: 0, | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| const chart = new Chart(ctx, chartConfig); | |||||
| chartInstances.push(chart); | |||||
| return chart; | |||||
| }; | }; | ||||
| drawSparklines(); | |||||
| const createSparklineForElements = (selector, data, color, type = 'bar') => { | |||||
| const elements = document.querySelectorAll(selector); | |||||
| elements.forEach((element, index) => { | |||||
| const elementId = element.id || `sparkline-${selector.replace(/[^a-zA-Z0-9]/g, '')}-${index}`; | |||||
| if (!element.id) element.id = elementId; | |||||
| createSparklineChart(elementId, data, color, type); | |||||
| }); | |||||
| }; | |||||
| // Redraw sparklines on resize | |||||
| $(window).resize(debounce(drawSparklines, 150)); | |||||
| // Listen for theme changes | |||||
| window.addEventListener('adminator:themeChanged', debounce(drawSparklines, 150)); | |||||
| // ------------------------------------------------------ | |||||
| // @Dashboard Sparklines | |||||
| // ------------------------------------------------------ | |||||
| const drawSparklines = () => { | |||||
| const sparkColors = Theme.getSparklineColors(); | |||||
| const data = [0, 5, 6, 10, 9, 12, 4, 9]; | |||||
| // Dashboard sparklines | |||||
| createSparklineChart('sparklinedash', data, sparkColors.success); | |||||
| createSparklineChart('sparklinedash2', data, sparkColors.purple); | |||||
| createSparklineChart('sparklinedash3', data, sparkColors.info); | |||||
| createSparklineChart('sparklinedash4', data, sparkColors.danger); | |||||
| }; | |||||
| // ------------------------------------------------------ | // ------------------------------------------------------ | ||||
| // @Other Sparklines | // @Other Sparklines | ||||
| // ------------------------------------------------------ | // ------------------------------------------------------ | ||||
| $('#sparkline').sparkline( | |||||
| [5, 6, 7, 9, 9, 5, 3, 2, 2, 4, 6, 7], | |||||
| { | |||||
| type: 'line', | |||||
| resize: true, | |||||
| height: '20', | |||||
| } | |||||
| ); | |||||
| $('#compositebar').sparkline( | |||||
| 'html', | |||||
| { | |||||
| type: 'bar', | |||||
| resize: true, | |||||
| barColor: Theme.getSparklineColors().light, | |||||
| height: '20', | |||||
| } | |||||
| ); | |||||
| $('#compositebar').sparkline( | |||||
| [4, 1, 5, 7, 9, 9, 8, 7, 6, 6, 4, 7, 8, 4, 3, 2, 2, 5, 6, 7], | |||||
| { | |||||
| composite: true, | |||||
| fillColor: false, | |||||
| lineColor: 'red', | |||||
| resize: true, | |||||
| height: '20', | |||||
| } | |||||
| ); | |||||
| $('#normalline').sparkline( | |||||
| 'html', | |||||
| { | |||||
| fillColor: false, | |||||
| normalRangeMin: -1, | |||||
| resize: true, | |||||
| normalRangeMax: 8, | |||||
| height: '20', | |||||
| } | |||||
| ); | |||||
| $('.sparktristate').sparkline( | |||||
| 'html', | |||||
| { | |||||
| type: 'tristate', | |||||
| resize: true, | |||||
| height: '20', | |||||
| } | |||||
| ); | |||||
| $('.sparktristatecols').sparkline( | |||||
| 'html', | |||||
| { | |||||
| type: 'tristate', | |||||
| colorMap: { | |||||
| '-2': '#fa7', | |||||
| resize: true, | |||||
| '2': '#44f', | |||||
| height: '20', | |||||
| }, | |||||
| } | |||||
| ); | |||||
| const values = [5, 4, 5, -2, 0, 3, -5, 6, 7, 9, 9, 5, -3, -2, 2, -4]; | |||||
| const valuesAlt = [1, 1, 0, 1, -1, -1, 1, -1, 0, 0, 1, 1]; | |||||
| $('.sparkline').sparkline(values, { | |||||
| type: 'line', | |||||
| barWidth: 4, | |||||
| barSpacing: 5, | |||||
| fillColor: '', | |||||
| lineColor: COLORS['red-500'], | |||||
| lineWidth: 2, | |||||
| spotRadius: 3, | |||||
| spotColor: COLORS['red-500'], | |||||
| maxSpotColor: COLORS['red-500'], | |||||
| minSpotColor: COLORS['red-500'], | |||||
| highlightSpotColor: COLORS['red-500'], | |||||
| highlightLineColor: '', | |||||
| tooltipSuffix: ' Bzzt', | |||||
| tooltipPrefix: 'Hello ', | |||||
| width: 100, | |||||
| height: undefined, | |||||
| barColor: '9f0', | |||||
| negBarColor: 'ff0', | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| }); | |||||
| const drawOtherSparklines = () => { | |||||
| const sparkColors = Theme.getSparklineColors(); | |||||
| // Line sparklines | |||||
| createSparklineChart('sparkline', [5, 6, 7, 9, 9, 5, 3, 2, 2, 4, 6, 7], COLORS['red-500'], 'line'); | |||||
| // Composite bar - simplified implementation | |||||
| createSparklineChart('compositebar', [4, 1, 5, 7, 9, 9, 8, 7, 6, 6, 4, 7, 8, 4, 3, 2, 2, 5, 6, 7], sparkColors.light); | |||||
| // Normal line | |||||
| createSparklineChart('normalline', [5, 6, 7, 9, 9, 5, 3, 2, 2, 4, 6, 7], sparkColors.info, 'line'); | |||||
| // Various sparkline types for elements with classes | |||||
| const values = [5, 4, 5, -2, 0, 3, -5, 6, 7, 9, 9, 5, -3, -2, 2, -4]; | |||||
| const valuesAlt = [1, 1, 0, 1, -1, -1, 1, -1, 0, 0, 1, 1]; | |||||
| $('.sparkbar').sparkline(values, { | |||||
| type: 'bar', | |||||
| barWidth: 4, | |||||
| barSpacing: 1, | |||||
| fillColor: '', | |||||
| lineColor: COLORS['deep-purple-500'], | |||||
| tooltipSuffix: 'Celsius', | |||||
| width: 100, | |||||
| barColor: '39f', | |||||
| negBarColor: COLORS['deep-purple-500'], | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| }); | |||||
| // Line sparklines | |||||
| createSparklineForElements('.sparkline', values, COLORS['red-500'], 'line'); | |||||
| // Bar sparklines | |||||
| createSparklineForElements('.sparkbar', values, COLORS['deep-purple-500'], 'bar'); | |||||
| // Tristate sparklines (simplified as bar charts) | |||||
| createSparklineForElements('.sparktri', valuesAlt, COLORS['light-blue-500'], 'bar'); | |||||
| createSparklineForElements('.sparktristate', valuesAlt, sparkColors.info, 'bar'); | |||||
| createSparklineForElements('.sparktristatecols', valuesAlt, '#fa7', 'bar'); | |||||
| // Discrete sparklines (as line charts) | |||||
| createSparklineForElements('.sparkdisc', values, '#9f0', 'line'); | |||||
| // Bullet sparklines (simplified as bar charts) | |||||
| createSparklineForElements('.sparkbull', values, COLORS['amber-500'], 'bar'); | |||||
| // Box sparklines (simplified as bar charts) | |||||
| createSparklineForElements('.sparkbox', values, '#9f0', 'bar'); | |||||
| }; | |||||
| $('.sparktri').sparkline(valuesAlt, { | |||||
| type: 'tristate', | |||||
| barWidth: 4, | |||||
| barSpacing: 1, | |||||
| fillColor: '', | |||||
| lineColor: COLORS['light-blue-500'], | |||||
| tooltipSuffix: 'Celsius', | |||||
| width: 100, | |||||
| barColor: COLORS['light-blue-500'], | |||||
| posBarColor: COLORS['light-blue-500'], | |||||
| negBarColor: 'f90', | |||||
| zeroBarColor: '000', | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| }); | |||||
| // ------------------------------------------------------ | |||||
| // @Initialization | |||||
| // ------------------------------------------------------ | |||||
| $('.sparkdisc').sparkline(values, { | |||||
| type: 'discrete', | |||||
| barWidth: 4, | |||||
| barSpacing: 5, | |||||
| fillColor: '', | |||||
| lineColor: '9f0', | |||||
| tooltipSuffix: 'Celsius', | |||||
| width: 100, | |||||
| barColor: '9f0', | |||||
| negBarColor: 'f90', | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| }); | |||||
| const initializeSparklines = () => { | |||||
| drawSparklines(); | |||||
| drawOtherSparklines(); | |||||
| }; | |||||
| $('.sparkbull').sparkline(values, { | |||||
| type: 'bullet', | |||||
| barWidth: 4, | |||||
| barSpacing: 5, | |||||
| fillColor: '', | |||||
| lineColor: COLORS['amber-500'], | |||||
| tooltipSuffix: 'Celsius', | |||||
| height: 'auto', | |||||
| width: 'auto', | |||||
| targetWidth: 'auto', | |||||
| barColor: COLORS['amber-500'], | |||||
| negBarColor: 'ff0', | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| }); | |||||
| // Initial draw | |||||
| initializeSparklines(); | |||||
| $('.sparkbox').sparkline(values, { | |||||
| type: 'box', | |||||
| barWidth: 4, | |||||
| barSpacing: 5, | |||||
| fillColor: '', | |||||
| lineColor: '9f0', | |||||
| tooltipSuffix: 'Celsius', | |||||
| width: 100, | |||||
| barColor: '9f0', | |||||
| negBarColor: 'ff0', | |||||
| stackedBarColor: ['ff0', '9f0', '999', 'f60'], | |||||
| sliceColors: ['ff0', '9f0', '000', 'f60'], | |||||
| offset: '30', | |||||
| borderWidth: 1, | |||||
| borderColor: '000', | |||||
| // Redraw sparklines on window resize | |||||
| window.addEventListener('resize', debounce(initializeSparklines, 150)); | |||||
| // Listen for theme changes | |||||
| window.addEventListener('adminator:themeChanged', debounce(initializeSparklines, 150)); | |||||
| // Cleanup function for chart instances | |||||
| window.addEventListener('beforeunload', () => { | |||||
| chartInstances.forEach(chart => { | |||||
| if (chart && typeof chart.destroy === 'function') { | |||||
| chart.destroy(); | |||||
| } | |||||
| }); | |||||
| chartInstances = []; | |||||
| }); | }); | ||||
| }()) | |||||
| // Export for external access | |||||
| return { | |||||
| redraw: initializeSparklines, | |||||
| destroy: () => { | |||||
| chartInstances.forEach(chart => { | |||||
| if (chart && typeof chart.destroy === 'function') { | |||||
| chart.destroy(); | |||||
| } | |||||
| }); | |||||
| chartInstances = []; | |||||
| }, | |||||
| }; | |||||
| }()); | |||||
| @ -1,8 +1,11 @@ | |||||
| import * as $ from 'jquery'; | |||||
| export default (function () { | export default (function () { | ||||
| $('#chat-sidebar-toggle').on('click', e => { | |||||
| $('#chat-sidebar').toggleClass('open'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| const chatSidebarToggle = document.getElementById('chat-sidebar-toggle'); | |||||
| const chatSidebar = document.getElementById('chat-sidebar'); | |||||
| if (chatSidebarToggle && chatSidebar) { | |||||
| chatSidebarToggle.addEventListener('click', e => { | |||||
| chatSidebar.classList.toggle('open'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| } | |||||
| }()) | }()) | ||||
| @ -1,6 +1,379 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import 'datatables'; | |||||
| import Theme from '../utils/theme.js'; | |||||
| export default (function () { | export default (function () { | ||||
| $('#dataTable').DataTable(); | |||||
| }()); | |||||
| // Vanilla JS DataTable implementation | |||||
| class VanillaDataTable { | |||||
| constructor(element, options = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| sortable: true, | |||||
| searchable: true, | |||||
| pagination: true, | |||||
| pageSize: 10, | |||||
| ...options, | |||||
| }; | |||||
| this.originalData = []; | |||||
| this.filteredData = []; | |||||
| this.currentPage = 1; | |||||
| this.sortColumn = null; | |||||
| this.sortDirection = 'asc'; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.extractData(); | |||||
| this.createControls(); | |||||
| this.applyStyles(); | |||||
| this.bindEvents(); | |||||
| this.render(); | |||||
| } | |||||
| extractData() { | |||||
| const rows = this.element.querySelectorAll('tbody tr'); | |||||
| this.originalData = Array.from(rows).map(row => { | |||||
| const cells = row.querySelectorAll('td'); | |||||
| return Array.from(cells).map(cell => cell.textContent.trim()); | |||||
| }); | |||||
| this.filteredData = [...this.originalData]; | |||||
| } | |||||
| createControls() { | |||||
| const wrapper = document.createElement('div'); | |||||
| wrapper.className = 'datatable-wrapper'; | |||||
| // Create search input | |||||
| if (this.options.searchable) { | |||||
| const searchWrapper = document.createElement('div'); | |||||
| searchWrapper.className = 'datatable-search'; | |||||
| searchWrapper.innerHTML = ` | |||||
| <label> | |||||
| Search: | |||||
| <input type="text" class="form-control" placeholder="Search..."> | |||||
| </label> | |||||
| `; | |||||
| wrapper.appendChild(searchWrapper); | |||||
| } | |||||
| // Create pagination info | |||||
| if (this.options.pagination) { | |||||
| const infoWrapper = document.createElement('div'); | |||||
| infoWrapper.className = 'datatable-info'; | |||||
| wrapper.appendChild(infoWrapper); | |||||
| } | |||||
| // Wrap the table | |||||
| this.element.parentNode.insertBefore(wrapper, this.element); | |||||
| wrapper.appendChild(this.element); | |||||
| // Create pagination controls | |||||
| if (this.options.pagination) { | |||||
| const paginationWrapper = document.createElement('div'); | |||||
| paginationWrapper.className = 'datatable-pagination'; | |||||
| wrapper.appendChild(paginationWrapper); | |||||
| } | |||||
| this.wrapper = wrapper; | |||||
| } | |||||
| applyStyles() { | |||||
| // Apply Bootstrap-like styles | |||||
| this.element.classList.add('table', 'table-striped', 'table-bordered'); | |||||
| // Add custom styles | |||||
| const style = document.createElement('style'); | |||||
| style.textContent = ` | |||||
| .datatable-wrapper { | |||||
| margin: 20px 0; | |||||
| } | |||||
| .datatable-search { | |||||
| margin-bottom: 15px; | |||||
| } | |||||
| .datatable-search input { | |||||
| width: 250px; | |||||
| display: inline-block; | |||||
| margin-left: 5px; | |||||
| } | |||||
| .datatable-info { | |||||
| margin-top: 15px; | |||||
| color: var(--c-text-muted, #6c757d); | |||||
| font-size: 14px; | |||||
| } | |||||
| .datatable-pagination { | |||||
| margin-top: 15px; | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| } | |||||
| .datatable-pagination button { | |||||
| background: var(--c-bkg-card, #fff); | |||||
| border: 1px solid var(--c-border, #dee2e6); | |||||
| color: var(--c-text-base, #333); | |||||
| padding: 6px 12px; | |||||
| margin: 0 2px; | |||||
| cursor: pointer; | |||||
| border-radius: 4px; | |||||
| } | |||||
| .datatable-pagination button:hover { | |||||
| background: var(--c-primary, #007bff); | |||||
| color: white; | |||||
| } | |||||
| .datatable-pagination button.active { | |||||
| background: var(--c-primary, #007bff); | |||||
| color: white; | |||||
| } | |||||
| .datatable-pagination button:disabled { | |||||
| opacity: 0.6; | |||||
| cursor: not-allowed; | |||||
| } | |||||
| .datatable-sort { | |||||
| cursor: pointer; | |||||
| user-select: none; | |||||
| position: relative; | |||||
| } | |||||
| .datatable-sort:hover { | |||||
| background: var(--c-bkg-card, #f8f9fa); | |||||
| } | |||||
| .datatable-sort::after { | |||||
| content: '↕'; | |||||
| position: absolute; | |||||
| right: 8px; | |||||
| opacity: 0.5; | |||||
| } | |||||
| .datatable-sort.asc::after { | |||||
| content: '↑'; | |||||
| opacity: 1; | |||||
| } | |||||
| .datatable-sort.desc::after { | |||||
| content: '↓'; | |||||
| opacity: 1; | |||||
| } | |||||
| `; | |||||
| document.head.appendChild(style); | |||||
| } | |||||
| bindEvents() { | |||||
| // Search functionality | |||||
| if (this.options.searchable) { | |||||
| const searchInput = this.wrapper.querySelector('.datatable-search input'); | |||||
| searchInput.addEventListener('input', (e) => { | |||||
| this.search(e.target.value); | |||||
| }); | |||||
| } | |||||
| // Sorting functionality | |||||
| if (this.options.sortable) { | |||||
| const headers = this.element.querySelectorAll('thead th'); | |||||
| headers.forEach((header, index) => { | |||||
| header.classList.add('datatable-sort'); | |||||
| header.addEventListener('click', () => { | |||||
| this.sort(index); | |||||
| }); | |||||
| }); | |||||
| } | |||||
| } | |||||
| search(query) { | |||||
| if (!query) { | |||||
| this.filteredData = [...this.originalData]; | |||||
| } else { | |||||
| this.filteredData = this.originalData.filter(row => | |||||
| row.some(cell => | |||||
| cell.toLowerCase().includes(query.toLowerCase()) | |||||
| ) | |||||
| ); | |||||
| } | |||||
| this.currentPage = 1; | |||||
| this.render(); | |||||
| } | |||||
| sort(columnIndex) { | |||||
| if (this.sortColumn === columnIndex) { | |||||
| this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; | |||||
| } else { | |||||
| this.sortColumn = columnIndex; | |||||
| this.sortDirection = 'asc'; | |||||
| } | |||||
| this.filteredData.sort((a, b) => { | |||||
| const aVal = a[columnIndex]; | |||||
| const bVal = b[columnIndex]; | |||||
| // Try to parse as numbers | |||||
| const aNum = parseFloat(aVal); | |||||
| const bNum = parseFloat(bVal); | |||||
| let comparison = 0; | |||||
| if (!isNaN(aNum) && !isNaN(bNum)) { | |||||
| comparison = aNum - bNum; | |||||
| } else { | |||||
| comparison = aVal.localeCompare(bVal); | |||||
| } | |||||
| return this.sortDirection === 'asc' ? comparison : -comparison; | |||||
| }); | |||||
| this.updateSortHeaders(); | |||||
| this.render(); | |||||
| } | |||||
| updateSortHeaders() { | |||||
| const headers = this.element.querySelectorAll('thead th'); | |||||
| headers.forEach((header, index) => { | |||||
| header.classList.remove('asc', 'desc'); | |||||
| if (index === this.sortColumn) { | |||||
| header.classList.add(this.sortDirection); | |||||
| } | |||||
| }); | |||||
| } | |||||
| render() { | |||||
| const tbody = this.element.querySelector('tbody'); | |||||
| const startIndex = (this.currentPage - 1) * this.options.pageSize; | |||||
| const endIndex = startIndex + this.options.pageSize; | |||||
| const pageData = this.filteredData.slice(startIndex, endIndex); | |||||
| // Clear tbody | |||||
| tbody.innerHTML = ''; | |||||
| // Add rows | |||||
| pageData.forEach(rowData => { | |||||
| const row = document.createElement('tr'); | |||||
| rowData.forEach(cellData => { | |||||
| const cell = document.createElement('td'); | |||||
| cell.textContent = cellData; | |||||
| row.appendChild(cell); | |||||
| }); | |||||
| tbody.appendChild(row); | |||||
| }); | |||||
| // Update pagination | |||||
| if (this.options.pagination) { | |||||
| this.updatePagination(); | |||||
| } | |||||
| // Update info | |||||
| this.updateInfo(); | |||||
| } | |||||
| updatePagination() { | |||||
| const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize); | |||||
| const paginationWrapper = this.wrapper.querySelector('.datatable-pagination'); | |||||
| paginationWrapper.innerHTML = ''; | |||||
| if (totalPages <= 1) return; | |||||
| // Previous button | |||||
| const prevBtn = document.createElement('button'); | |||||
| prevBtn.textContent = 'Previous'; | |||||
| prevBtn.disabled = this.currentPage === 1; | |||||
| prevBtn.addEventListener('click', () => { | |||||
| if (this.currentPage > 1) { | |||||
| this.currentPage--; | |||||
| this.render(); | |||||
| } | |||||
| }); | |||||
| paginationWrapper.appendChild(prevBtn); | |||||
| // Page numbers | |||||
| for (let i = 1; i <= totalPages; i++) { | |||||
| const pageBtn = document.createElement('button'); | |||||
| pageBtn.textContent = i; | |||||
| pageBtn.classList.toggle('active', i === this.currentPage); | |||||
| pageBtn.addEventListener('click', () => { | |||||
| this.currentPage = i; | |||||
| this.render(); | |||||
| }); | |||||
| paginationWrapper.appendChild(pageBtn); | |||||
| } | |||||
| // Next button | |||||
| const nextBtn = document.createElement('button'); | |||||
| nextBtn.textContent = 'Next'; | |||||
| nextBtn.disabled = this.currentPage === totalPages; | |||||
| nextBtn.addEventListener('click', () => { | |||||
| if (this.currentPage < totalPages) { | |||||
| this.currentPage++; | |||||
| this.render(); | |||||
| } | |||||
| }); | |||||
| paginationWrapper.appendChild(nextBtn); | |||||
| } | |||||
| updateInfo() { | |||||
| const infoWrapper = this.wrapper.querySelector('.datatable-info'); | |||||
| if (!infoWrapper) return; | |||||
| const startIndex = (this.currentPage - 1) * this.options.pageSize + 1; | |||||
| const endIndex = Math.min(startIndex + this.options.pageSize - 1, this.filteredData.length); | |||||
| const total = this.filteredData.length; | |||||
| infoWrapper.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries`; | |||||
| } | |||||
| destroy() { | |||||
| if (this.wrapper && this.wrapper.parentNode) { | |||||
| this.wrapper.parentNode.replaceChild(this.element, this.wrapper); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Initialize DataTable | |||||
| const initializeDataTable = () => { | |||||
| const tableElement = document.getElementById('dataTable'); | |||||
| if (tableElement) { | |||||
| // Clean up existing instance | |||||
| if (tableElement.dataTableInstance) { | |||||
| tableElement.dataTableInstance.destroy(); | |||||
| } | |||||
| // Create new instance | |||||
| const dataTable = new VanillaDataTable(tableElement, { | |||||
| sortable: true, | |||||
| searchable: true, | |||||
| pagination: true, | |||||
| pageSize: 10, | |||||
| }); | |||||
| // Store instance for cleanup | |||||
| tableElement.dataTableInstance = dataTable; | |||||
| } | |||||
| }; | |||||
| // Initialize on load | |||||
| initializeDataTable(); | |||||
| // Reinitialize on theme change | |||||
| window.addEventListener('adminator:themeChanged', () => { | |||||
| setTimeout(initializeDataTable, 100); | |||||
| }); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| const tableElement = document.getElementById('dataTable'); | |||||
| if (tableElement && tableElement.dataTableInstance) { | |||||
| tableElement.dataTableInstance.destroy(); | |||||
| } | |||||
| }); | |||||
| // Return public API | |||||
| return { | |||||
| init: initializeDataTable, | |||||
| VanillaDataTable, | |||||
| }; | |||||
| }()); | |||||
| @ -1,8 +1,303 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import 'bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js'; | |||||
| import 'bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css'; | |||||
| import DateUtils from '../utils/date.js'; | |||||
| import Theme from '../utils/theme.js'; | |||||
| export default (function () { | export default (function () { | ||||
| $('.start-date').datepicker(); | |||||
| $('.end-date').datepicker(); | |||||
| }()) | |||||
| // Enhanced HTML5 date picker with vanilla JS | |||||
| class VanillaDatePicker { | |||||
| constructor(element, options = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.convertToHTML5(); | |||||
| this.enhanceInput(); | |||||
| this.applyStyles(); | |||||
| this.bindEvents(); | |||||
| } | |||||
| convertToHTML5() { | |||||
| // Convert input to HTML5 date type | |||||
| this.element.type = 'date'; | |||||
| this.element.classList.add('form-control', 'vanilla-datepicker'); | |||||
| // Remove placeholder since HTML5 date inputs don't need it | |||||
| this.element.removeAttribute('placeholder'); | |||||
| // Set default value to today if no value is set | |||||
| if (!this.element.value) { | |||||
| this.element.value = DateUtils.form.toInputValue(DateUtils.now()); | |||||
| } | |||||
| // Ensure proper styling | |||||
| this.element.style.minHeight = '38px'; | |||||
| this.element.style.lineHeight = '1.5'; | |||||
| this.element.style.cursor = 'pointer'; | |||||
| } | |||||
| enhanceInput() { | |||||
| // Create wrapper for enhanced functionality | |||||
| const wrapper = document.createElement('div'); | |||||
| wrapper.className = 'vanilla-datepicker-wrapper'; | |||||
| wrapper.style.position = 'relative'; | |||||
| // Wrap the input | |||||
| this.element.parentNode.insertBefore(wrapper, this.element); | |||||
| wrapper.appendChild(this.element); | |||||
| // Add calendar icon if input is in an input group | |||||
| const inputGroup = this.element.closest('.input-group'); | |||||
| if (inputGroup) { | |||||
| const calendarIcon = inputGroup.querySelector('.input-group-text i.ti-calendar'); | |||||
| if (calendarIcon) { | |||||
| calendarIcon.addEventListener('click', (e) => { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| this.openPicker(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| this.wrapper = wrapper; | |||||
| } | |||||
| applyStyles() { | |||||
| // Add custom styles for enhanced appearance | |||||
| const style = document.createElement('style'); | |||||
| style.textContent = ` | |||||
| .vanilla-datepicker-wrapper { | |||||
| position: relative; | |||||
| } | |||||
| .vanilla-datepicker { | |||||
| width: 100%; | |||||
| padding: 6px 12px; | |||||
| border: 1px solid var(--c-border, #ced4da); | |||||
| border-radius: 4px; | |||||
| background-color: var(--c-bkg-card, #fff); | |||||
| color: var(--c-text-base, #495057); | |||||
| font-size: 14px; | |||||
| transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | |||||
| } | |||||
| .vanilla-datepicker:focus { | |||||
| outline: none; | |||||
| border-color: var(--c-primary, #007bff); | |||||
| box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | |||||
| } | |||||
| .vanilla-datepicker::-webkit-calendar-picker-indicator { | |||||
| cursor: pointer; | |||||
| border-radius: 4px; | |||||
| margin-right: 2px; | |||||
| opacity: 0.6; | |||||
| transition: opacity 0.15s ease-in-out; | |||||
| } | |||||
| .vanilla-datepicker::-webkit-calendar-picker-indicator:hover { | |||||
| opacity: 1; | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-fields-wrapper { | |||||
| padding: 0; | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-month-field, | |||||
| .vanilla-datepicker::-webkit-datetime-edit-day-field, | |||||
| .vanilla-datepicker::-webkit-datetime-edit-year-field { | |||||
| color: var(--c-text-base, #495057); | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-text { | |||||
| color: var(--c-text-muted, #6c757d); | |||||
| } | |||||
| /* Dark mode support */ | |||||
| [data-theme="dark"] .vanilla-datepicker { | |||||
| background-color: var(--c-bkg-card, #1f2937); | |||||
| color: var(--c-text-base, #f9fafb); | |||||
| border-color: var(--c-border, #374151); | |||||
| } | |||||
| [data-theme="dark"] .vanilla-datepicker::-webkit-calendar-picker-indicator { | |||||
| filter: invert(1); | |||||
| } | |||||
| .datepicker-today-indicator { | |||||
| position: absolute; | |||||
| top: 2px; | |||||
| right: 8px; | |||||
| width: 6px; | |||||
| height: 6px; | |||||
| background-color: var(--c-primary, #007bff); | |||||
| border-radius: 50%; | |||||
| opacity: 0.8; | |||||
| pointer-events: none; | |||||
| } | |||||
| .datepicker-animation { | |||||
| animation: datepicker-highlight 0.3s ease-in-out; | |||||
| } | |||||
| @keyframes datepicker-highlight { | |||||
| 0% { transform: scale(1); } | |||||
| 50% { transform: scale(1.02); } | |||||
| 100% { transform: scale(1); } | |||||
| } | |||||
| `; | |||||
| // Only add the style if it doesn't exist | |||||
| if (!document.querySelector('style[data-vanilla-datepicker-styles]')) { | |||||
| style.setAttribute('data-vanilla-datepicker-styles', 'true'); | |||||
| document.head.appendChild(style); | |||||
| } | |||||
| } | |||||
| bindEvents() { | |||||
| // Handle click events | |||||
| this.element.addEventListener('click', (e) => { | |||||
| this.openPicker(); | |||||
| }); | |||||
| // Handle keyboard events | |||||
| this.element.addEventListener('keydown', (e) => { | |||||
| if (e.key === 'Enter' || e.key === ' ') { | |||||
| this.openPicker(); | |||||
| } | |||||
| }); | |||||
| // Handle change events | |||||
| this.element.addEventListener('change', (e) => { | |||||
| this.handleDateChange(e); | |||||
| }); | |||||
| // Handle focus events | |||||
| this.element.addEventListener('focus', (e) => { | |||||
| this.element.classList.add('datepicker-animation'); | |||||
| setTimeout(() => { | |||||
| this.element.classList.remove('datepicker-animation'); | |||||
| }, 300); | |||||
| }); | |||||
| } | |||||
| openPicker() { | |||||
| this.element.focus(); | |||||
| // Try to open the native date picker | |||||
| if (this.element.showPicker && typeof this.element.showPicker === 'function') { | |||||
| try { | |||||
| this.element.showPicker(); | |||||
| } catch (e) { | |||||
| // Fallback for browsers that don't support showPicker | |||||
| } | |||||
| } | |||||
| } | |||||
| handleDateChange(e) { | |||||
| const selectedDate = e.target.value; | |||||
| if (selectedDate) { | |||||
| // Add visual feedback | |||||
| this.element.classList.add('datepicker-animation'); | |||||
| setTimeout(() => { | |||||
| this.element.classList.remove('datepicker-animation'); | |||||
| }, 300); | |||||
| // Trigger custom event | |||||
| const changeEvent = new CustomEvent('datepicker:change', { | |||||
| detail: { | |||||
| date: selectedDate, | |||||
| formattedDate: this.formatDate(selectedDate), | |||||
| }, | |||||
| }); | |||||
| this.element.dispatchEvent(changeEvent); | |||||
| } | |||||
| } | |||||
| formatDate(dateString) { | |||||
| const date = new Date(dateString); | |||||
| return DateUtils.format(date, this.options.format); | |||||
| } | |||||
| setDate(dateString) { | |||||
| this.element.value = dateString; | |||||
| this.handleDateChange({ target: this.element }); | |||||
| } | |||||
| getDate() { | |||||
| return this.element.value; | |||||
| } | |||||
| destroy() { | |||||
| if (this.wrapper && this.wrapper.parentNode) { | |||||
| this.wrapper.parentNode.replaceChild(this.element, this.wrapper); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Initialize date pickers | |||||
| const initializeDatePickers = () => { | |||||
| // Start date pickers | |||||
| const startDateElements = document.querySelectorAll('.start-date'); | |||||
| startDateElements.forEach(element => { | |||||
| if (element.vanillaDatePicker) { | |||||
| element.vanillaDatePicker.destroy(); | |||||
| } | |||||
| const datePicker = new VanillaDatePicker(element, { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| }); | |||||
| element.vanillaDatePicker = datePicker; | |||||
| }); | |||||
| // End date pickers | |||||
| const endDateElements = document.querySelectorAll('.end-date'); | |||||
| endDateElements.forEach(element => { | |||||
| if (element.vanillaDatePicker) { | |||||
| element.vanillaDatePicker.destroy(); | |||||
| } | |||||
| const datePicker = new VanillaDatePicker(element, { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| }); | |||||
| element.vanillaDatePicker = datePicker; | |||||
| }); | |||||
| }; | |||||
| // Initialize on load | |||||
| initializeDatePickers(); | |||||
| // Reinitialize on theme change | |||||
| window.addEventListener('adminator:themeChanged', () => { | |||||
| setTimeout(initializeDatePickers, 100); | |||||
| }); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| document.querySelectorAll('.start-date, .end-date').forEach(element => { | |||||
| if (element.vanillaDatePicker) { | |||||
| element.vanillaDatePicker.destroy(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| // Return public API | |||||
| return { | |||||
| init: initializeDatePickers, | |||||
| VanillaDatePicker, | |||||
| }; | |||||
| }()); | |||||
| @ -1,13 +1,25 @@ | |||||
| import * as $ from 'jquery'; | |||||
| export default (function () { | export default (function () { | ||||
| $('.email-side-toggle').on('click', e => { | |||||
| $('.email-app').toggleClass('side-active'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| // Email side toggle functionality | |||||
| const emailSideToggle = document.querySelector('.email-side-toggle'); | |||||
| const emailApp = document.querySelector('.email-app'); | |||||
| if (emailSideToggle && emailApp) { | |||||
| emailSideToggle.addEventListener('click', e => { | |||||
| emailApp.classList.toggle('side-active'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| } | |||||
| $('.email-list-item, .back-to-mailbox').on('click', e => { | |||||
| $('.email-content').toggleClass('open'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| // Email list item and back to mailbox functionality | |||||
| const emailListItems = document.querySelectorAll('.email-list-item, .back-to-mailbox'); | |||||
| const emailContent = document.querySelector('.email-content'); | |||||
| if (emailListItems.length > 0 && emailContent) { | |||||
| emailListItems.forEach(item => { | |||||
| item.addEventListener('click', e => { | |||||
| emailContent.classList.toggle('open'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| }); | |||||
| } | |||||
| }()) | }()) | ||||
| @ -1,22 +1,109 @@ | |||||
| // import * as $ from 'jquery'; | |||||
| import * as bootstrap from 'bootstrap' | |||||
| // Simple vanilla JS tooltip and popover implementation | |||||
| export default (function () { | export default (function () { | ||||
| // ------------------------------------------------------ | |||||
| // @Popover | |||||
| // ------------------------------------------------------ | |||||
| var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) | |||||
| var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { | |||||
| return new bootstrap.Popover(popoverTriggerEl) | |||||
| }) | |||||
| // ------------------------------------------------------ | |||||
| // @Tooltips | |||||
| // ------------------------------------------------------ | |||||
| var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) | |||||
| var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { | |||||
| return new bootstrap.Tooltip(tooltipTriggerEl) | |||||
| }) | |||||
| // Simple tooltip implementation | |||||
| function initTooltips() { | |||||
| const tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]'); | |||||
| tooltipElements.forEach(element => { | |||||
| const tooltipText = element.getAttribute('data-bs-title') || element.getAttribute('title'); | |||||
| if (tooltipText) { | |||||
| element.addEventListener('mouseenter', function() { | |||||
| const tooltip = document.createElement('div'); | |||||
| tooltip.className = 'custom-tooltip'; | |||||
| tooltip.textContent = tooltipText; | |||||
| tooltip.style.cssText = ` | |||||
| position: absolute; | |||||
| background: #000; | |||||
| color: #fff; | |||||
| padding: 4px 8px; | |||||
| border-radius: 4px; | |||||
| font-size: 12px; | |||||
| z-index: 1050; | |||||
| pointer-events: none; | |||||
| white-space: nowrap; | |||||
| `; | |||||
| document.body.appendChild(tooltip); | |||||
| const rect = element.getBoundingClientRect(); | |||||
| tooltip.style.left = `${rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) }px`; | |||||
| tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5 }px`; | |||||
| element._tooltip = tooltip; | |||||
| }); | |||||
| element.addEventListener('mouseleave', function() { | |||||
| if (element._tooltip) { | |||||
| element._tooltip.remove(); | |||||
| element._tooltip = null; | |||||
| } | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| // Simple popover implementation | |||||
| function initPopovers() { | |||||
| const popoverElements = document.querySelectorAll('[data-bs-toggle="popover"]'); | |||||
| popoverElements.forEach(element => { | |||||
| const popoverContent = element.getAttribute('data-bs-content'); | |||||
| const popoverTitle = element.getAttribute('data-bs-title'); | |||||
| if (popoverContent) { | |||||
| element.addEventListener('click', function(e) { | |||||
| e.preventDefault(); | |||||
| // Remove existing popover | |||||
| if (element._popover) { | |||||
| element._popover.remove(); | |||||
| element._popover = null; | |||||
| return; | |||||
| } | |||||
| const popover = document.createElement('div'); | |||||
| popover.className = 'custom-popover'; | |||||
| popover.innerHTML = ` | |||||
| ${popoverTitle ? `<div class="popover-title">${popoverTitle}</div>` : ''} | |||||
| <div class="popover-content">${popoverContent}</div> | |||||
| `; | |||||
| popover.style.cssText = ` | |||||
| position: absolute; | |||||
| background: #fff; | |||||
| border: 1px solid #ccc; | |||||
| border-radius: 6px; | |||||
| box-shadow: 0 2px 8px rgba(0,0,0,0.15); | |||||
| z-index: 1050; | |||||
| min-width: 200px; | |||||
| max-width: 300px; | |||||
| `; | |||||
| document.body.appendChild(popover); | |||||
| const rect = element.getBoundingClientRect(); | |||||
| popover.style.left = `${rect.left }px`; | |||||
| popover.style.top = `${rect.bottom + 5 }px`; | |||||
| element._popover = popover; | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| // Initialize both | |||||
| initTooltips(); | |||||
| initPopovers(); | |||||
| // Close popovers when clicking outside | |||||
| document.addEventListener('click', function(e) { | |||||
| const popovers = document.querySelectorAll('.custom-popover'); | |||||
| popovers.forEach(popover => { | |||||
| if (!popover.contains(e.target)) { | |||||
| popover.remove(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| }()); | }()); | ||||
| @ -1,10 +1,9 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import PerfectScrollbar from 'perfect-scrollbar'; | import PerfectScrollbar from 'perfect-scrollbar'; | ||||
| export default (function () { | export default (function () { | ||||
| const scrollables = $('.scrollable'); | |||||
| const scrollables = document.querySelectorAll('.scrollable'); | |||||
| if (scrollables.length > 0) { | if (scrollables.length > 0) { | ||||
| scrollables.each((index, el) => { | |||||
| scrollables.forEach(el => { | |||||
| new PerfectScrollbar(el); | new PerfectScrollbar(el); | ||||
| }); | }); | ||||
| } | } | ||||
| @ -1,9 +1,15 @@ | |||||
| import * as $ from 'jquery'; | |||||
| export default (function () { | export default (function () { | ||||
| $('.search-toggle').on('click', e => { | |||||
| $('.search-box, .search-input').toggleClass('active'); | |||||
| $('.search-input input').focus(); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| const searchToggle = document.querySelector('.search-toggle'); | |||||
| const searchBox = document.querySelector('.search-box'); | |||||
| const searchInput = document.querySelector('.search-input'); | |||||
| const searchInputField = document.querySelector('.search-input input'); | |||||
| if (searchToggle && searchBox && searchInput && searchInputField) { | |||||
| searchToggle.addEventListener('click', e => { | |||||
| searchBox.classList.toggle('active'); | |||||
| searchInput.classList.toggle('active'); | |||||
| searchInputField.focus(); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| } | |||||
| }()); | }()); | ||||
| @ -1,76 +1,140 @@ | |||||
| import * as $ from 'jquery'; | |||||
| // Vanilla JS slide animations | |||||
| function slideUp(element, duration = 200, callback = null) { | |||||
| element.style.height = `${element.scrollHeight }px`; | |||||
| element.style.transition = `height ${duration}ms ease`; | |||||
| element.style.overflow = 'hidden'; | |||||
| requestAnimationFrame(() => { | |||||
| element.style.height = '0'; | |||||
| element.style.paddingTop = '0'; | |||||
| element.style.paddingBottom = '0'; | |||||
| element.style.marginTop = '0'; | |||||
| element.style.marginBottom = '0'; | |||||
| }); | |||||
| setTimeout(() => { | |||||
| element.style.display = 'none'; | |||||
| element.style.removeProperty('height'); | |||||
| element.style.removeProperty('padding-top'); | |||||
| element.style.removeProperty('padding-bottom'); | |||||
| element.style.removeProperty('margin-top'); | |||||
| element.style.removeProperty('margin-bottom'); | |||||
| element.style.removeProperty('overflow'); | |||||
| element.style.removeProperty('transition'); | |||||
| if (callback) callback(); | |||||
| }, duration); | |||||
| } | |||||
| function slideDown(element, duration = 200, callback = null) { | |||||
| element.style.removeProperty('display'); | |||||
| let display = window.getComputedStyle(element).display; | |||||
| if (display === 'none') display = 'block'; | |||||
| element.style.display = display; | |||||
| element.style.height = '0'; | |||||
| element.style.paddingTop = '0'; | |||||
| element.style.paddingBottom = '0'; | |||||
| element.style.marginTop = '0'; | |||||
| element.style.marginBottom = '0'; | |||||
| element.style.overflow = 'hidden'; | |||||
| const height = element.scrollHeight; | |||||
| element.style.transition = `height ${duration}ms ease`; | |||||
| requestAnimationFrame(() => { | |||||
| element.style.height = `${height }px`; | |||||
| element.style.removeProperty('padding-top'); | |||||
| element.style.removeProperty('padding-bottom'); | |||||
| element.style.removeProperty('margin-top'); | |||||
| element.style.removeProperty('margin-bottom'); | |||||
| }); | |||||
| setTimeout(() => { | |||||
| element.style.removeProperty('height'); | |||||
| element.style.removeProperty('overflow'); | |||||
| element.style.removeProperty('transition'); | |||||
| if (callback) callback(); | |||||
| }, duration); | |||||
| } | |||||
| export default (function () { | export default (function () { | ||||
| // Sidebar links | // Sidebar links | ||||
| $('.sidebar .sidebar-menu li a').on('click', function () { | |||||
| const $this = $(this); | |||||
| if ($this.parent().hasClass('open')) { | |||||
| $this | |||||
| .parent() | |||||
| .children('.dropdown-menu') | |||||
| .slideUp(200, () => { | |||||
| $this.parent().removeClass('open'); | |||||
| const sidebarLinks = document.querySelectorAll('.sidebar .sidebar-menu li a'); | |||||
| sidebarLinks.forEach(link => { | |||||
| link.addEventListener('click', function () { | |||||
| const parentLi = this.parentElement; | |||||
| const dropdownMenu = parentLi.querySelector('.dropdown-menu'); | |||||
| if (!dropdownMenu) return; | |||||
| if (parentLi.classList.contains('open')) { | |||||
| slideUp(dropdownMenu, 200, () => { | |||||
| parentLi.classList.remove('open'); | |||||
| }); | }); | ||||
| } else { | |||||
| $this | |||||
| .parent() | |||||
| .parent() | |||||
| .children('li.open') | |||||
| .children('.dropdown-menu') | |||||
| .slideUp(200); | |||||
| $this | |||||
| .parent() | |||||
| .parent() | |||||
| .children('li.open') | |||||
| .children('a') | |||||
| .removeClass('open'); | |||||
| $this | |||||
| .parent() | |||||
| .parent() | |||||
| .children('li.open') | |||||
| .removeClass('open'); | |||||
| $this | |||||
| .parent() | |||||
| .children('.dropdown-menu') | |||||
| .slideDown(200, () => { | |||||
| $this.parent().addClass('open'); | |||||
| } else { | |||||
| // Close all other open menus at the same level | |||||
| const siblingMenus = parentLi.parentElement.querySelectorAll('li.open'); | |||||
| siblingMenus.forEach(sibling => { | |||||
| const siblingDropdown = sibling.querySelector('.dropdown-menu'); | |||||
| const siblingLink = sibling.querySelector('a'); | |||||
| if (siblingDropdown) { | |||||
| slideUp(siblingDropdown, 200); | |||||
| } | |||||
| if (siblingLink) { | |||||
| siblingLink.classList.remove('open'); | |||||
| } | |||||
| sibling.classList.remove('open'); | |||||
| }); | }); | ||||
| } | |||||
| // Open current menu | |||||
| slideDown(dropdownMenu, 200, () => { | |||||
| parentLi.classList.add('open'); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| }); | }); | ||||
| // Sidebar Activity Class | // Sidebar Activity Class | ||||
| const sidebarLinks = $('.sidebar').find('.sidebar-link'); | |||||
| sidebarLinks | |||||
| .each((index, el) => { | |||||
| $(el).removeClass('active'); | |||||
| }) | |||||
| .filter(function () { | |||||
| const href = $(this).attr('href'); | |||||
| const sidebarLinkElements = document.querySelectorAll('.sidebar .sidebar-link'); | |||||
| sidebarLinkElements.forEach(link => { | |||||
| link.classList.remove('active'); | |||||
| const href = link.getAttribute('href'); | |||||
| if (href) { | |||||
| const pattern = href[0] === '/' ? href.substr(1) : href; | const pattern = href[0] === '/' ? href.substr(1) : href; | ||||
| return pattern === (window.location.pathname).substr(1); | |||||
| }) | |||||
| .addClass('active'); | |||||
| // ٍSidebar Toggle | |||||
| $('.sidebar-toggle').on('click', e => { | |||||
| $('.app').toggleClass('is-collapsed'); | |||||
| e.preventDefault(); | |||||
| if (pattern === window.location.pathname.substr(1)) { | |||||
| link.classList.add('active'); | |||||
| } | |||||
| } | |||||
| }); | }); | ||||
| // Sidebar Toggle | |||||
| const sidebarToggle = document.querySelector('.sidebar-toggle'); | |||||
| const app = document.querySelector('.app'); | |||||
| if (sidebarToggle && app) { | |||||
| sidebarToggle.addEventListener('click', e => { | |||||
| app.classList.toggle('is-collapsed'); | |||||
| e.preventDefault(); | |||||
| }); | |||||
| } | |||||
| /** | /** | ||||
| * Wait untill sidebar fully toggled (animated in/out) | |||||
| * Wait until sidebar fully toggled (animated in/out) | |||||
| * then trigger window resize event in order to recalculate | * then trigger window resize event in order to recalculate | ||||
| * masonry layout widths and gutters. | * masonry layout widths and gutters. | ||||
| */ | */ | ||||
| $('#sidebar-toggle').click(e => { | |||||
| e.preventDefault(); | |||||
| setTimeout(() => { | |||||
| window.dispatchEvent(window.EVENT); | |||||
| }, 300); | |||||
| }); | |||||
| const sidebarToggleById = document.getElementById('sidebar-toggle'); | |||||
| if (sidebarToggleById) { | |||||
| sidebarToggleById.addEventListener('click', e => { | |||||
| e.preventDefault(); | |||||
| setTimeout(() => { | |||||
| window.dispatchEvent(new Event('resize')); | |||||
| }, 300); | |||||
| }); | |||||
| } | |||||
| }()); | }()); | ||||
| @ -0,0 +1,412 @@ | |||||
| /** | |||||
| * UI Page Bootstrap Components | |||||
| * Vanilla JavaScript implementations for Bootstrap components | |||||
| */ | |||||
| export default (function () { | |||||
| // Modal functionality | |||||
| class VanillaModal { | |||||
| constructor(element) { | |||||
| this.element = element; | |||||
| this.modal = null; | |||||
| this.backdrop = null; | |||||
| this.isOpen = false; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.modal = document.querySelector(this.element.getAttribute('data-bs-target')); | |||||
| if (this.modal) { | |||||
| this.element.addEventListener('click', (e) => { | |||||
| e.preventDefault(); | |||||
| this.show(); | |||||
| }); | |||||
| // Close button functionality | |||||
| const closeButtons = this.modal.querySelectorAll('[data-bs-dismiss="modal"]'); | |||||
| closeButtons.forEach(btn => { | |||||
| btn.addEventListener('click', () => this.hide()); | |||||
| }); | |||||
| // Close on backdrop click | |||||
| this.modal.addEventListener('click', (e) => { | |||||
| if (e.target === this.modal) { | |||||
| this.hide(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| show() { | |||||
| if (this.isOpen) return; | |||||
| // Create backdrop | |||||
| this.backdrop = document.createElement('div'); | |||||
| this.backdrop.className = 'modal-backdrop fade show'; | |||||
| document.body.appendChild(this.backdrop); | |||||
| // Show modal | |||||
| this.modal.style.display = 'block'; | |||||
| this.modal.classList.add('show'); | |||||
| document.body.classList.add('modal-open'); | |||||
| this.isOpen = true; | |||||
| // Focus the modal | |||||
| this.modal.setAttribute('tabindex', '-1'); | |||||
| this.modal.focus(); | |||||
| // Escape key handler | |||||
| this.escapeHandler = (e) => { | |||||
| if (e.key === 'Escape') { | |||||
| this.hide(); | |||||
| } | |||||
| }; | |||||
| document.addEventListener('keydown', this.escapeHandler); | |||||
| } | |||||
| hide() { | |||||
| if (!this.isOpen) return; | |||||
| // Hide modal | |||||
| this.modal.classList.remove('show'); | |||||
| this.modal.style.display = 'none'; | |||||
| document.body.classList.remove('modal-open'); | |||||
| // Remove backdrop | |||||
| if (this.backdrop) { | |||||
| this.backdrop.remove(); | |||||
| this.backdrop = null; | |||||
| } | |||||
| this.isOpen = false; | |||||
| // Remove escape handler | |||||
| if (this.escapeHandler) { | |||||
| document.removeEventListener('keydown', this.escapeHandler); | |||||
| this.escapeHandler = null; | |||||
| } | |||||
| } | |||||
| } | |||||
| // Dropdown functionality | |||||
| class VanillaDropdown { | |||||
| constructor(element) { | |||||
| this.element = element; | |||||
| this.menu = null; | |||||
| this.isOpen = false; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.menu = this.element.parentNode.querySelector('.dropdown-menu'); | |||||
| if (this.menu) { | |||||
| this.element.addEventListener('click', (e) => { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| this.toggle(); | |||||
| }); | |||||
| // Close on outside click | |||||
| document.addEventListener('click', (e) => { | |||||
| if (!this.element.parentNode.contains(e.target)) { | |||||
| this.hide(); | |||||
| } | |||||
| }); | |||||
| // Close on escape | |||||
| document.addEventListener('keydown', (e) => { | |||||
| if (e.key === 'Escape' && this.isOpen) { | |||||
| this.hide(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| toggle() { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| show() { | |||||
| if (this.isOpen) return; | |||||
| // Close other dropdowns | |||||
| document.querySelectorAll('.dropdown-menu.show').forEach(menu => { | |||||
| menu.classList.remove('show'); | |||||
| }); | |||||
| this.menu.classList.add('show'); | |||||
| this.element.setAttribute('aria-expanded', 'true'); | |||||
| this.isOpen = true; | |||||
| } | |||||
| hide() { | |||||
| if (!this.isOpen) return; | |||||
| this.menu.classList.remove('show'); | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| this.isOpen = false; | |||||
| } | |||||
| } | |||||
| // Popover functionality | |||||
| class VanillaPopover { | |||||
| constructor(element) { | |||||
| this.element = element; | |||||
| this.popover = null; | |||||
| this.isOpen = false; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.element.addEventListener('click', (e) => { | |||||
| e.preventDefault(); | |||||
| this.toggle(); | |||||
| }); | |||||
| // Close on outside click | |||||
| document.addEventListener('click', (e) => { | |||||
| if (!this.element.contains(e.target) && (!this.popover || !this.popover.contains(e.target))) { | |||||
| this.hide(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| toggle() { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| show() { | |||||
| if (this.isOpen) return; | |||||
| // Close other popovers | |||||
| document.querySelectorAll('.popover').forEach(popover => { | |||||
| popover.remove(); | |||||
| }); | |||||
| const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title'); | |||||
| const content = this.element.getAttribute('data-bs-content'); | |||||
| this.popover = document.createElement('div'); | |||||
| this.popover.className = 'popover bs-popover-top show'; | |||||
| this.popover.style.position = 'absolute'; | |||||
| this.popover.style.zIndex = '1070'; | |||||
| this.popover.style.maxWidth = '276px'; | |||||
| this.popover.style.backgroundColor = '#fff'; | |||||
| this.popover.style.border = '1px solid rgba(0,0,0,.2)'; | |||||
| this.popover.style.borderRadius = '6px'; | |||||
| this.popover.style.boxShadow = '0 5px 10px rgba(0,0,0,.2)'; | |||||
| let popoverContent = ''; | |||||
| if (title) { | |||||
| popoverContent += `<div class="popover-header" style="padding: 8px 14px; margin-bottom: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; font-weight: 600;">${title}</div>`; | |||||
| } | |||||
| popoverContent += `<div class="popover-body" style="padding: 9px 14px; word-wrap: break-word;">${content}</div>`; | |||||
| this.popover.innerHTML = popoverContent; | |||||
| document.body.appendChild(this.popover); | |||||
| // Position popover | |||||
| const rect = this.element.getBoundingClientRect(); | |||||
| this.popover.style.left = `${rect.left + (rect.width / 2) - (this.popover.offsetWidth / 2)}px`; | |||||
| this.popover.style.top = `${rect.top - this.popover.offsetHeight - 10}px`; | |||||
| this.isOpen = true; | |||||
| } | |||||
| hide() { | |||||
| if (!this.isOpen) return; | |||||
| if (this.popover) { | |||||
| this.popover.remove(); | |||||
| this.popover = null; | |||||
| } | |||||
| this.isOpen = false; | |||||
| } | |||||
| } | |||||
| // Tooltip functionality | |||||
| class VanillaTooltip { | |||||
| constructor(element) { | |||||
| this.element = element; | |||||
| this.tooltip = null; | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.element.addEventListener('mouseenter', () => this.show()); | |||||
| this.element.addEventListener('mouseleave', () => this.hide()); | |||||
| this.element.addEventListener('focus', () => this.show()); | |||||
| this.element.addEventListener('blur', () => this.hide()); | |||||
| } | |||||
| show() { | |||||
| if (this.tooltip) return; | |||||
| const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title'); | |||||
| const placement = this.element.getAttribute('data-bs-placement') || 'top'; | |||||
| if (!title) return; | |||||
| this.tooltip = document.createElement('div'); | |||||
| this.tooltip.className = `tooltip bs-tooltip-${placement} show`; | |||||
| this.tooltip.style.position = 'absolute'; | |||||
| this.tooltip.style.zIndex = '1070'; | |||||
| this.tooltip.style.maxWidth = '200px'; | |||||
| this.tooltip.style.padding = '4px 8px'; | |||||
| this.tooltip.style.fontSize = '12px'; | |||||
| this.tooltip.style.backgroundColor = '#000'; | |||||
| this.tooltip.style.color = '#fff'; | |||||
| this.tooltip.style.borderRadius = '4px'; | |||||
| this.tooltip.style.pointerEvents = 'none'; | |||||
| this.tooltip.style.whiteSpace = 'nowrap'; | |||||
| this.tooltip.innerHTML = `<div class="tooltip-inner">${title}</div>`; | |||||
| document.body.appendChild(this.tooltip); | |||||
| // Position tooltip | |||||
| const rect = this.element.getBoundingClientRect(); | |||||
| switch (placement) { | |||||
| case 'top': | |||||
| this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`; | |||||
| this.tooltip.style.top = `${rect.top - this.tooltip.offsetHeight - 5}px`; | |||||
| break; | |||||
| case 'bottom': | |||||
| this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`; | |||||
| this.tooltip.style.top = `${rect.bottom + 5}px`; | |||||
| break; | |||||
| case 'left': | |||||
| this.tooltip.style.left = `${rect.left - this.tooltip.offsetWidth - 5}px`; | |||||
| this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`; | |||||
| break; | |||||
| case 'right': | |||||
| this.tooltip.style.left = `${rect.right + 5}px`; | |||||
| this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`; | |||||
| break; | |||||
| } | |||||
| } | |||||
| hide() { | |||||
| if (this.tooltip) { | |||||
| this.tooltip.remove(); | |||||
| this.tooltip = null; | |||||
| } | |||||
| } | |||||
| } | |||||
| // Accordion functionality | |||||
| class VanillaAccordion { | |||||
| constructor(element) { | |||||
| this.element = element; | |||||
| this.accordion = element.closest('.accordion'); | |||||
| this.target = document.querySelector(element.getAttribute('data-bs-target')); | |||||
| this.isOpen = !element.classList.contains('collapsed'); | |||||
| this.init(); | |||||
| } | |||||
| init() { | |||||
| this.element.addEventListener('click', (e) => { | |||||
| e.preventDefault(); | |||||
| this.toggle(); | |||||
| }); | |||||
| } | |||||
| toggle() { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| show() { | |||||
| if (this.isOpen) return; | |||||
| // Close other accordion items in the same parent | |||||
| const parentAccordion = this.accordion; | |||||
| if (parentAccordion) { | |||||
| const otherItems = parentAccordion.querySelectorAll('.accordion-collapse.show'); | |||||
| otherItems.forEach(item => { | |||||
| if (item !== this.target) { | |||||
| item.classList.remove('show'); | |||||
| const button = parentAccordion.querySelector(`[data-bs-target="#${item.id}"]`); | |||||
| if (button) { | |||||
| button.classList.add('collapsed'); | |||||
| button.setAttribute('aria-expanded', 'false'); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| // Show this item | |||||
| this.target.classList.add('show'); | |||||
| this.element.classList.remove('collapsed'); | |||||
| this.element.setAttribute('aria-expanded', 'true'); | |||||
| this.isOpen = true; | |||||
| } | |||||
| hide() { | |||||
| if (!this.isOpen) return; | |||||
| this.target.classList.remove('show'); | |||||
| this.element.classList.add('collapsed'); | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| this.isOpen = false; | |||||
| } | |||||
| } | |||||
| // Initialize all components | |||||
| const initComponents = () => { | |||||
| // Initialize modals | |||||
| document.querySelectorAll('[data-bs-toggle="modal"]').forEach(element => { | |||||
| new VanillaModal(element); | |||||
| }); | |||||
| // Initialize dropdowns | |||||
| document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(element => { | |||||
| new VanillaDropdown(element); | |||||
| }); | |||||
| // Initialize popovers | |||||
| document.querySelectorAll('[data-bs-toggle="popover"]').forEach(element => { | |||||
| new VanillaPopover(element); | |||||
| }); | |||||
| // Initialize tooltips | |||||
| document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(element => { | |||||
| new VanillaTooltip(element); | |||||
| }); | |||||
| // Initialize accordions | |||||
| document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(element => { | |||||
| new VanillaAccordion(element); | |||||
| }); | |||||
| }; | |||||
| // Initialize when DOM is ready | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', initComponents); | |||||
| } else { | |||||
| initComponents(); | |||||
| } | |||||
| // Public API | |||||
| return { | |||||
| init: initComponents, | |||||
| Modal: VanillaModal, | |||||
| Dropdown: VanillaDropdown, | |||||
| Popover: VanillaPopover, | |||||
| Tooltip: VanillaTooltip, | |||||
| Accordion: VanillaAccordion | |||||
| }; | |||||
| }()); | |||||
| @ -1,101 +1,277 @@ | |||||
| import * as $ from 'jquery'; | |||||
| import 'jvectormap'; | |||||
| import 'jvectormap/jquery-jvectormap.css'; | |||||
| import './jquery-jvectormap-world-mill.js'; | |||||
| import jsVectorMap from 'jsvectormap'; | |||||
| import 'jsvectormap/dist/jsvectormap.css'; | |||||
| import 'jsvectormap/dist/maps/world.js'; | |||||
| import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||
| import Theme from '../utils/theme.js'; | import Theme from '../utils/theme.js'; | ||||
| export default (function () { | export default (function () { | ||||
| // Store map instance for cleanup | |||||
| let mapInstance = null; | |||||
| // Main initialization function | |||||
| const vectorMapInit = () => { | const vectorMapInit = () => { | ||||
| if ($('#world-map-marker').length > 0) { | |||||
| // This is a hack, as the .empty() did not do the work | |||||
| $('#vmap').remove(); | |||||
| // we recreate (after removing it) the container div, to reset all the data of the map | |||||
| $('#world-map-marker').append(` | |||||
| <div | |||||
| id="vmap" | |||||
| style=" | |||||
| height: 490px; | |||||
| position: relative; | |||||
| overflow: hidden; | |||||
| background-color: transparent; | |||||
| " | |||||
| > | |||||
| </div> | |||||
| `); | |||||
| // Get current theme colors | |||||
| const colors = Theme.getVectorMapColors(); | |||||
| $('#vmap').vectorMap({ | |||||
| map: 'world_mill', | |||||
| backgroundColor: colors.backgroundColor, | |||||
| borderColor: colors.borderColor, | |||||
| borderOpacity: 0.25, | |||||
| borderWidth: 0, | |||||
| color: colors.regionColor, | |||||
| regionStyle : { | |||||
| initial : { | |||||
| fill : colors.regionColor, | |||||
| const worldMapContainer = document.getElementById('world-map-marker'); | |||||
| if (!worldMapContainer) return; | |||||
| // Remove existing map | |||||
| const existingMap = document.getElementById('vmap'); | |||||
| if (existingMap) { | |||||
| existingMap.remove(); | |||||
| } | |||||
| // Destroy existing map instance | |||||
| if (mapInstance) { | |||||
| try { | |||||
| mapInstance.destroy(); | |||||
| } catch (e) { | |||||
| // Map instance cleanup | |||||
| } | |||||
| mapInstance = null; | |||||
| } | |||||
| // Get current theme colors - using template colors directly | |||||
| const isDark = Theme.current() === 'dark'; | |||||
| const colors = { | |||||
| backgroundColor: isDark ? '#313644' : '#f9fafb', | |||||
| regionColor: isDark ? '#565a5c' : '#e6eaf0', | |||||
| borderColor: isDark ? '#72777a' : '#d3d9e3', | |||||
| hoverColor: isDark ? '#7774e7' : '#0f9aee', | |||||
| selectedColor: isDark ? '#37c936' : '#7774e7', | |||||
| markerFill: isDark ? '#0f9aee' : '#7774e7', | |||||
| markerStroke: isDark ? '#37c936' : '#0f9aee', | |||||
| scaleStart: isDark ? '#b9c2d0' : '#e6eaf0', | |||||
| scaleEnd: isDark ? '#0f9aee' : '#007bff', | |||||
| textColor: isDark ? '#99abb4' : '#72777a', | |||||
| }; | |||||
| // Create new map container | |||||
| const mapContainer = document.createElement('div'); | |||||
| mapContainer.id = 'vmap'; | |||||
| mapContainer.style.height = '490px'; | |||||
| mapContainer.style.position = 'relative'; | |||||
| mapContainer.style.overflow = 'hidden'; | |||||
| mapContainer.style.backgroundColor = colors.backgroundColor; | |||||
| mapContainer.style.borderRadius = '8px'; | |||||
| mapContainer.style.border = `1px solid ${colors.borderColor}`; | |||||
| worldMapContainer.appendChild(mapContainer); | |||||
| // Initialize JSVectorMap | |||||
| try { | |||||
| mapInstance = jsVectorMap({ | |||||
| selector: '#vmap', | |||||
| map: 'world', | |||||
| // Styling options | |||||
| backgroundColor: 'transparent', | |||||
| // Region styling | |||||
| regionStyle: { | |||||
| initial: { | |||||
| fill: colors.regionColor, | |||||
| stroke: colors.borderColor, | |||||
| 'stroke-width': 1, | |||||
| 'stroke-opacity': 0.4, | |||||
| }, | |||||
| hover: { | |||||
| fill: colors.hoverColor, | |||||
| cursor: 'pointer', | |||||
| }, | |||||
| selected: { | |||||
| fill: colors.selectedColor, | |||||
| }, | }, | ||||
| }, | }, | ||||
| // Marker styling | |||||
| markerStyle: { | markerStyle: { | ||||
| initial: { | initial: { | ||||
| r: 7, | r: 7, | ||||
| 'fill': colors.markerFill, | |||||
| 'fill-opacity':1, | |||||
| 'stroke': colors.markerStroke, | |||||
| 'stroke-width' : 2, | |||||
| fill: colors.markerFill, | |||||
| stroke: colors.markerStroke, | |||||
| 'stroke-width': 2, | |||||
| 'stroke-opacity': 0.4, | 'stroke-opacity': 0.4, | ||||
| }, | }, | ||||
| hover: { | |||||
| r: 10, | |||||
| fill: colors.hoverColor, | |||||
| 'stroke-opacity': 0.8, | |||||
| cursor: 'pointer', | |||||
| }, | |||||
| }, | }, | ||||
| markers : [{ | |||||
| latLng : [21.00, 78.00], | |||||
| name : 'INDIA : 350', | |||||
| }, { | |||||
| latLng : [-33.00, 151.00], | |||||
| name : 'Australia : 250', | |||||
| }, { | |||||
| latLng : [36.77, -119.41], | |||||
| name : 'USA : 250', | |||||
| }, { | |||||
| latLng : [55.37, -3.41], | |||||
| name : 'UK : 250', | |||||
| }, { | |||||
| latLng : [25.20, 55.27], | |||||
| name : 'UAE : 250', | |||||
| }], | |||||
| series: { | |||||
| regions: [{ | |||||
| values: { | |||||
| 'US': 298, | |||||
| 'SA': 200, | |||||
| 'AU': 760, | |||||
| 'IN': 200, | |||||
| 'GB': 120, | |||||
| }, | |||||
| scale: [colors.scaleStart, colors.scaleEnd], | |||||
| normalizeFunction: 'polynomial', | |||||
| }], | |||||
| }, | |||||
| hoverOpacity: null, | |||||
| normalizeFunction: 'linear', | |||||
| // Markers data | |||||
| markers: [ | |||||
| { | |||||
| name: 'INDIA : 350', | |||||
| coords: [21.00, 78.00], | |||||
| }, | |||||
| { | |||||
| name: 'Australia : 250', | |||||
| coords: [-33.00, 151.00], | |||||
| }, | |||||
| { | |||||
| name: 'USA : 250', | |||||
| coords: [36.77, -119.41], | |||||
| }, | |||||
| { | |||||
| name: 'UK : 250', | |||||
| coords: [55.37, -3.41], | |||||
| }, | |||||
| { | |||||
| name: 'UAE : 250', | |||||
| coords: [25.20, 55.27], | |||||
| }, | |||||
| ], | |||||
| // Simplified approach - remove series for now to test base colors | |||||
| // series: { | |||||
| // regions: [ | |||||
| // { | |||||
| // attribute: 'fill', | |||||
| // scale: [colors.scaleStart, colors.scaleEnd], | |||||
| // normalizeFunction: 'polynomial', | |||||
| // values: { | |||||
| // 'US': 50, | |||||
| // 'SA': 30, | |||||
| // 'AU': 70, | |||||
| // 'IN': 40, | |||||
| // 'GB': 60, | |||||
| // 'LV': 80, | |||||
| // }, | |||||
| // }, | |||||
| // ], | |||||
| // }, | |||||
| // Interaction options | |||||
| zoomOnScroll: false, | zoomOnScroll: false, | ||||
| scaleColors: [colors.scaleLight, colors.scaleDark], | |||||
| selectedColor: colors.selectedColor, | |||||
| selectedRegions: [], | |||||
| enableZoom: false, | |||||
| hoverColor: colors.hoverColor, | |||||
| zoomButtons: false, | |||||
| // Event handlers | |||||
| onMarkerTooltipShow(event, tooltip, index) { | |||||
| // Safe access to marker data | |||||
| const marker = this.markers && this.markers[index]; | |||||
| const markerName = marker ? marker.name : `Marker ${index + 1}`; | |||||
| tooltip.text(markerName); | |||||
| }, | |||||
| onRegionTooltipShow(event, tooltip, code) { | |||||
| // Safe access to region data | |||||
| const regionName = (this.mapData && this.mapData.paths && this.mapData.paths[code]) | |||||
| ? this.mapData.paths[code].name || code | |||||
| : code; | |||||
| const value = (this.series && this.series.regions && this.series.regions[0] && this.series.regions[0].values) | |||||
| ? this.series.regions[0].values[code] | |||||
| : null; | |||||
| tooltip.text(`${regionName}${value ? `: ${ value}` : ''}`); | |||||
| }, | |||||
| onLoaded(map) { | |||||
| // Map loaded successfully | |||||
| }, | |||||
| }); | }); | ||||
| // Store instance for theme updates | |||||
| worldMapContainer.mapInstance = mapInstance; | |||||
| } catch (error) { | |||||
| // Error initializing JSVectorMap | |||||
| // Fallback: show a simple message | |||||
| mapContainer.innerHTML = ` | |||||
| <div style=" | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| height: 100%; | |||||
| background: ${colors.backgroundColor}; | |||||
| border: 1px solid ${colors.borderColor}; | |||||
| border-radius: 8px; | |||||
| color: ${colors.textColor}; | |||||
| font-size: 14px; | |||||
| "> | |||||
| <div style="text-align: center;"> | |||||
| <div style="font-size: 24px; margin-bottom: 8px;">🗺️</div> | |||||
| <div>World Map</div> | |||||
| <div style="font-size: 12px; margin-top: 4px;">Interactive map will load here</div> | |||||
| </div> | |||||
| </div> | |||||
| `; | |||||
| } | } | ||||
| }; | }; | ||||
| // Theme update function | |||||
| const updateMapTheme = () => { | |||||
| if (mapInstance) { | |||||
| const isDark = Theme.current() === 'dark'; | |||||
| const colors = { | |||||
| backgroundColor: isDark ? '#313644' : '#f9fafb', | |||||
| regionColor: isDark ? '#565a5c' : '#e6eaf0', | |||||
| borderColor: isDark ? '#72777a' : '#d3d9e3', | |||||
| hoverColor: isDark ? '#7774e7' : '#0f9aee', | |||||
| selectedColor: isDark ? '#37c936' : '#7774e7', | |||||
| markerFill: isDark ? '#0f9aee' : '#7774e7', | |||||
| markerStroke: isDark ? '#37c936' : '#0f9aee', | |||||
| scaleStart: isDark ? '#b9c2d0' : '#e6eaf0', | |||||
| scaleEnd: isDark ? '#0f9aee' : '#007bff', | |||||
| textColor: isDark ? '#99abb4' : '#72777a', | |||||
| }; | |||||
| try { | |||||
| // Update region styles - commented out series for now | |||||
| // mapInstance.updateSeries('regions', { | |||||
| // attribute: 'fill', | |||||
| // scale: [colors.scaleStart, colors.scaleEnd], | |||||
| // values: { | |||||
| // 'US': 50, | |||||
| // 'SA': 30, | |||||
| // 'AU': 70, | |||||
| // 'IN': 40, | |||||
| // 'GB': 60, | |||||
| // 'LV': 80, | |||||
| // }, | |||||
| // }); | |||||
| // Update container background | |||||
| const container = document.getElementById('vmap'); | |||||
| if (container) { | |||||
| container.style.backgroundColor = colors.backgroundColor; | |||||
| } | |||||
| } catch (error) { | |||||
| // Theme update failed, reinitializing map | |||||
| vectorMapInit(); | |||||
| } | |||||
| } else { | |||||
| vectorMapInit(); | |||||
| } | |||||
| }; | |||||
| // Initialize map | |||||
| vectorMapInit(); | vectorMapInit(); | ||||
| $(window).resize(debounce(vectorMapInit, 150)); | |||||
| // Listen for theme changes and reinitialize the vector map | |||||
| window.addEventListener('adminator:themeChanged', debounce(vectorMapInit, 150)); | |||||
| })(); | |||||
| // Reinitialize on window resize | |||||
| window.addEventListener('resize', debounce(vectorMapInit, 300)); | |||||
| // Listen for theme changes | |||||
| window.addEventListener('adminator:themeChanged', debounce(updateMapTheme, 150)); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| if (mapInstance) { | |||||
| try { | |||||
| mapInstance.destroy(); | |||||
| } catch (e) { | |||||
| // Map cleanup on unload | |||||
| } | |||||
| mapInstance = null; | |||||
| } | |||||
| }); | |||||
| // Return public API | |||||
| return { | |||||
| init: vectorMapInit, | |||||
| updateTheme: updateMapTheme, | |||||
| getInstance: () => mapInstance, | |||||
| }; | |||||
| }()); | |||||