| @ -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 () { | |||
| 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 { COLORS } from '../../constants/colors'; | |||
| import Theme from '../../utils/theme.js'; | |||
| // Register Chart.js components | |||
| Chart.register(...registerables); | |||
| 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 | |||
| // ------------------------------------------------------ | |||
| $('#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 () { | |||
| $('#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 () { | |||
| $('#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 () { | |||
| $('.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 () { | |||
| $('.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 () { | |||
| // ------------------------------------------------------ | |||
| // @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'; | |||
| export default (function () { | |||
| const scrollables = $('.scrollable'); | |||
| const scrollables = document.querySelectorAll('.scrollable'); | |||
| if (scrollables.length > 0) { | |||
| scrollables.each((index, el) => { | |||
| scrollables.forEach(el => { | |||
| new PerfectScrollbar(el); | |||
| }); | |||
| } | |||
| @ -1,9 +1,15 @@ | |||
| import * as $ from 'jquery'; | |||
| 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 () { | |||
| // 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 | |||
| 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; | |||
| 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 | |||
| * 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 Theme from '../utils/theme.js'; | |||
| export default (function () { | |||
| // Store map instance for cleanup | |||
| let mapInstance = null; | |||
| // Main initialization function | |||
| 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: { | |||
| initial: { | |||
| 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, | |||
| }, | |||
| 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, | |||
| 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(); | |||
| $(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, | |||
| }; | |||
| }()); | |||