6415 lines
182 KiB
JavaScript
6415 lines
182 KiB
JavaScript
"use strict";
|
|
(self["webpackChunk"] = self["webpackChunk"] || []).push([[792],{
|
|
|
|
/***/ 170
|
|
(__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) {
|
|
|
|
|
|
;// ../assets/scripts/utils/dom.js
|
|
/**
|
|
* Adminator DOM Utility Functions
|
|
* Provides jQuery-like functionality using vanilla JavaScript
|
|
*
|
|
* @module utils/dom
|
|
* @example
|
|
* import { DOM } from './utils/dom';
|
|
*
|
|
* // Select elements
|
|
* const button = DOM.select('.my-button');
|
|
* const items = DOM.selectAll('.list-item');
|
|
*
|
|
* // Add event listeners
|
|
* DOM.on(button, 'click', () => console.log('Clicked!'));
|
|
*
|
|
* // Manipulate classes
|
|
* DOM.addClass(button, 'active');
|
|
* DOM.toggleClass(button, 'loading');
|
|
*/
|
|
|
|
/**
|
|
* DOM utility object providing jQuery-like methods
|
|
* @namespace
|
|
*/
|
|
const DOM = {
|
|
/**
|
|
* Select a single element matching the selector
|
|
* Replaces jQuery's $('selector').first()
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @param {Document|Element} [context=document] - Context to search within
|
|
* @returns {Element|null} The matched element or null
|
|
*
|
|
* @example
|
|
* const header = DOM.select('.header');
|
|
* const navItem = DOM.select('.nav-item', sidebar);
|
|
*/
|
|
select: function (selector) {
|
|
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
|
|
return context.querySelector(selector);
|
|
},
|
|
/**
|
|
* Select all elements matching the selector
|
|
* Replaces jQuery's $('selector')
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @param {Document|Element} [context=document] - Context to search within
|
|
* @returns {Element[]} Array of matched elements
|
|
*
|
|
* @example
|
|
* const buttons = DOM.selectAll('.btn');
|
|
* buttons.forEach(btn => DOM.addClass(btn, 'initialized'));
|
|
*/
|
|
selectAll: function (selector) {
|
|
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
|
|
return Array.from(context.querySelectorAll(selector));
|
|
},
|
|
/**
|
|
* Check if an element matching the selector exists
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @returns {boolean} True if element exists
|
|
*
|
|
* @example
|
|
* if (DOM.exists('.sidebar')) {
|
|
* initSidebar();
|
|
* }
|
|
*/
|
|
exists: selector => {
|
|
return document.querySelector(selector) !== null;
|
|
},
|
|
/**
|
|
* Add an event listener to an element
|
|
* Replaces jQuery's $.on()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} event - Event name (e.g., 'click', 'change')
|
|
* @param {Function} handler - Event handler function
|
|
* @param {Object} [options={}] - addEventListener options
|
|
*
|
|
* @example
|
|
* DOM.on('.btn', 'click', handleClick);
|
|
* DOM.on(button, 'click', handleClick, { once: true });
|
|
*/
|
|
on: function (element, event, handler) {
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.addEventListener(event, handler, options);
|
|
}
|
|
},
|
|
/**
|
|
* Remove an event listener from an element
|
|
* Replaces jQuery's $.off()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} event - Event name
|
|
* @param {Function} handler - Event handler to remove
|
|
*
|
|
* @example
|
|
* DOM.off(button, 'click', handleClick);
|
|
*/
|
|
off: (element, event, handler) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.removeEventListener(event, handler);
|
|
}
|
|
},
|
|
/**
|
|
* Add a class to an element
|
|
* Replaces jQuery's $.addClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to add
|
|
*
|
|
* @example
|
|
* DOM.addClass('.menu', 'open');
|
|
*/
|
|
addClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.add(className);
|
|
}
|
|
},
|
|
/**
|
|
* Remove a class from an element
|
|
* Replaces jQuery's $.removeClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to remove
|
|
*
|
|
* @example
|
|
* DOM.removeClass('.menu', 'open');
|
|
*/
|
|
removeClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.remove(className);
|
|
}
|
|
},
|
|
/**
|
|
* Toggle a class on an element
|
|
* Replaces jQuery's $.toggleClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to toggle
|
|
*
|
|
* @example
|
|
* DOM.toggleClass('.dropdown', 'show');
|
|
*/
|
|
toggleClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.toggle(className);
|
|
}
|
|
},
|
|
/**
|
|
* Check if an element has a class
|
|
* Replaces jQuery's $.hasClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to check
|
|
* @returns {boolean} True if element has the class
|
|
*
|
|
* @example
|
|
* if (DOM.hasClass('.menu', 'open')) {
|
|
* closeMenu();
|
|
* }
|
|
*/
|
|
hasClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
return element ? element.classList.contains(className) : false;
|
|
},
|
|
/**
|
|
* Get or set an attribute on an element
|
|
* Replaces jQuery's $.attr()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} name - Attribute name
|
|
* @param {string} [value] - Value to set (omit to get)
|
|
* @returns {string|Element|null} Attribute value when getting, element when setting
|
|
*
|
|
* @example
|
|
* // Get attribute
|
|
* const href = DOM.attr(link, 'href');
|
|
*
|
|
* // Set attribute
|
|
* DOM.attr(link, 'href', '/new-page');
|
|
*/
|
|
attr: (element, name, value) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (value === undefined) {
|
|
return element.getAttribute(name);
|
|
} else {
|
|
element.setAttribute(name, value);
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set a data attribute on an element
|
|
* Replaces jQuery's $.data()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} name - Data attribute name (without 'data-' prefix)
|
|
* @param {string} [value] - Value to set (omit to get)
|
|
* @returns {string|Element|null} Data value when getting, element when setting
|
|
*
|
|
* @example
|
|
* // Get data attribute
|
|
* const id = DOM.data(row, 'id'); // Gets data-id
|
|
*
|
|
* // Set data attribute
|
|
* DOM.data(row, 'id', '123');
|
|
*/
|
|
data: (element, name, value) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
const dataName = `data-${name}`;
|
|
if (value === undefined) {
|
|
return element.getAttribute(dataName);
|
|
} else {
|
|
element.setAttribute(dataName, value);
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set text content of an element
|
|
* Replaces jQuery's $.text()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [content] - Text to set (omit to get)
|
|
* @returns {string|Element|null} Text content when getting, element when setting
|
|
*
|
|
* @example
|
|
* const text = DOM.text('.title');
|
|
* DOM.text('.title', 'New Title');
|
|
*/
|
|
text: (element, content) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (content === undefined) {
|
|
return element.textContent;
|
|
} else {
|
|
element.textContent = content;
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set HTML content of an element
|
|
* Replaces jQuery's $.html()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [content] - HTML to set (omit to get)
|
|
* @returns {string|Element|null} HTML content when getting, element when setting
|
|
*
|
|
* @example
|
|
* const html = DOM.html('.container');
|
|
* DOM.html('.container', '<p>New content</p>');
|
|
*/
|
|
html: (element, content) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (content === undefined) {
|
|
return element.innerHTML;
|
|
} else {
|
|
element.innerHTML = content;
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Hide an element
|
|
* Replaces jQuery's $.hide()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
*
|
|
* @example
|
|
* DOM.hide('.modal');
|
|
*/
|
|
hide: element => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.style.display = 'none';
|
|
}
|
|
},
|
|
/**
|
|
* Show an element
|
|
* Replaces jQuery's $.show()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [display='block'] - Display value to use
|
|
*
|
|
* @example
|
|
* DOM.show('.modal');
|
|
* DOM.show('.flex-item', 'flex');
|
|
*/
|
|
show: function (element) {
|
|
let display = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'block';
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.style.display = display;
|
|
}
|
|
},
|
|
/**
|
|
* Toggle element visibility
|
|
* Replaces jQuery's $.toggle()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [display='block'] - Display value when showing
|
|
*
|
|
* @example
|
|
* DOM.toggle('.menu');
|
|
*/
|
|
toggle: function (element) {
|
|
let display = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'block';
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
if (element.style.display === 'none') {
|
|
element.style.display = display;
|
|
} else {
|
|
element.style.display = 'none';
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Animate element sliding up (collapsing)
|
|
* Replaces jQuery's $.slideUp()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.slideUp('.panel');
|
|
*/
|
|
slideUp: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
const height = element.scrollHeight;
|
|
element.style.height = `${height}px`;
|
|
element.style.overflow = 'hidden';
|
|
element.animate([{
|
|
height: `${height}px`
|
|
}, {
|
|
height: '0px'
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.display = 'none';
|
|
element.style.height = '';
|
|
element.style.overflow = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element sliding down (expanding)
|
|
* Replaces jQuery's $.slideDown()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.slideDown('.panel');
|
|
*/
|
|
slideDown: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.style.display = 'block';
|
|
element.style.height = '0px';
|
|
element.style.overflow = 'hidden';
|
|
const height = element.scrollHeight;
|
|
element.animate([{
|
|
height: '0px'
|
|
}, {
|
|
height: `${height}px`
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.height = 'auto';
|
|
element.style.overflow = 'visible';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element fading in
|
|
* Replaces jQuery's $.fadeIn()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.fadeIn('.modal');
|
|
*/
|
|
fadeIn: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.style.opacity = '0';
|
|
element.style.display = 'block';
|
|
element.animate([{
|
|
opacity: 0
|
|
}, {
|
|
opacity: 1
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.opacity = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element fading out
|
|
* Replaces jQuery's $.fadeOut()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.fadeOut('.modal');
|
|
*/
|
|
fadeOut: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.animate([{
|
|
opacity: 1
|
|
}, {
|
|
opacity: 0
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.display = 'none';
|
|
element.style.opacity = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Get element dimensions and position relative to viewport
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @returns {Object|null} Dimensions object or null
|
|
* @property {number} width - Element width
|
|
* @property {number} height - Element height
|
|
* @property {number} top - Distance from viewport top
|
|
* @property {number} left - Distance from viewport left
|
|
* @property {number} bottom - Distance from viewport bottom
|
|
* @property {number} right - Distance from viewport right
|
|
*
|
|
* @example
|
|
* const { width, height, top, left } = DOM.dimensions('.card');
|
|
*/
|
|
dimensions: element => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
const rect = element.getBoundingClientRect();
|
|
return {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top,
|
|
left: rect.left,
|
|
bottom: rect.bottom,
|
|
right: rect.right
|
|
};
|
|
},
|
|
/**
|
|
* Execute callback when DOM is ready
|
|
* Replaces jQuery's $(document).ready()
|
|
*
|
|
* @param {Function} callback - Function to execute when DOM is ready
|
|
*
|
|
* @example
|
|
* DOM.ready(() => {
|
|
* initApp();
|
|
* });
|
|
*/
|
|
ready: callback => {
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
},
|
|
/**
|
|
* Create an element with optional attributes and children
|
|
*
|
|
* @param {string} tag - HTML tag name
|
|
* @param {Object} [attrs={}] - Attributes to set
|
|
* @param {Array<Element|string>} [children=[]] - Child elements or text
|
|
* @returns {Element} The created element
|
|
*
|
|
* @example
|
|
* const button = DOM.create('button', { class: 'btn', type: 'submit' }, ['Submit']);
|
|
*/
|
|
create: function (tag) {
|
|
let attrs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
let children = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
|
|
const el = document.createElement(tag);
|
|
Object.entries(attrs).forEach(_ref => {
|
|
let [key, value] = _ref;
|
|
if (key === 'class') {
|
|
el.className = value;
|
|
} else if (key.startsWith('data-')) {
|
|
el.setAttribute(key, value);
|
|
} else {
|
|
el[key] = value;
|
|
}
|
|
});
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
el.appendChild(document.createTextNode(child));
|
|
} else if (child instanceof Element) {
|
|
el.appendChild(child);
|
|
}
|
|
});
|
|
return el;
|
|
},
|
|
/**
|
|
* Find the closest ancestor matching a selector
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} selector - Selector to match
|
|
* @returns {Element|null} Closest matching ancestor or null
|
|
*
|
|
* @example
|
|
* const form = DOM.closest(input, 'form');
|
|
*/
|
|
closest: (element, selector) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
return element ? element.closest(selector) : null;
|
|
}
|
|
};
|
|
/* harmony default export */ const dom = ((/* unused pure expression or super */ null && (DOM)));
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/dayjs.min.js
|
|
var dayjs_min = __webpack_require__(464);
|
|
var dayjs_min_default = /*#__PURE__*/__webpack_require__.n(dayjs_min);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/utc.js
|
|
var utc = __webpack_require__(657);
|
|
var utc_default = /*#__PURE__*/__webpack_require__.n(utc);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/timezone.js
|
|
var timezone = __webpack_require__(168);
|
|
var timezone_default = /*#__PURE__*/__webpack_require__.n(timezone);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/relativeTime.js
|
|
var relativeTime = __webpack_require__(562);
|
|
var relativeTime_default = /*#__PURE__*/__webpack_require__.n(relativeTime);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/customParseFormat.js
|
|
var customParseFormat = __webpack_require__(630);
|
|
var customParseFormat_default = /*#__PURE__*/__webpack_require__.n(customParseFormat);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/advancedFormat.js
|
|
var advancedFormat = __webpack_require__(258);
|
|
var advancedFormat_default = /*#__PURE__*/__webpack_require__.n(advancedFormat);
|
|
;// ../assets/scripts/utils/date.js
|
|
/**
|
|
* Modern Date Utilities
|
|
* Using Day.js (2KB) instead of Moment.js (67KB) - 97% size reduction
|
|
* Provides consistent date formatting and manipulation across the application
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Enable Day.js plugins
|
|
dayjs_min_default().extend((utc_default()));
|
|
dayjs_min_default().extend((timezone_default()));
|
|
dayjs_min_default().extend((relativeTime_default()));
|
|
dayjs_min_default().extend((customParseFormat_default()));
|
|
dayjs_min_default().extend((advancedFormat_default()));
|
|
const DateUtils = {
|
|
/**
|
|
* Get current date/time
|
|
*/
|
|
now: () => dayjs_min_default()(),
|
|
/**
|
|
* Parse date from string or Date object
|
|
*/
|
|
parse: function (input) {
|
|
let format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
return format ? dayjs_min_default()(input, format) : dayjs_min_default()(input);
|
|
},
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
format: function (date) {
|
|
let format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'YYYY-MM-DD';
|
|
return dayjs_min_default()(date).format(format);
|
|
},
|
|
/**
|
|
* Common date formatting presets
|
|
*/
|
|
formatters: {
|
|
// Dashboard display formats
|
|
shortDate: date => dayjs_min_default()(date).format('MMM DD, YYYY'),
|
|
longDate: date => dayjs_min_default()(date).format('MMMM DD, YYYY'),
|
|
dateTime: date => dayjs_min_default()(date).format('MMM DD, YYYY h:mm A'),
|
|
// Calendar formats
|
|
calendarDate: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
calendarDateTime: date => dayjs_min_default()(date).format('YYYY-MM-DD HH:mm:ss'),
|
|
// Form input formats
|
|
inputDate: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
inputDateTime: date => dayjs_min_default()(date).format('YYYY-MM-DDTHH:mm'),
|
|
// Display formats
|
|
timeOnly: date => dayjs_min_default()(date).format('h:mm A'),
|
|
monthYear: date => dayjs_min_default()(date).format('MMMM YYYY'),
|
|
dayMonth: date => dayjs_min_default()(date).format('DD MMM'),
|
|
// Relative time
|
|
relative: date => dayjs_min_default()(date).fromNow(),
|
|
relativeCalendar: date => {
|
|
const now = dayjs_min_default()();
|
|
const target = dayjs_min_default()(date);
|
|
const diffDays = now.diff(target, 'day');
|
|
if (diffDays === 0) return 'Today';
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays === -1) return 'Tomorrow';
|
|
if (diffDays > 1 && diffDays < 7) return `${diffDays} days ago`;
|
|
if (diffDays < -1 && diffDays > -7) return `In ${Math.abs(diffDays)} days`;
|
|
return target.format('MMM DD, YYYY');
|
|
}
|
|
},
|
|
/**
|
|
* Date manipulation
|
|
*/
|
|
add: (date, amount, unit) => dayjs_min_default()(date).add(amount, unit),
|
|
subtract: (date, amount, unit) => dayjs_min_default()(date).subtract(amount, unit),
|
|
startOf: (date, unit) => dayjs_min_default()(date).startOf(unit),
|
|
endOf: (date, unit) => dayjs_min_default()(date).endOf(unit),
|
|
/**
|
|
* Date comparison
|
|
*/
|
|
isBefore: (date1, date2) => dayjs_min_default()(date1).isBefore(dayjs_min_default()(date2)),
|
|
isAfter: (date1, date2) => dayjs_min_default()(date1).isAfter(dayjs_min_default()(date2)),
|
|
isSame: function (date1, date2) {
|
|
let unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'day';
|
|
return dayjs_min_default()(date1).isSame(dayjs_min_default()(date2), unit);
|
|
},
|
|
isBetween: (date, start, end) => dayjs_min_default()(date).isBetween(dayjs_min_default()(start), dayjs_min_default()(end)),
|
|
/**
|
|
* Date validation
|
|
*/
|
|
isValid: date => dayjs_min_default()(date).isValid(),
|
|
/**
|
|
* Timezone utilities
|
|
*/
|
|
timezone: {
|
|
convert: (date, tz) => dayjs_min_default()(date).tz(tz),
|
|
utc: date => dayjs_min_default()(date).utc(),
|
|
local: date => dayjs_min_default()(date).local(),
|
|
guess: () => dayjs_min_default().tz.guess()
|
|
},
|
|
/**
|
|
* Calendar utilities
|
|
*/
|
|
calendar: {
|
|
// Get calendar month data for building calendar views
|
|
getMonthData: function () {
|
|
let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
|
const target = date ? dayjs_min_default()(date) : dayjs_min_default()();
|
|
const startOfMonth = target.startOf('month');
|
|
const endOfMonth = target.endOf('month');
|
|
const startOfCalendar = startOfMonth.startOf('week');
|
|
const endOfCalendar = endOfMonth.endOf('week');
|
|
const days = [];
|
|
let current = startOfCalendar;
|
|
while (current.isBefore(endOfCalendar) || current.isSame(endOfCalendar, 'day')) {
|
|
days.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
day: current.date(),
|
|
isCurrentMonth: current.isSame(target, 'month'),
|
|
isToday: current.isSame(dayjs_min_default()(), 'day'),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, 'day');
|
|
}
|
|
return {
|
|
month: target.format('MMMM YYYY'),
|
|
year: target.year(),
|
|
monthIndex: target.month(),
|
|
days
|
|
};
|
|
},
|
|
// Get week data
|
|
getWeekData: function () {
|
|
let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
|
const target = date ? dayjs_min_default()(date) : dayjs_min_default()();
|
|
const startOfWeek = target.startOf('week');
|
|
const endOfWeek = target.endOf('week');
|
|
const days = [];
|
|
let current = startOfWeek;
|
|
while (current.isBefore(endOfWeek) || current.isSame(endOfWeek, 'day')) {
|
|
days.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
day: current.date(),
|
|
dayName: current.format('dddd'),
|
|
shortDayName: current.format('ddd'),
|
|
isToday: current.isSame(dayjs_min_default()(), 'day'),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, 'day');
|
|
}
|
|
return {
|
|
weekStart: startOfWeek.format('MMM DD'),
|
|
weekEnd: endOfWeek.format('MMM DD, YYYY'),
|
|
days
|
|
};
|
|
}
|
|
},
|
|
/**
|
|
* Form utilities
|
|
*/
|
|
form: {
|
|
// Convert date to HTML5 input format
|
|
toInputValue: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
toDateTimeInputValue: date => dayjs_min_default()(date).format('YYYY-MM-DDTHH:mm'),
|
|
// Parse from HTML5 input
|
|
fromInputValue: value => dayjs_min_default()(value),
|
|
// Validate date input
|
|
validateDateInput: value => {
|
|
const parsed = dayjs_min_default()(value);
|
|
return parsed.isValid() && value.length >= 8; // Basic validation
|
|
}
|
|
},
|
|
/**
|
|
* Chart/Data utilities
|
|
*/
|
|
charts: {
|
|
// Generate date ranges for charts
|
|
generateDateRange: function (start, end) {
|
|
let interval = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'day';
|
|
const dates = [];
|
|
let current = dayjs_min_default()(start);
|
|
const endDate = dayjs_min_default()(end);
|
|
while (current.isBefore(endDate) || current.isSame(endDate, interval)) {
|
|
dates.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
label: current.format('MMM DD'),
|
|
value: current.toISOString(),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, interval);
|
|
}
|
|
return dates;
|
|
},
|
|
// Get common chart date labels
|
|
getChartLabels: function () {
|
|
let period = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'week';
|
|
const now = dayjs_min_default()();
|
|
switch (period) {
|
|
case 'week':
|
|
return Array.from({
|
|
length: 7
|
|
}, (_, i) => now.subtract(6 - i, 'day').format('ddd'));
|
|
case 'month':
|
|
return Array.from({
|
|
length: 30
|
|
}, (_, i) => now.subtract(29 - i, 'day').format('DD'));
|
|
case 'year':
|
|
return Array.from({
|
|
length: 12
|
|
}, (_, i) => now.subtract(11 - i, 'month').format('MMM'));
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Export dayjs instance for direct use when needed
|
|
|
|
|
|
// Default export
|
|
/* harmony default export */ const date = (DateUtils);
|
|
;// ../assets/scripts/utils/theme.js
|
|
/**
|
|
* Adminator Theme Manager
|
|
* Handles light/dark mode switching with localStorage persistence
|
|
*
|
|
* @module utils/theme
|
|
* @example
|
|
* // Get current theme
|
|
* const current = Theme.current(); // 'light' or 'dark'
|
|
*
|
|
* // Toggle theme
|
|
* Theme.toggle();
|
|
*
|
|
* // Apply specific theme
|
|
* Theme.apply('dark');
|
|
*
|
|
* // Listen for theme changes
|
|
* window.addEventListener('adminator:themeChanged', (e) => {
|
|
* console.log('New theme:', e.detail.theme);
|
|
* });
|
|
*/
|
|
|
|
/* global Chart */
|
|
|
|
/** @constant {string} Storage key for theme preference */
|
|
const THEME_KEY = 'adminator-theme';
|
|
|
|
/** @constant {string[]} Valid theme values */
|
|
const VALID_THEMES = ['light', 'dark'];
|
|
|
|
/**
|
|
* Safe localStorage wrapper
|
|
* Handles cases where localStorage is unavailable (private browsing, etc.)
|
|
* @private
|
|
*/
|
|
const Storage = {
|
|
/**
|
|
* Get item from localStorage
|
|
* @param {string} key - Storage key
|
|
* @returns {string|null} Stored value or null
|
|
*/
|
|
get(key) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
/**
|
|
* Set item in localStorage
|
|
* @param {string} key - Storage key
|
|
* @param {string} value - Value to store
|
|
* @returns {boolean} Success status
|
|
*/
|
|
set(key, value) {
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Theme Manager
|
|
* @namespace
|
|
*/
|
|
const Theme = {
|
|
/**
|
|
* Apply a theme to the document
|
|
* Updates Chart.js defaults if available
|
|
*
|
|
* @param {('light'|'dark')} theme - Theme to apply
|
|
* @fires adminator:themeChanged
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Theme.apply('dark');
|
|
*/
|
|
apply(theme) {
|
|
// Validate theme
|
|
if (!VALID_THEMES.includes(theme)) {
|
|
console.warn(`[Adminator] Invalid theme "${theme}". Using "light".`);
|
|
theme = 'light';
|
|
}
|
|
|
|
// Apply to document
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
// Update Chart.js defaults if available
|
|
if (window.Chart && Chart.defaults) {
|
|
const isDark = theme === 'dark';
|
|
const textColor = isDark ? '#FFFFFF' : '#212529';
|
|
const mutedColor = isDark ? '#D1D5DB' : '#6C757D';
|
|
const borderColor = isDark ? '#374151' : '#E2E5E8';
|
|
const gridColor = isDark ? 'rgba(209, 213, 219, 0.15)' : 'rgba(0, 0, 0, 0.05)';
|
|
const tooltipBg = isDark ? '#1F2937' : 'rgba(255, 255, 255, 0.95)';
|
|
|
|
// Set global defaults
|
|
Chart.defaults.color = textColor;
|
|
Chart.defaults.borderColor = borderColor;
|
|
Chart.defaults.backgroundColor = tooltipBg;
|
|
|
|
// Set plugin defaults
|
|
Chart.defaults.plugins.legend.labels.color = textColor;
|
|
Chart.defaults.plugins.tooltip.backgroundColor = tooltipBg;
|
|
Chart.defaults.plugins.tooltip.titleColor = textColor;
|
|
Chart.defaults.plugins.tooltip.bodyColor = textColor;
|
|
Chart.defaults.plugins.tooltip.borderColor = borderColor;
|
|
|
|
// Set scale defaults
|
|
const scales = ['category', 'linear', 'logarithmic', 'time', 'radialLinear'];
|
|
scales.forEach(scale => {
|
|
if (Chart.defaults.scales[scale]) {
|
|
Chart.defaults.scales[scale].ticks.color = mutedColor;
|
|
Chart.defaults.scales[scale].grid.color = gridColor;
|
|
}
|
|
});
|
|
|
|
// RadialLinear specific
|
|
if (Chart.defaults.scales.radialLinear) {
|
|
Chart.defaults.scales.radialLinear.pointLabels.color = mutedColor;
|
|
Chart.defaults.scales.radialLinear.angleLines.color = gridColor;
|
|
}
|
|
}
|
|
|
|
// Persist to storage
|
|
Storage.set(THEME_KEY, theme);
|
|
|
|
// Update toggle accessibility state if exists
|
|
const toggle = document.getElementById('theme-toggle');
|
|
if (toggle) {
|
|
toggle.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
|
|
}
|
|
|
|
// Dispatch event for other components
|
|
window.dispatchEvent(new CustomEvent('adminator:themeChanged', {
|
|
detail: {
|
|
theme
|
|
}
|
|
}));
|
|
return true;
|
|
},
|
|
/**
|
|
* Toggle between light and dark themes
|
|
*
|
|
* @returns {('light'|'dark')} The new theme
|
|
*
|
|
* @example
|
|
* const newTheme = Theme.toggle();
|
|
* console.log('Switched to:', newTheme);
|
|
*/
|
|
toggle() {
|
|
const next = this.current() === 'dark' ? 'light' : 'dark';
|
|
this.apply(next);
|
|
return next;
|
|
},
|
|
/**
|
|
* Get the current theme
|
|
*
|
|
* @returns {('light'|'dark')} Current theme
|
|
*
|
|
* @example
|
|
* if (Theme.current() === 'dark') {
|
|
* // Dark mode specific logic
|
|
* }
|
|
*/
|
|
current() {
|
|
const stored = Storage.get(THEME_KEY);
|
|
return VALID_THEMES.includes(stored) ? stored : 'light';
|
|
},
|
|
/**
|
|
* Initialize the theme system
|
|
* Detects OS preference on first visit, otherwise uses stored preference
|
|
*
|
|
* @returns {('light'|'dark')} The applied theme
|
|
*
|
|
* @example
|
|
* // Call once on app initialization
|
|
* Theme.init();
|
|
*/
|
|
init() {
|
|
const stored = Storage.get(THEME_KEY);
|
|
if (!stored) {
|
|
// First visit - detect OS preference
|
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const theme = prefersDark ? 'dark' : 'light';
|
|
this.apply(theme);
|
|
return theme;
|
|
}
|
|
|
|
// Use stored preference
|
|
this.apply(this.current());
|
|
return this.current();
|
|
},
|
|
/**
|
|
* Get a CSS variable value from the document
|
|
*
|
|
* @param {string} varName - CSS variable name (with or without --)
|
|
* @returns {string} The CSS variable value
|
|
*
|
|
* @example
|
|
* const bgColor = Theme.getCSSVar('--c-bkg-body');
|
|
*/
|
|
getCSSVar(varName) {
|
|
const name = varName.startsWith('--') ? varName : `--${varName}`;
|
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
},
|
|
/**
|
|
* Get theme-aware colors for vector maps
|
|
*
|
|
* @returns {Object} Vector map color configuration
|
|
* @property {string} backgroundColor - Map background color
|
|
* @property {string} borderColor - Border color
|
|
* @property {string} regionColor - Default region fill color
|
|
* @property {string} markerFill - Marker fill color
|
|
* @property {string} markerStroke - Marker stroke color
|
|
* @property {string} hoverColor - Region hover color
|
|
* @property {string} selectedColor - Selected region color
|
|
* @property {string} scaleStart - Scale gradient start
|
|
* @property {string} scaleEnd - Scale gradient end
|
|
*/
|
|
getVectorMapColors() {
|
|
return {
|
|
backgroundColor: this.getCSSVar('--vmap-bg-color'),
|
|
borderColor: this.getCSSVar('--vmap-border-color'),
|
|
regionColor: this.getCSSVar('--vmap-region-color'),
|
|
markerFill: this.getCSSVar('--vmap-marker-fill'),
|
|
markerStroke: this.getCSSVar('--vmap-marker-stroke'),
|
|
hoverColor: this.getCSSVar('--vmap-hover-color'),
|
|
selectedColor: this.getCSSVar('--vmap-selected-color'),
|
|
scaleStart: this.getCSSVar('--vmap-scale-start'),
|
|
scaleEnd: this.getCSSVar('--vmap-scale-end'),
|
|
scaleLight: this.getCSSVar('--vmap-scale-light'),
|
|
scaleDark: this.getCSSVar('--vmap-scale-dark')
|
|
};
|
|
},
|
|
/**
|
|
* Get theme-aware colors for sparkline charts
|
|
*
|
|
* @returns {Object} Sparkline color configuration
|
|
*/
|
|
getSparklineColors() {
|
|
return {
|
|
success: this.getCSSVar('--sparkline-success'),
|
|
purple: this.getCSSVar('--sparkline-purple'),
|
|
info: this.getCSSVar('--sparkline-info'),
|
|
danger: this.getCSSVar('--sparkline-danger'),
|
|
light: this.getCSSVar('--sparkline-light')
|
|
};
|
|
},
|
|
/**
|
|
* Get theme-aware colors for Chart.js charts
|
|
*
|
|
* @returns {Object} Chart color configuration
|
|
* @property {string} textColor - Main text color
|
|
* @property {string} mutedColor - Muted/secondary text color
|
|
* @property {string} borderColor - Border color
|
|
* @property {string} gridColor - Grid line color
|
|
* @property {string} tooltipBg - Tooltip background color
|
|
*/
|
|
getChartColors() {
|
|
const isDark = this.current() === 'dark';
|
|
return {
|
|
textColor: isDark ? '#FFFFFF' : '#212529',
|
|
mutedColor: isDark ? '#D1D5DB' : '#6C757D',
|
|
borderColor: isDark ? '#374151' : '#E2E5E8',
|
|
gridColor: isDark ? 'rgba(209, 213, 219, 0.15)' : 'rgba(0, 0, 0, 0.05)',
|
|
tooltipBg: isDark ? '#1F2937' : 'rgba(255, 255, 255, 0.95)'
|
|
};
|
|
},
|
|
/**
|
|
* Check if dark mode is currently active
|
|
*
|
|
* @returns {boolean} True if dark mode is active
|
|
*
|
|
* @example
|
|
* if (Theme.isDark()) {
|
|
* // Apply dark-specific styles
|
|
* }
|
|
*/
|
|
isDark() {
|
|
return this.current() === 'dark';
|
|
},
|
|
/**
|
|
* Check if light mode is currently active
|
|
*
|
|
* @returns {boolean} True if light mode is active
|
|
*/
|
|
isLight() {
|
|
return this.current() === 'light';
|
|
}
|
|
};
|
|
/* harmony default export */ const theme = (Theme);
|
|
;// ../assets/scripts/utils/events.js
|
|
/**
|
|
* Adminator Event Utilities
|
|
* Provides efficient event handling with delegation and cleanup
|
|
*
|
|
* @module utils/events
|
|
*/
|
|
|
|
/**
|
|
* Store for event handlers to enable proper cleanup
|
|
* @type {WeakMap<Element, Map<string, Set<Function>>>}
|
|
*/
|
|
const handlerRegistry = new WeakMap();
|
|
|
|
/**
|
|
* Store for AbortControllers to enable easy cleanup
|
|
* @type {WeakMap<Element, AbortController>}
|
|
*/
|
|
const controllerRegistry = new WeakMap();
|
|
|
|
/**
|
|
* Event utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Events = {
|
|
/**
|
|
* Add an event listener with automatic cleanup support
|
|
* Uses AbortController for efficient removal
|
|
*
|
|
* @param {Element} element - Target element
|
|
* @param {string} event - Event type
|
|
* @param {Function} handler - Event handler
|
|
* @param {Object} [options={}] - Event listener options
|
|
* @returns {Function} Cleanup function to remove the listener
|
|
*
|
|
* @example
|
|
* const cleanup = Events.on(button, 'click', handleClick);
|
|
* // Later: cleanup() to remove
|
|
*/
|
|
on(element, event, handler) {
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
if (!element) return () => {};
|
|
|
|
// Get or create AbortController for this element
|
|
let controller = controllerRegistry.get(element);
|
|
if (!controller) {
|
|
controller = new AbortController();
|
|
controllerRegistry.set(element, controller);
|
|
}
|
|
|
|
// Register handler
|
|
if (!handlerRegistry.has(element)) {
|
|
handlerRegistry.set(element, new Map());
|
|
}
|
|
const elementHandlers = handlerRegistry.get(element);
|
|
if (!elementHandlers.has(event)) {
|
|
elementHandlers.set(event, new Set());
|
|
}
|
|
elementHandlers.get(event).add(handler);
|
|
|
|
// Add listener with abort signal
|
|
element.addEventListener(event, handler, {
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
var _handlerRegistry$get;
|
|
element.removeEventListener(event, handler, options);
|
|
const handlers = (_handlerRegistry$get = handlerRegistry.get(element)) === null || _handlerRegistry$get === void 0 ? void 0 : _handlerRegistry$get.get(event);
|
|
if (handlers) {
|
|
handlers.delete(handler);
|
|
}
|
|
};
|
|
},
|
|
/**
|
|
* Remove all event listeners from an element
|
|
*
|
|
* @param {Element} element - Target element
|
|
*
|
|
* @example
|
|
* Events.off(element); // Removes all listeners
|
|
*/
|
|
off(element) {
|
|
if (!element) return;
|
|
const controller = controllerRegistry.get(element);
|
|
if (controller) {
|
|
controller.abort();
|
|
controllerRegistry.delete(element);
|
|
}
|
|
handlerRegistry.delete(element);
|
|
},
|
|
/**
|
|
* Add event delegation - listen on parent for events from children
|
|
* More efficient than adding listeners to many elements
|
|
*
|
|
* @param {Element} parent - Parent element to listen on
|
|
* @param {string} event - Event type
|
|
* @param {string} selector - CSS selector for target elements
|
|
* @param {Function} handler - Event handler (receives event and matched element)
|
|
* @param {Object} [options={}] - Event listener options
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* // Instead of adding click to every .btn
|
|
* Events.delegate(container, 'click', '.btn', (e, btn) => {
|
|
* console.log('Button clicked:', btn);
|
|
* });
|
|
*/
|
|
delegate(parent, event, selector, handler) {
|
|
let options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
|
|
if (!parent) return () => {};
|
|
const delegatedHandler = e => {
|
|
const target = e.target.closest(selector);
|
|
if (target && parent.contains(target)) {
|
|
handler.call(target, e, target);
|
|
}
|
|
};
|
|
return this.on(parent, event, delegatedHandler, options);
|
|
},
|
|
/**
|
|
* Add a one-time event listener
|
|
*
|
|
* @param {Element} element - Target element
|
|
* @param {string} event - Event type
|
|
* @param {Function} handler - Event handler
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* Events.once(button, 'click', handleFirstClick);
|
|
*/
|
|
once(element, event, handler) {
|
|
return this.on(element, event, handler, {
|
|
once: true
|
|
});
|
|
},
|
|
/**
|
|
* Create a debounced event handler
|
|
*
|
|
* @param {Function} handler - Original handler
|
|
* @param {number} [delay=250] - Debounce delay in ms
|
|
* @returns {Function} Debounced handler
|
|
*
|
|
* @example
|
|
* const debouncedResize = Events.debounce(handleResize, 200);
|
|
* window.addEventListener('resize', debouncedResize);
|
|
*/
|
|
debounce(handler) {
|
|
let delay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 250;
|
|
let timeoutId;
|
|
return function debounced() {
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => handler.apply(this, args), delay);
|
|
};
|
|
},
|
|
/**
|
|
* Create a throttled event handler
|
|
*
|
|
* @param {Function} handler - Original handler
|
|
* @param {number} [limit=250] - Throttle limit in ms
|
|
* @returns {Function} Throttled handler
|
|
*
|
|
* @example
|
|
* const throttledScroll = Events.throttle(handleScroll, 100);
|
|
* window.addEventListener('scroll', throttledScroll);
|
|
*/
|
|
throttle(handler) {
|
|
let limit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 250;
|
|
let inThrottle;
|
|
return function throttled() {
|
|
if (!inThrottle) {
|
|
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
args[_key2] = arguments[_key2];
|
|
}
|
|
handler.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
},
|
|
/**
|
|
* Dispatch a custom event
|
|
*
|
|
* @param {Element|Window} target - Target to dispatch on
|
|
* @param {string} eventName - Event name
|
|
* @param {Object} [detail={}] - Event detail data
|
|
* @param {Object} [options={}] - Event options (bubbles, cancelable)
|
|
* @returns {boolean} Whether the event was not cancelled
|
|
*
|
|
* @example
|
|
* Events.emit(element, 'custom:event', { data: 'value' });
|
|
*/
|
|
emit(target, eventName) {
|
|
let detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
const event = new CustomEvent(eventName, {
|
|
detail,
|
|
bubbles: options.bubbles ?? true,
|
|
cancelable: options.cancelable ?? true
|
|
});
|
|
return target.dispatchEvent(event);
|
|
}
|
|
};
|
|
/* harmony default export */ const events = (Events);
|
|
;// ../assets/scripts/utils/performance.js
|
|
/**
|
|
* Adminator Performance Utilities
|
|
* Provides ResizeObserver, IntersectionObserver, and lazy loading utilities
|
|
*
|
|
* @module utils/performance
|
|
*/
|
|
|
|
/**
|
|
* Store for ResizeObserver instances
|
|
* @type {WeakMap<Element, Function>}
|
|
*/
|
|
const resizeCallbacks = new WeakMap();
|
|
|
|
/**
|
|
* Shared ResizeObserver instance for efficiency
|
|
* @type {ResizeObserver|null}
|
|
*/
|
|
let sharedResizeObserver = null;
|
|
|
|
/**
|
|
* Store for IntersectionObserver callbacks
|
|
* @type {WeakMap<Element, Function>}
|
|
*/
|
|
const intersectionCallbacks = new WeakMap();
|
|
|
|
/**
|
|
* Map of IntersectionObservers by threshold
|
|
* @type {Map<string, IntersectionObserver>}
|
|
*/
|
|
const intersectionObservers = new Map();
|
|
|
|
/**
|
|
* Performance utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Performance = {
|
|
/**
|
|
* Observe element resize events efficiently
|
|
* Uses shared ResizeObserver for better performance
|
|
*
|
|
* @param {Element} element - Element to observe
|
|
* @param {Function} callback - Callback receiving { width, height, entry }
|
|
* @returns {Function} Cleanup function to stop observing
|
|
*
|
|
* @example
|
|
* const unobserve = Performance.onResize(chart, ({ width, height }) => {
|
|
* chart.resize(width, height);
|
|
* });
|
|
*/
|
|
onResize(element, callback) {
|
|
if (!element || typeof callback !== 'function') {
|
|
return () => {};
|
|
}
|
|
|
|
// Create shared observer if needed
|
|
if (!sharedResizeObserver) {
|
|
sharedResizeObserver = new ResizeObserver(entries => {
|
|
for (const entry of entries) {
|
|
const cb = resizeCallbacks.get(entry.target);
|
|
if (cb) {
|
|
const {
|
|
width,
|
|
height
|
|
} = entry.contentRect;
|
|
cb({
|
|
width,
|
|
height,
|
|
entry
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Store callback and observe
|
|
resizeCallbacks.set(element, callback);
|
|
sharedResizeObserver.observe(element);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
var _sharedResizeObserver;
|
|
resizeCallbacks.delete(element);
|
|
(_sharedResizeObserver = sharedResizeObserver) === null || _sharedResizeObserver === void 0 || _sharedResizeObserver.unobserve(element);
|
|
};
|
|
},
|
|
/**
|
|
* Observe when element enters/exits viewport
|
|
* Useful for lazy loading and animations
|
|
*
|
|
* @param {Element} element - Element to observe
|
|
* @param {Function} callback - Callback receiving { isIntersecting, entry }
|
|
* @param {Object} [options={}] - IntersectionObserver options
|
|
* @param {number} [options.threshold=0] - Visibility threshold (0-1)
|
|
* @param {string} [options.rootMargin='0px'] - Root margin
|
|
* @returns {Function} Cleanup function to stop observing
|
|
*
|
|
* @example
|
|
* const unobserve = Performance.onVisible(element, ({ isIntersecting }) => {
|
|
* if (isIntersecting) {
|
|
* loadContent();
|
|
* unobserve(); // Stop after first trigger
|
|
* }
|
|
* });
|
|
*/
|
|
onVisible(element, callback) {
|
|
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
if (!element || typeof callback !== 'function') {
|
|
return () => {};
|
|
}
|
|
const {
|
|
threshold = 0,
|
|
rootMargin = '0px'
|
|
} = options;
|
|
const key = `${threshold}-${rootMargin}`;
|
|
|
|
// Get or create observer for this threshold
|
|
if (!intersectionObservers.has(key)) {
|
|
const observer = new IntersectionObserver(entries => {
|
|
for (const entry of entries) {
|
|
const cb = intersectionCallbacks.get(entry.target);
|
|
if (cb) {
|
|
cb({
|
|
isIntersecting: entry.isIntersecting,
|
|
ratio: entry.intersectionRatio,
|
|
entry
|
|
});
|
|
}
|
|
}
|
|
}, {
|
|
threshold,
|
|
rootMargin
|
|
});
|
|
intersectionObservers.set(key, observer);
|
|
}
|
|
const observer = intersectionObservers.get(key);
|
|
|
|
// Store callback and observe
|
|
intersectionCallbacks.set(element, callback);
|
|
observer.observe(element);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
intersectionCallbacks.delete(element);
|
|
observer.unobserve(element);
|
|
};
|
|
},
|
|
/**
|
|
* Lazy load an element when it becomes visible
|
|
* Automatically handles cleanup after loading
|
|
*
|
|
* @param {Element} element - Element to lazy load
|
|
* @param {Function} loadFn - Function to call when visible
|
|
* @param {Object} [options={}] - Options
|
|
* @param {string} [options.rootMargin='100px'] - Preload margin
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* Performance.lazyLoad(chartContainer, () => {
|
|
* initializeChart(chartContainer);
|
|
* });
|
|
*/
|
|
lazyLoad(element, loadFn) {
|
|
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
const {
|
|
rootMargin = '100px'
|
|
} = options;
|
|
let loaded = false;
|
|
const unobserve = this.onVisible(element, _ref => {
|
|
let {
|
|
isIntersecting
|
|
} = _ref;
|
|
if (isIntersecting && !loaded) {
|
|
loaded = true;
|
|
loadFn();
|
|
unobserve();
|
|
}
|
|
}, {
|
|
rootMargin
|
|
});
|
|
return unobserve;
|
|
},
|
|
/**
|
|
* Batch DOM reads and writes to prevent layout thrashing
|
|
*
|
|
* @param {Function} readFn - Function that reads from DOM
|
|
* @param {Function} writeFn - Function that writes to DOM
|
|
*
|
|
* @example
|
|
* Performance.batch(
|
|
* () => element.offsetHeight, // Read
|
|
* (height) => element.style.minHeight = height + 'px' // Write
|
|
* );
|
|
*/
|
|
batch(readFn, writeFn) {
|
|
// Use requestAnimationFrame for batching
|
|
requestAnimationFrame(() => {
|
|
const value = readFn();
|
|
requestAnimationFrame(() => {
|
|
writeFn(value);
|
|
});
|
|
});
|
|
},
|
|
/**
|
|
* Execute callback on next animation frame
|
|
*
|
|
* @param {Function} callback - Function to execute
|
|
* @returns {number} Request ID for cancellation
|
|
*
|
|
* @example
|
|
* const id = Performance.nextFrame(() => updateUI());
|
|
* // Cancel: cancelAnimationFrame(id);
|
|
*/
|
|
nextFrame(callback) {
|
|
return requestAnimationFrame(callback);
|
|
},
|
|
/**
|
|
* Execute callback when browser is idle
|
|
* Falls back to setTimeout if requestIdleCallback not available
|
|
*
|
|
* @param {Function} callback - Function to execute
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.timeout=1000] - Maximum wait time
|
|
* @returns {number} Request ID for cancellation
|
|
*
|
|
* @example
|
|
* Performance.whenIdle(() => {
|
|
* // Non-critical work
|
|
* preloadNextPage();
|
|
* });
|
|
*/
|
|
whenIdle(callback) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
const {
|
|
timeout = 1000
|
|
} = options;
|
|
if ('requestIdleCallback' in window) {
|
|
return requestIdleCallback(callback, {
|
|
timeout
|
|
});
|
|
}
|
|
|
|
// Fallback for Safari
|
|
return setTimeout(callback, 1);
|
|
},
|
|
/**
|
|
* Cancel an idle callback
|
|
*
|
|
* @param {number} id - Request ID from whenIdle
|
|
*/
|
|
cancelIdle(id) {
|
|
if ('cancelIdleCallback' in window) {
|
|
cancelIdleCallback(id);
|
|
} else {
|
|
clearTimeout(id);
|
|
}
|
|
},
|
|
/**
|
|
* Preload a resource (image, script, etc.)
|
|
*
|
|
* @param {string} url - URL to preload
|
|
* @param {string} [as='image'] - Resource type (image, script, style, font)
|
|
* @returns {Promise<void>} Resolves when loaded
|
|
*
|
|
* @example
|
|
* await Performance.preload('/images/hero.jpg', 'image');
|
|
*/
|
|
preload(url) {
|
|
let as = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'image';
|
|
return new Promise((resolve, reject) => {
|
|
const link = document.createElement('link');
|
|
link.rel = 'preload';
|
|
link.as = as;
|
|
link.href = url;
|
|
link.onload = resolve;
|
|
link.onerror = reject;
|
|
document.head.appendChild(link);
|
|
});
|
|
},
|
|
/**
|
|
* Measure execution time of a function
|
|
*
|
|
* @param {string} label - Label for the measurement
|
|
* @param {Function} fn - Function to measure
|
|
* @returns {*} Return value of the function
|
|
*
|
|
* @example
|
|
* const result = Performance.measure('render', () => renderChart());
|
|
*/
|
|
measure(label, fn) {
|
|
const start = performance.now();
|
|
const result = fn();
|
|
const end = performance.now();
|
|
if (false) // removed by dead control flow
|
|
{}
|
|
return result;
|
|
},
|
|
/**
|
|
* Cleanup all observers
|
|
* Call this when destroying the app
|
|
*/
|
|
cleanup() {
|
|
// Cleanup resize observer
|
|
if (sharedResizeObserver) {
|
|
sharedResizeObserver.disconnect();
|
|
sharedResizeObserver = null;
|
|
}
|
|
|
|
// Cleanup intersection observers
|
|
for (const observer of intersectionObservers.values()) {
|
|
observer.disconnect();
|
|
}
|
|
intersectionObservers.clear();
|
|
}
|
|
};
|
|
/* harmony default export */ const utils_performance = (Performance);
|
|
;// ../assets/scripts/utils/logger.js
|
|
/**
|
|
* Adminator Logger Utility
|
|
* Development-only logging utility for debugging
|
|
*
|
|
* @module utils/logger
|
|
*/
|
|
|
|
/**
|
|
* Check if we're in development mode
|
|
* @returns {boolean}
|
|
*/
|
|
const isDev = () => {
|
|
try {
|
|
return false || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Logger object with development-only output
|
|
* All methods are no-ops in production for zero overhead
|
|
*/
|
|
const Logger = {
|
|
/**
|
|
* Log informational messages (development only)
|
|
* @param {string} message - The message to log
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
info(message, context) {
|
|
if (isDev()) {
|
|
console.info(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log warning messages (development only)
|
|
* @param {string} message - The warning message
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
warn(message, context) {
|
|
if (isDev()) {
|
|
console.warn(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log error messages (development only)
|
|
* @param {string} message - The error message
|
|
* @param {Error|Object} [context] - Error object or context data
|
|
*/
|
|
error(message, context) {
|
|
if (isDev()) {
|
|
console.error(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log debug messages (development only)
|
|
* @param {string} message - The debug message
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
debug(message, context) {
|
|
if (isDev()) {
|
|
console.debug(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Group related log messages (development only)
|
|
* @param {string} label - Group label
|
|
*/
|
|
group(label) {
|
|
if (isDev()) {
|
|
console.group(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* End a log group (development only)
|
|
*/
|
|
groupEnd() {
|
|
if (isDev()) {
|
|
console.groupEnd();
|
|
}
|
|
},
|
|
/**
|
|
* Log with timing information (development only)
|
|
* @param {string} label - Timer label
|
|
*/
|
|
time(label) {
|
|
if (isDev()) {
|
|
console.time(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* End timing and log result (development only)
|
|
* @param {string} label - Timer label (must match time() call)
|
|
*/
|
|
timeEnd(label) {
|
|
if (isDev()) {
|
|
console.timeEnd(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* Log a table of data (development only)
|
|
* @param {Array|Object} data - Data to display as table
|
|
*/
|
|
table(data) {
|
|
if (isDev()) {
|
|
console.table(data);
|
|
}
|
|
}
|
|
};
|
|
/* harmony default export */ const logger = (Logger);
|
|
;// ../assets/scripts/components/Sidebar.js
|
|
/**
|
|
* Modern Sidebar Component
|
|
* Replaces jQuery-based sidebar functionality with vanilla JavaScript
|
|
*/
|
|
|
|
class Sidebar {
|
|
constructor() {
|
|
this.sidebar = document.querySelector('.sidebar');
|
|
this.sidebarMenu = document.querySelector('.sidebar .sidebar-menu');
|
|
this.sidebarToggleLinks = document.querySelectorAll('.sidebar-toggle a');
|
|
this.sidebarToggleById = document.querySelector('#sidebar-toggle');
|
|
this.app = document.querySelector('.app');
|
|
this.init();
|
|
}
|
|
init() {
|
|
if (!this.sidebar || !this.sidebarMenu) {
|
|
return;
|
|
}
|
|
this.setupMenuToggle();
|
|
this.setupSidebarToggle();
|
|
this.setActiveLink();
|
|
}
|
|
|
|
/**
|
|
* Setup dropdown menu functionality
|
|
*/
|
|
setupMenuToggle() {
|
|
const menuLinks = this.sidebarMenu.querySelectorAll('li a');
|
|
menuLinks.forEach(link => {
|
|
link.addEventListener('click', e => {
|
|
const listItem = link.parentElement;
|
|
const dropdownMenu = listItem.querySelector('.dropdown-menu');
|
|
|
|
// If this is a regular navigation link (not dropdown), allow normal navigation
|
|
if (!dropdownMenu) {
|
|
// Don't prevent default for regular navigation links
|
|
return;
|
|
}
|
|
|
|
// Only prevent default for dropdown toggles
|
|
e.preventDefault();
|
|
if (listItem.classList.contains('open')) {
|
|
this.closeDropdown(listItem, dropdownMenu);
|
|
} else {
|
|
this.closeAllDropdowns();
|
|
this.openDropdown(listItem, dropdownMenu);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open dropdown with smooth animation
|
|
*/
|
|
openDropdown(listItem, dropdownMenu) {
|
|
listItem.classList.add('open');
|
|
dropdownMenu.style.display = 'block';
|
|
dropdownMenu.style.height = '0px';
|
|
dropdownMenu.style.overflow = 'hidden';
|
|
|
|
// Get the natural height
|
|
const height = dropdownMenu.scrollHeight;
|
|
|
|
// Animate to full height
|
|
dropdownMenu.animate([{
|
|
height: '0px'
|
|
}, {
|
|
height: `${height}px`
|
|
}], {
|
|
duration: 200,
|
|
easing: 'ease-out'
|
|
}).onfinish = () => {
|
|
dropdownMenu.style.height = 'auto';
|
|
dropdownMenu.style.overflow = 'visible';
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close dropdown with smooth animation
|
|
*/
|
|
closeDropdown(listItem, dropdownMenu) {
|
|
const height = dropdownMenu.scrollHeight;
|
|
dropdownMenu.style.height = `${height}px`;
|
|
dropdownMenu.style.overflow = 'hidden';
|
|
dropdownMenu.animate([{
|
|
height: `${height}px`
|
|
}, {
|
|
height: '0px'
|
|
}], {
|
|
duration: 200,
|
|
easing: 'ease-in'
|
|
}).onfinish = () => {
|
|
listItem.classList.remove('open');
|
|
dropdownMenu.style.display = 'none';
|
|
dropdownMenu.style.height = '';
|
|
dropdownMenu.style.overflow = '';
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close all open dropdowns
|
|
*/
|
|
closeAllDropdowns() {
|
|
const openItems = this.sidebarMenu.querySelectorAll('li.open');
|
|
openItems.forEach(item => {
|
|
const dropdownMenu = item.querySelector('.dropdown-menu');
|
|
if (dropdownMenu) {
|
|
this.closeDropdown(item, dropdownMenu);
|
|
}
|
|
|
|
// Also remove the has-active-child class
|
|
item.classList.remove('has-active-child');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup sidebar toggle functionality
|
|
*/
|
|
setupSidebarToggle() {
|
|
// Handle mobile sidebar toggle links (inside .sidebar-toggle divs)
|
|
this.sidebarToggleLinks.forEach(link => {
|
|
if (link && this.app) {
|
|
link.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggleSidebar();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle the main topbar sidebar toggle
|
|
if (this.sidebarToggleById && this.app) {
|
|
this.sidebarToggleById.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggleSidebar();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle sidebar and handle resize events properly
|
|
*/
|
|
toggleSidebar() {
|
|
this.app.classList.toggle('is-collapsed');
|
|
|
|
// Only trigger resize for masonry, but avoid chart redraw issues
|
|
setTimeout(() => {
|
|
// Dispatch a custom event instead of generic resize to avoid chart issues
|
|
window.dispatchEvent(new CustomEvent('sidebar:toggle', {
|
|
detail: {
|
|
collapsed: this.app.classList.contains('is-collapsed')
|
|
}
|
|
}));
|
|
|
|
// Still trigger resize for masonry but with a specific check
|
|
if (window.EVENT) {
|
|
window.dispatchEvent(window.EVENT);
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* Set active link based on current URL
|
|
*/
|
|
setActiveLink() {
|
|
// Remove active class from all nav items (including dropdown items)
|
|
const allNavItems = this.sidebar.querySelectorAll('.nav-item');
|
|
allNavItems.forEach(item => {
|
|
item.classList.remove('actived');
|
|
});
|
|
|
|
// Close all dropdowns first
|
|
this.closeAllDropdowns();
|
|
|
|
// Get current page filename
|
|
const currentPath = window.location.pathname;
|
|
const currentPage = currentPath.split('/').pop() || 'index.html';
|
|
|
|
// Find and activate the correct nav item
|
|
const allLinks = this.sidebar.querySelectorAll('a[href]');
|
|
allLinks.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (!href || href === 'javascript:void(0);' || href === 'javascript:void(0)') return;
|
|
|
|
// Extract filename from href
|
|
const linkPage = href.split('/').pop();
|
|
if (linkPage === currentPage) {
|
|
const navItem = link.closest('.nav-item');
|
|
if (navItem) {
|
|
navItem.classList.add('actived');
|
|
|
|
// If this is inside a dropdown, handle parent dropdown specially
|
|
const parentDropdown = navItem.closest('.dropdown-menu');
|
|
if (parentDropdown) {
|
|
const parentDropdownItem = parentDropdown.closest('.nav-item.dropdown');
|
|
if (parentDropdownItem) {
|
|
// Open the parent dropdown
|
|
parentDropdownItem.classList.add('open');
|
|
parentDropdown.style.display = 'block';
|
|
|
|
// Add special styling to indicate parent has active child
|
|
parentDropdownItem.classList.add('has-active-child');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Public method to refresh active links (useful for SPA navigation)
|
|
*/
|
|
refreshActiveLink() {
|
|
this.setActiveLink();
|
|
}
|
|
|
|
/**
|
|
* Public method to toggle sidebar programmatically
|
|
*/
|
|
toggle() {
|
|
if (this.app) {
|
|
this.app.classList.toggle('is-collapsed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Public method to check if sidebar is collapsed
|
|
*/
|
|
isCollapsed() {
|
|
return this.app ? this.app.classList.contains('is-collapsed') : false;
|
|
}
|
|
}
|
|
/* harmony default export */ const components_Sidebar = (Sidebar);
|
|
// EXTERNAL MODULE: ../../node_modules/chart.js/dist/chart.js + 1 modules
|
|
var dist_chart = __webpack_require__(371);
|
|
;// ../assets/scripts/constants/colors.js
|
|
const COLORS = {
|
|
'white': '#ffffff',
|
|
'red-50': '#ffebee',
|
|
'red-100': '#ffcdd2',
|
|
'red-200': '#ef9a9a',
|
|
'red-300': '#e57373',
|
|
'red-400': '#ef5350',
|
|
'red-500': '#f44336',
|
|
'red-600': '#e53935',
|
|
'red-700': '#d32f2f',
|
|
'red-800': '#c62828',
|
|
'red-900': '#b71c1c',
|
|
'red-a100': '#ff8a80',
|
|
'red-a200': '#ff5252',
|
|
'red-a400': '#ff1744',
|
|
'red-a700': '#d50000',
|
|
'pink-50': '#fce4ec',
|
|
'pink-100': '#f8bbd0',
|
|
'pink-200': '#f48fb1',
|
|
'pink-300': '#f06292',
|
|
'pink-400': '#ec407a',
|
|
'pink-500': '#e91e63',
|
|
'pink-600': '#d81b60',
|
|
'pink-700': '#c2185b',
|
|
'pink-800': '#ad1457',
|
|
'pink-900': '#880e4f',
|
|
'pink-a100': '#ff80ab',
|
|
'pink-a200': '#ff4081',
|
|
'pink-a400': '#f50057',
|
|
'pink-a700': '#c51162',
|
|
'purple-50': '#f3e5f5',
|
|
'purple-100': '#e1bee7',
|
|
'purple-200': '#ce93d8',
|
|
'purple-300': '#ba68c8',
|
|
'purple-400': '#ab47bc',
|
|
'purple-500': '#9c27b0',
|
|
'purple-600': '#8e24aa',
|
|
'purple-700': '#7b1fa2',
|
|
'purple-800': '#6a1b9a',
|
|
'purple-900': '#4a148c',
|
|
'purple-a100': '#ea80fc',
|
|
'purple-a200': '#e040fb',
|
|
'purple-a400': '#d500f9',
|
|
'purple-a700': '#aa00ff',
|
|
'deep-purple-50': '#ede7f6',
|
|
'deep-purple-100': '#d1c4e9',
|
|
'deep-purple-200': '#b39ddb',
|
|
'deep-purple-300': '#9575cd',
|
|
'deep-purple-400': '#7e57c2',
|
|
'deep-purple-500': '#673ab7',
|
|
'deep-purple-600': '#5e35b1',
|
|
'deep-purple-700': '#512da8',
|
|
'deep-purple-800': '#4527a0',
|
|
'deep-purple-900': '#311b92',
|
|
'deep-purple-a100': '#b388ff',
|
|
'deep-purple-a200': '#7c4dff',
|
|
'deep-purple-a400': '#651fff',
|
|
'deep-purple-a700': '#6200ea',
|
|
'indigo-50': '#e8eaf6',
|
|
'indigo-100': '#c5cae9',
|
|
'indigo-200': '#9fa8da',
|
|
'indigo-300': '#7986cb',
|
|
'indigo-400': '#5c6bc0',
|
|
'indigo-500': '#3f51b5',
|
|
'indigo-600': '#3949ab',
|
|
'indigo-700': '#303f9f',
|
|
'indigo-800': '#283593',
|
|
'indigo-900': '#1a237e',
|
|
'indigo-a100': '#8c9eff',
|
|
'indigo-a200': '#536dfe',
|
|
'indigo-a400': '#3d5afe',
|
|
'indigo-a700': '#304ffe',
|
|
'blue-50': '#e3f2fd',
|
|
'blue-100': '#bbdefb',
|
|
'blue-200': '#90caf9',
|
|
'blue-300': '#64b5f6',
|
|
'blue-400': '#42a5f5',
|
|
'blue-500': '#2196f3',
|
|
'blue-600': '#1e88e5',
|
|
'blue-700': '#1976d2',
|
|
'blue-800': '#1565c0',
|
|
'blue-900': '#0d47a1',
|
|
'blue-a100': '#82b1ff',
|
|
'blue-a200': '#448aff',
|
|
'blue-a400': '#2979ff',
|
|
'blue-a700': '#2962ff',
|
|
'light-blue-50': '#e1f5fe',
|
|
'light-blue-100': '#b3e5fc',
|
|
'light-blue-200': '#81d4fa',
|
|
'light-blue-300': '#4fc3f7',
|
|
'light-blue-400': '#29b6f6',
|
|
'light-blue-500': '#03a9f4',
|
|
'light-blue-600': '#039be5',
|
|
'light-blue-700': '#0288d1',
|
|
'light-blue-800': '#0277bd',
|
|
'light-blue-900': '#01579b',
|
|
'light-blue-a100': '#80d8ff',
|
|
'light-blue-a200': '#40c4ff',
|
|
'light-blue-a400': '#00b0ff',
|
|
'light-blue-a700': '#0091ea',
|
|
'cyan-50': '#e0f7fa',
|
|
'cyan-100': '#b2ebf2',
|
|
'cyan-200': '#80deea',
|
|
'cyan-300': '#4dd0e1',
|
|
'cyan-400': '#26c6da',
|
|
'cyan-500': '#00bcd4',
|
|
'cyan-600': '#00acc1',
|
|
'cyan-700': '#0097a7',
|
|
'cyan-800': '#00838f',
|
|
'cyan-900': '#006064',
|
|
'cyan-a100': '#84ffff',
|
|
'cyan-a200': '#18ffff',
|
|
'cyan-a400': '#00e5ff',
|
|
'cyan-a700': '#00b8d4',
|
|
'teal-50': '#e0f2f1',
|
|
'teal-100': '#b2dfdb',
|
|
'teal-200': '#80cbc4',
|
|
'teal-300': '#4db6ac',
|
|
'teal-400': '#26a69a',
|
|
'teal-500': '#009688',
|
|
'teal-600': '#00897b',
|
|
'teal-700': '#00796b',
|
|
'teal-800': '#00695c',
|
|
'teal-900': '#004d40',
|
|
'teal-a100': '#a7ffeb',
|
|
'teal-a200': '#64ffda',
|
|
'teal-a400': '#1de9b6',
|
|
'teal-a700': '#00bfa5',
|
|
'green-50': '#e8f5e9',
|
|
'green-100': '#c8e6c9',
|
|
'green-200': '#a5d6a7',
|
|
'green-300': '#81c784',
|
|
'green-400': '#66bb6a',
|
|
'green-500': '#4caf50',
|
|
'green-600': '#43a047',
|
|
'green-700': '#388e3c',
|
|
'green-800': '#2e7d32',
|
|
'green-900': '#1b5e20',
|
|
'green-a100': '#b9f6ca',
|
|
'green-a200': '#69f0ae',
|
|
'green-a400': '#00e676',
|
|
'green-a700': '#00c853',
|
|
'light-green-50': '#f1f8e9',
|
|
'light-green-100': '#dcedc8',
|
|
'light-green-200': '#c5e1a5',
|
|
'light-green-300': '#aed581',
|
|
'light-green-400': '#9ccc65',
|
|
'light-green-500': '#8bc34a',
|
|
'light-green-600': '#7cb342',
|
|
'light-green-700': '#689f38',
|
|
'light-green-800': '#558b2f',
|
|
'light-green-900': '#33691e',
|
|
'light-green-a100': '#ccff90',
|
|
'light-green-a200': '#b2ff59',
|
|
'light-green-a400': '#76ff03',
|
|
'light-green-a700': '#64dd17',
|
|
'lime-50': '#f9fbe7',
|
|
'lime-100': '#f0f4c3',
|
|
'lime-200': '#e6ee9c',
|
|
'lime-300': '#dce775',
|
|
'lime-400': '#d4e157',
|
|
'lime-500': '#cddc39',
|
|
'lime-600': '#c0ca33',
|
|
'lime-700': '#afb42b',
|
|
'lime-800': '#9e9d24',
|
|
'lime-900': '#827717',
|
|
'lime-a100': '#f4ff81',
|
|
'lime-a200': '#eeff41',
|
|
'lime-a400': '#c6ff00',
|
|
'lime-a700': '#aeea00',
|
|
'yellow-50': '#fffde7',
|
|
'yellow-100': '#fff9c4',
|
|
'yellow-200': '#fff59d',
|
|
'yellow-300': '#fff176',
|
|
'yellow-400': '#ffee58',
|
|
'yellow-500': '#ffeb3b',
|
|
'yellow-600': '#fdd835',
|
|
'yellow-700': '#fbc02d',
|
|
'yellow-800': '#f9a825',
|
|
'yellow-900': '#f57f17',
|
|
'yellow-a100': '#ffff8d',
|
|
'yellow-a200': '#ffff00',
|
|
'yellow-a400': '#ffea00',
|
|
'yellow-a700': '#ffd600',
|
|
'amber-50': '#fff8e1',
|
|
'amber-100': '#ffecb3',
|
|
'amber-200': '#ffe082',
|
|
'amber-300': '#ffd54f',
|
|
'amber-400': '#ffca28',
|
|
'amber-500': '#ffc107',
|
|
'amber-600': '#ffb300',
|
|
'amber-700': '#ffa000',
|
|
'amber-800': '#ff8f00',
|
|
'amber-900': '#ff6f00',
|
|
'amber-a100': '#ffe57f',
|
|
'amber-a200': '#ffd740',
|
|
'amber-a400': '#ffc400',
|
|
'amber-a700': '#ffab00',
|
|
'orange-50': '#fff3e0',
|
|
'orange-100': '#ffe0b2',
|
|
'orange-200': '#ffcc80',
|
|
'orange-300': '#ffb74d',
|
|
'orange-400': '#ffa726',
|
|
'orange-500': '#ff9800',
|
|
'orange-600': '#fb8c00',
|
|
'orange-700': '#f57c00',
|
|
'orange-800': '#ef6c00',
|
|
'orange-900': '#e65100',
|
|
'orange-a100': '#ffd180',
|
|
'orange-a200': '#ffab40',
|
|
'orange-a400': '#ff9100',
|
|
'orange-a700': '#ff6d00',
|
|
'deep-orange-50': '#fbe9e7',
|
|
'deep-orange-100': '#ffccbc',
|
|
'deep-orange-200': '#ffab91',
|
|
'deep-orange-300': '#ff8a65',
|
|
'deep-orange-400': '#ff7043',
|
|
'deep-orange-500': '#ff5722',
|
|
'deep-orange-600': '#f4511e',
|
|
'deep-orange-700': '#e64a19',
|
|
'deep-orange-800': '#d84315',
|
|
'deep-orange-900': '#bf360c',
|
|
'deep-orange-a100': '#ff9e80',
|
|
'deep-orange-a200': '#ff6e40',
|
|
'deep-orange-a400': '#ff3d00',
|
|
'deep-orange-a700': '#dd2c00',
|
|
'brown-50': '#efebe9',
|
|
'brown-100': '#d7ccc8',
|
|
'brown-200': '#bcaaa4',
|
|
'brown-300': '#a1887f',
|
|
'brown-400': '#8d6e63',
|
|
'brown-500': '#795548',
|
|
'brown-600': '#6d4c41',
|
|
'brown-700': '#5d4037',
|
|
'brown-800': '#4e342e',
|
|
'brown-900': '#3e2723',
|
|
'grey-50': '#fafafa',
|
|
'grey-100': '#f5f5f5',
|
|
'grey-200': '#eeeeee',
|
|
'grey-300': '#e0e0e0',
|
|
'grey-400': '#bdbdbd',
|
|
'grey-500': '#9e9e9e',
|
|
'grey-600': '#757575',
|
|
'grey-700': '#616161',
|
|
'grey-800': '#424242',
|
|
'grey-900': '#212121',
|
|
'blue-grey-50': '#eceff1',
|
|
'blue-grey-100': '#cfd8dc',
|
|
'blue-grey-200': '#b0bec5',
|
|
'blue-grey-300': '#90a4ae',
|
|
'blue-grey-400': '#78909c',
|
|
'blue-grey-500': '#607d8b',
|
|
'blue-grey-600': '#546e7a',
|
|
'blue-grey-700': '#455a64',
|
|
'blue-grey-800': '#37474f',
|
|
'blue-grey-900': '#263238'
|
|
};
|
|
const GREYS = {
|
|
'grey-100': '#f9fafb',
|
|
'grey-200': '#f2f3f5',
|
|
'grey-300': '#e6eaf0',
|
|
'grey-400': '#d3d9e3',
|
|
'grey-500': '#b9c2d0',
|
|
'grey-600': '#7c8695',
|
|
'grey-700': '#72777a',
|
|
'grey-800': '#565a5c',
|
|
'grey-900': '#313435'
|
|
};
|
|
|
|
;// ../assets/scripts/components/Chart.js
|
|
/**
|
|
* Modern Chart Component
|
|
* Replaces jQuery Sparkline with Chart.js
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Register Chart.js components
|
|
dist_chart/* Chart */.t1.register(...dist_chart/* registerables */.$L);
|
|
class ChartComponent {
|
|
constructor() {
|
|
this.charts = new Map(); // Store chart instances
|
|
this.debounceTimer = null;
|
|
this.init();
|
|
}
|
|
init() {
|
|
// Only disable resizing for small sparkline charts
|
|
this.createSparklines();
|
|
this.createOtherCharts();
|
|
this.setupResizeHandler();
|
|
}
|
|
|
|
/**
|
|
* Create sparklines (only for dashboard page)
|
|
*/
|
|
createSparklines() {
|
|
// Only create sparklines if we're on a page that has them
|
|
const sparklineExists = document.getElementById('sparklinedash');
|
|
if (!sparklineExists) {
|
|
return;
|
|
}
|
|
const sparklineConfigs = [{
|
|
id: 'sparklinedash',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#4caf50'
|
|
}, {
|
|
id: 'sparklinedash2',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#9675ce'
|
|
}, {
|
|
id: 'sparklinedash3',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#03a9f3'
|
|
}, {
|
|
id: 'sparklinedash4',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#f96262'
|
|
}];
|
|
sparklineConfigs.forEach(config => {
|
|
// Only create if the target element exists
|
|
if (document.getElementById(config.id)) {
|
|
this.createSparklineChart(config);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create sparkline chart from configuration
|
|
*/
|
|
createSparklineChart(_ref) {
|
|
let {
|
|
id,
|
|
data,
|
|
color
|
|
} = _ref;
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if we have a valid target element
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace the span with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
// Set canvas dimensions to match original sparkline
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: color,
|
|
borderColor: color,
|
|
borderWidth: 0,
|
|
barPercentage: 0.6,
|
|
categoryPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
},
|
|
elements: {
|
|
bar: {
|
|
borderRadius: 1
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create other chart types (only if they exist on the page)
|
|
*/
|
|
createOtherCharts() {
|
|
// Determine if we're on the dashboard or charts page
|
|
const isChartsPage = document.getElementById('area-chart') !== null;
|
|
const isDashboard = !isChartsPage && document.getElementById('line-chart') !== null;
|
|
|
|
// Create Monthly Stats chart with enhanced dual-line data (dashboard only)
|
|
if (isDashboard) {
|
|
this.createMonthlyStatsChart();
|
|
}
|
|
|
|
// Charts page specific charts (only on charts page)
|
|
if (isChartsPage) {
|
|
this.createChartsPageCharts();
|
|
}
|
|
|
|
// Only create charts if their target elements exist
|
|
if (document.getElementById('sparkline')) {
|
|
this.createLineChart('sparkline', [5, 6, 7, 9, 9, 5, 3, 2, 2, 4, 6, 7]);
|
|
}
|
|
if (document.getElementById('compositebar')) {
|
|
this.createCompositeChart('compositebar', [4, 1, 5, 7, 9, 9, 8, 7, 6, 6, 4, 7, 8, 4, 3, 2, 2, 5, 6, 7]);
|
|
}
|
|
|
|
// Regular sparklines with custom colors (only on pages that have them)
|
|
this.createCustomSparklines();
|
|
|
|
// Easy Pie Charts (only if they exist)
|
|
this.createEasyPieCharts();
|
|
}
|
|
|
|
/**
|
|
* Create enhanced Monthly Stats chart with dual lines and more data
|
|
*/
|
|
createMonthlyStatsChart() {
|
|
const canvas = document.getElementById('line-chart');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Enhanced data for monthly stats
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
const salesData = [120, 135, 145, 165, 180, 195, 210, 225, 240, 220, 200, 185];
|
|
const profitData = [45, 52, 58, 62, 68, 75, 82, 88, 92, 85, 78, 72];
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: months,
|
|
datasets: [{
|
|
label: 'Sales ($K)',
|
|
data: salesData,
|
|
borderColor: '#4caf50',
|
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
borderWidth: 3,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7,
|
|
pointBackgroundColor: '#4caf50',
|
|
pointBorderColor: '#ffffff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
fill: false
|
|
}, {
|
|
label: 'Profit ($K)',
|
|
data: profitData,
|
|
borderColor: '#2196f3',
|
|
backgroundColor: 'rgba(33, 150, 243, 0.1)',
|
|
borderWidth: 3,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7,
|
|
pointBackgroundColor: '#2196f3',
|
|
pointBorderColor: '#ffffff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
fill: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: '600'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
cornerRadius: 8,
|
|
displayColors: true,
|
|
intersect: false,
|
|
mode: 'index',
|
|
callbacks: {
|
|
label(context) {
|
|
return `${context.dataset.label}: $${context.parsed.y}K`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
},
|
|
callback(value) {
|
|
return `$${value}K`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
}
|
|
}
|
|
});
|
|
this.charts.set('line-chart', chart);
|
|
}
|
|
|
|
/**
|
|
* Create line chart (only if target exists)
|
|
*/
|
|
createLineChart(id, data) {
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if target element exists
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace element with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
borderColor: COLORS['blue-500'],
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
pointRadius: 0,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create composite chart (only if target exists)
|
|
*/
|
|
createCompositeChart(id, data) {
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if target element exists
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace element with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
type: 'bar',
|
|
data,
|
|
backgroundColor: '#aaf',
|
|
borderColor: '#aaf',
|
|
borderWidth: 0
|
|
}, {
|
|
type: 'line',
|
|
data,
|
|
borderColor: 'red',
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
pointRadius: 0,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create custom sparklines for different elements (only if they exist)
|
|
*/
|
|
createCustomSparklines() {
|
|
const sparklineElements = document.querySelectorAll('.sparkline');
|
|
const sparkbarElements = document.querySelectorAll('.sparkbar');
|
|
const sparktriElements = document.querySelectorAll('.sparktri');
|
|
const sparkdiscElements = document.querySelectorAll('.sparkdisc');
|
|
const sparkbullElements = document.querySelectorAll('.sparkbull');
|
|
const sparkboxElements = document.querySelectorAll('.sparkbox');
|
|
|
|
// Only create if we have elements
|
|
if (sparklineElements.length === 0 && sparkbarElements.length === 0 && sparktriElements.length === 0 && sparkdiscElements.length === 0 && sparkbullElements.length === 0 && sparkboxElements.length === 0) {
|
|
return;
|
|
}
|
|
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];
|
|
sparklineElements.forEach((element, index) => {
|
|
this.createCustomLineChart(element, values, `sparkline-${index}`);
|
|
});
|
|
sparkbarElements.forEach((element, index) => {
|
|
this.createCustomBarChart(element, values, `sparkbar-${index}`);
|
|
});
|
|
sparktriElements.forEach((element, index) => {
|
|
this.createTristateChart(element, valuesAlt, `sparktri-${index}`);
|
|
});
|
|
sparkdiscElements.forEach((element, index) => {
|
|
this.createDiscreteChart(element, values, `sparkdisc-${index}`);
|
|
});
|
|
sparkbullElements.forEach((element, index) => {
|
|
this.createBulletChart(element, values, `sparkbull-${index}`);
|
|
});
|
|
sparkboxElements.forEach((element, index) => {
|
|
this.createBoxChart(element, values, `sparkbox-${index}`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create custom line chart for sparkline elements
|
|
*/
|
|
createCustomLineChart(element, data, id) {
|
|
// Create canvas if it doesn't exist
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
borderColor: COLORS['red-500'],
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: COLORS['red-500'],
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
// Disable animations to prevent resize triggers
|
|
events: [],
|
|
// Disable all events to prevent resize
|
|
onResize: null,
|
|
// Explicitly disable resize callback
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
} // Disable tooltip to prevent events
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create custom bar chart for sparkbar elements
|
|
*/
|
|
createCustomBarChart(element, data, id) {
|
|
// Create canvas if it doesn't exist
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: data.map(val => val < 0 ? COLORS['deep-purple-500'] : '#39f'),
|
|
borderColor: data.map(val => val < 0 ? COLORS['deep-purple-500'] : '#39f'),
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Setup resize handler for charts
|
|
*/
|
|
setupResizeHandler() {
|
|
// Setup responsive resize for large charts only
|
|
window.addEventListener('resize', () => {
|
|
this.debounceResize();
|
|
});
|
|
|
|
// Listen for sidebar toggle events
|
|
window.addEventListener('sidebar:toggle', () => {
|
|
this.debounceResize();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Debounced resize handler
|
|
*/
|
|
debounceResize() {
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
}
|
|
this.debounceTimer = setTimeout(() => {
|
|
this.redrawLargeChartsOnly();
|
|
}, 150);
|
|
}
|
|
|
|
/**
|
|
* Redraw only large charts, not sparklines
|
|
*/
|
|
redrawLargeChartsOnly() {
|
|
const largeChartIds = ['line-chart', 'area-chart', 'scatter-chart', 'bar-chart', 'doughnut-chart', 'polar-chart', 'radar-chart', 'mixed-chart', 'bubble-chart'];
|
|
largeChartIds.forEach(id => {
|
|
const chart = this.charts.get(id);
|
|
if (chart && chart.options.responsive) {
|
|
chart.resize();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Redraw all charts (used sparingly)
|
|
*/
|
|
redrawCharts() {
|
|
this.charts.forEach(chart => {
|
|
if (chart.options.responsive) {
|
|
chart.resize();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update chart data
|
|
*/
|
|
updateChart(id, newData) {
|
|
const chart = this.charts.get(id);
|
|
if (chart) {
|
|
chart.data.datasets[0].data = newData;
|
|
chart.update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create charts for the charts.html page
|
|
*/
|
|
createChartsPageCharts() {
|
|
// Line Chart
|
|
this.createLargeChart('line-chart', 'line', {
|
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [65, 59, 80, 81, 56, 55, 40],
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
tension: 0.4
|
|
}]
|
|
});
|
|
|
|
// Area Chart
|
|
this.createLargeChart('area-chart', 'line', {
|
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [65, 59, 80, 81, 56, 55, 40],
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.4)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
});
|
|
|
|
// Scatter Chart with more data points
|
|
this.createLargeChart('scatter-chart', 'scatter', {
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [{
|
|
x: -15,
|
|
y: 8
|
|
}, {
|
|
x: -12,
|
|
y: 12
|
|
}, {
|
|
x: -8,
|
|
y: 3
|
|
}, {
|
|
x: -5,
|
|
y: 15
|
|
}, {
|
|
x: -2,
|
|
y: 7
|
|
}, {
|
|
x: 0,
|
|
y: 10
|
|
}, {
|
|
x: 3,
|
|
y: 18
|
|
}, {
|
|
x: 6,
|
|
y: 5
|
|
}, {
|
|
x: 9,
|
|
y: 22
|
|
}, {
|
|
x: 12,
|
|
y: 8
|
|
}, {
|
|
x: 15,
|
|
y: 14
|
|
}, {
|
|
x: 18,
|
|
y: 19
|
|
}, {
|
|
x: -10,
|
|
y: 0
|
|
}, {
|
|
x: 10,
|
|
y: 5
|
|
}, {
|
|
x: 0.5,
|
|
y: 5.5
|
|
}, {
|
|
x: 7,
|
|
y: 12
|
|
}, {
|
|
x: -7,
|
|
y: 17
|
|
}, {
|
|
x: 4,
|
|
y: 9
|
|
}, {
|
|
x: 11,
|
|
y: 16
|
|
}, {
|
|
x: -3,
|
|
y: 11
|
|
}],
|
|
backgroundColor: 'rgba(255, 99, 132, 0.7)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 1
|
|
}, {
|
|
label: 'Dataset 2',
|
|
data: [{
|
|
x: -13,
|
|
y: 4
|
|
}, {
|
|
x: -9,
|
|
y: 8
|
|
}, {
|
|
x: -6,
|
|
y: 13
|
|
}, {
|
|
x: -1,
|
|
y: 6
|
|
}, {
|
|
x: 2,
|
|
y: 11
|
|
}, {
|
|
x: 5,
|
|
y: 15
|
|
}, {
|
|
x: 8,
|
|
y: 2
|
|
}, {
|
|
x: 13,
|
|
y: 17
|
|
}, {
|
|
x: 16,
|
|
y: 9
|
|
}, {
|
|
x: -4,
|
|
y: 14
|
|
}, {
|
|
x: 1,
|
|
y: 20
|
|
}, {
|
|
x: 14,
|
|
y: 4
|
|
}],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 1
|
|
}]
|
|
});
|
|
|
|
// Bar Chart
|
|
this.createLargeChart('bar-chart', 'bar', {
|
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
|
datasets: [{
|
|
label: '# of Votes',
|
|
data: [12, 19, 3, 5, 2, 3],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 205, 86, 0.6)', 'rgba(75, 192, 192, 0.6)', 'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)'],
|
|
borderColor: ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 205, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'],
|
|
borderWidth: 1
|
|
}]
|
|
});
|
|
|
|
// Doughnut Chart
|
|
this.createLargeChart('doughnut-chart', 'doughnut', {
|
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
|
datasets: [{
|
|
label: 'My First Dataset',
|
|
data: [300, 50, 100, 75, 120, 60],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 205, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)', 'rgba(255, 159, 64, 0.8)'],
|
|
borderColor: ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 205, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'],
|
|
borderWidth: 2,
|
|
hoverOffset: 10
|
|
}]
|
|
});
|
|
|
|
// Polar Area Chart
|
|
this.createLargeChart('polar-chart', 'polarArea', {
|
|
labels: ['Red', 'Green', 'Yellow', 'Grey', 'Blue'],
|
|
datasets: [{
|
|
label: 'My First Dataset',
|
|
data: [11, 16, 7, 3, 14],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(255, 205, 86, 0.7)', 'rgba(201, 203, 207, 0.7)', 'rgba(54, 162, 235, 0.7)'],
|
|
borderColor: ['rgb(255, 99, 132)', 'rgb(75, 192, 192)', 'rgb(255, 205, 86)', 'rgb(201, 203, 207)', 'rgb(54, 162, 235)'],
|
|
borderWidth: 2
|
|
}]
|
|
});
|
|
|
|
// Radar Chart
|
|
this.createLargeChart('radar-chart', 'radar', {
|
|
labels: ['Speed', 'Reliability', 'Comfort', 'Safety', 'Efficiency', 'Innovation'],
|
|
datasets: [{
|
|
label: 'Product A',
|
|
data: [65, 59, 90, 81, 56, 55],
|
|
fill: true,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 2,
|
|
pointBackgroundColor: 'rgb(54, 162, 235)',
|
|
pointBorderColor: '#fff',
|
|
pointHoverBackgroundColor: '#fff',
|
|
pointHoverBorderColor: 'rgb(54, 162, 235)'
|
|
}, {
|
|
label: 'Product B',
|
|
data: [28, 48, 40, 95, 86, 27],
|
|
fill: true,
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 2,
|
|
pointBackgroundColor: 'rgb(255, 99, 132)',
|
|
pointBorderColor: '#fff',
|
|
pointHoverBackgroundColor: '#fff',
|
|
pointHoverBorderColor: 'rgb(255, 99, 132)'
|
|
}]
|
|
});
|
|
|
|
// Mixed Chart (Bar + Line)
|
|
this.createLargeChart('mixed-chart', 'bar', {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
datasets: [{
|
|
type: 'bar',
|
|
label: 'Sales',
|
|
data: [12, 19, 3, 5, 2, 3],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 1
|
|
}, {
|
|
type: 'line',
|
|
label: 'Revenue',
|
|
data: [18, 25, 8, 15, 12, 18],
|
|
fill: false,
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
borderWidth: 3,
|
|
tension: 0.4,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7
|
|
}]
|
|
});
|
|
|
|
// Bubble Chart
|
|
this.createLargeChart('bubble-chart', 'bubble', {
|
|
datasets: [{
|
|
label: 'First Dataset',
|
|
data: [{
|
|
x: 20,
|
|
y: 30,
|
|
r: 15
|
|
}, {
|
|
x: 40,
|
|
y: 10,
|
|
r: 10
|
|
}, {
|
|
x: 30,
|
|
y: 40,
|
|
r: 20
|
|
}, {
|
|
x: 50,
|
|
y: 35,
|
|
r: 12
|
|
}, {
|
|
x: 10,
|
|
y: 50,
|
|
r: 8
|
|
}, {
|
|
x: 60,
|
|
y: 20,
|
|
r: 18
|
|
}, {
|
|
x: 25,
|
|
y: 25,
|
|
r: 14
|
|
}],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 2
|
|
}, {
|
|
label: 'Second Dataset',
|
|
data: [{
|
|
x: 15,
|
|
y: 45,
|
|
r: 12
|
|
}, {
|
|
x: 35,
|
|
y: 15,
|
|
r: 16
|
|
}, {
|
|
x: 45,
|
|
y: 25,
|
|
r: 9
|
|
}, {
|
|
x: 55,
|
|
y: 45,
|
|
r: 14
|
|
}, {
|
|
x: 25,
|
|
y: 35,
|
|
r: 11
|
|
}],
|
|
backgroundColor: 'rgba(255, 99, 132, 0.6)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 2
|
|
}]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create large chart for charts page
|
|
*/
|
|
createLargeChart(id, type, data) {
|
|
const canvas = document.getElementById(id);
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Define chart-specific options
|
|
const chartOptions = this.getChartOptions(type);
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type,
|
|
data,
|
|
options: chartOptions
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Get chart-specific options based on chart type
|
|
*/
|
|
getChartOptions(type) {
|
|
const baseOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: '600'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
cornerRadius: 8,
|
|
displayColors: true
|
|
}
|
|
}
|
|
};
|
|
|
|
// Chart type specific configurations
|
|
switch (type) {
|
|
case 'doughnut':
|
|
case 'pie':
|
|
return {
|
|
...baseOptions,
|
|
plugins: {
|
|
...baseOptions.plugins,
|
|
legend: {
|
|
...baseOptions.plugins.legend,
|
|
position: 'right'
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false
|
|
}
|
|
};
|
|
case 'polarArea':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
r: {
|
|
pointLabels: {
|
|
display: true,
|
|
centerPointLabels: true,
|
|
font: {
|
|
size: 10
|
|
}
|
|
},
|
|
grid: {}
|
|
}
|
|
}
|
|
};
|
|
case 'radar':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
r: {
|
|
angleLines: {
|
|
display: true
|
|
},
|
|
grid: {},
|
|
pointLabels: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
},
|
|
ticks: {
|
|
display: true,
|
|
font: {
|
|
size: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
case 'bubble':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
type: 'linear',
|
|
position: 'bottom',
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
...baseOptions.plugins,
|
|
tooltip: {
|
|
...baseOptions.plugins.tooltip,
|
|
callbacks: {
|
|
label(context) {
|
|
return `${context.dataset.label}: (${context.parsed.x}, ${context.parsed.y}), Size: ${context.parsed._custom}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
case 'scatter':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
type: 'linear',
|
|
position: 'bottom',
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
default:
|
|
// For line, bar, area, mixed charts
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create tristate chart (for .sparktri elements)
|
|
*/
|
|
createTristateChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data: data.map(val => Math.abs(val)),
|
|
backgroundColor: data.map(val => {
|
|
if (val > 0) return COLORS['light-blue-500'];
|
|
if (val < 0) return '#f90';
|
|
return '#000';
|
|
}),
|
|
borderColor: data.map(val => {
|
|
if (val > 0) return COLORS['light-blue-500'];
|
|
if (val < 0) return '#f90';
|
|
return '#000';
|
|
}),
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create discrete chart (for .sparkdisc elements)
|
|
*/
|
|
createDiscreteChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'scatter',
|
|
data: {
|
|
datasets: [{
|
|
data: data.map((val, index) => ({
|
|
x: index,
|
|
y: val
|
|
})),
|
|
backgroundColor: '#9f0',
|
|
borderColor: '#9f0',
|
|
pointRadius: 2,
|
|
showLine: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create bullet chart (for .sparkbull elements)
|
|
*/
|
|
createBulletChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Simplified bullet chart as horizontal bar
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [''],
|
|
datasets: [{
|
|
data: [Math.max(...data)],
|
|
backgroundColor: COLORS['amber-500'],
|
|
borderColor: COLORS['amber-500'],
|
|
borderWidth: 1,
|
|
barPercentage: 0.6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
indexAxis: 'y',
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.x}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create box chart (for .sparkbox elements)
|
|
*/
|
|
createBoxChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Box plot simplified as bar chart showing quartiles
|
|
const sortedData = [...data].sort((a, b) => a - b);
|
|
const q1 = sortedData[Math.floor(sortedData.length * 0.25)];
|
|
const median = sortedData[Math.floor(sortedData.length * 0.5)];
|
|
const q3 = sortedData[Math.floor(sortedData.length * 0.75)];
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Q1', 'Med', 'Q3'],
|
|
datasets: [{
|
|
data: [q1, median, q3],
|
|
backgroundColor: '#9f0',
|
|
borderColor: '#9f0',
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create Easy Pie Charts (replaces jQuery Easy Pie Chart)
|
|
*/
|
|
createEasyPieCharts() {
|
|
const easyPieElements = document.querySelectorAll('.easy-pie-chart');
|
|
easyPieElements.forEach((element, index) => {
|
|
const size = parseInt(element.dataset.size) || 80;
|
|
const percent = parseInt(element.dataset.percent) || 0;
|
|
const barColor = element.dataset.barColor || '#f44336';
|
|
|
|
// Create canvas for the pie chart
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
canvas.style.width = `${size}px`;
|
|
canvas.style.height = `${size}px`;
|
|
element.appendChild(canvas);
|
|
}
|
|
|
|
// Create percentage display
|
|
const percentDisplay = element.querySelector('span');
|
|
if (percentDisplay) {
|
|
percentDisplay.textContent = `${percent}%`;
|
|
percentDisplay.style.position = 'absolute';
|
|
percentDisplay.style.top = '50%';
|
|
percentDisplay.style.left = '50%';
|
|
percentDisplay.style.transform = 'translate(-50%, -50%)';
|
|
percentDisplay.style.fontSize = '14px';
|
|
percentDisplay.style.fontWeight = 'bold';
|
|
}
|
|
|
|
// Set element position to relative for absolute positioning of text
|
|
element.style.position = 'relative';
|
|
element.style.display = 'inline-block';
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new dist_chart/* Chart */.t1(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
datasets: [{
|
|
data: [percent, 100 - percent],
|
|
backgroundColor: [barColor, '#f0f0f0'],
|
|
borderWidth: 0,
|
|
cutout: '70%'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(`easy-pie-${index}`, chart);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Destroy all charts
|
|
*/
|
|
destroy() {
|
|
this.charts.forEach(chart => {
|
|
chart.destroy();
|
|
});
|
|
this.charts.clear();
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
}
|
|
}
|
|
}
|
|
/* harmony default export */ const components_Chart = (ChartComponent);
|
|
// EXTERNAL MODULE: ../../node_modules/@fullcalendar/core/index.js
|
|
var core = __webpack_require__(932);
|
|
// EXTERNAL MODULE: ../../node_modules/@fullcalendar/interaction/index.js
|
|
var interaction = __webpack_require__(453);
|
|
// EXTERNAL MODULE: ../../node_modules/@fullcalendar/daygrid/index.js
|
|
var daygrid = __webpack_require__(699);
|
|
// EXTERNAL MODULE: ../../node_modules/@fullcalendar/timegrid/index.js + 1 modules
|
|
var timegrid = __webpack_require__(747);
|
|
// EXTERNAL MODULE: ../../node_modules/@fullcalendar/list/index.js + 1 modules
|
|
var list = __webpack_require__(408);
|
|
;// ../assets/scripts/fullcalendar/index.js
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const calendarEl = document.getElementById('calendar');
|
|
|
|
// element found in dom ?
|
|
if (calendarEl == null) {
|
|
return;
|
|
}
|
|
const calendar = new core/* Calendar */.Vv(calendarEl, {
|
|
plugins: [interaction/* default */.Ay, daygrid/* default */.A, timegrid/* default */.A, list/* default */.A],
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
},
|
|
initialDate: date.format(date.now(), 'YYYY-MM-DD'),
|
|
navLinks: true,
|
|
// can click day/week names to navigate views
|
|
editable: true,
|
|
dayMaxEvents: true,
|
|
// allow "more" link when too many events
|
|
events: [{
|
|
title: 'All Day Event',
|
|
start: date.format(date.now(), 'YYYY-MM-DD')
|
|
}, {
|
|
title: 'Long Event',
|
|
start: date.format(date.add(date.now(), 1, 'day'), 'YYYY-MM-DD'),
|
|
end: date.format(date.add(date.now(), 4, 'day'), 'YYYY-MM-DD')
|
|
}, {
|
|
groupId: 999,
|
|
title: 'Repeating Event',
|
|
start: date.format(date.add(date.now(), 2, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T16:00')
|
|
}, {
|
|
groupId: 999,
|
|
title: 'Repeating Event',
|
|
start: date.format(date.add(date.now(), 9, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T16:00')
|
|
}, {
|
|
title: 'Conference',
|
|
start: date.format(date.add(date.now(), 5, 'day'), 'YYYY-MM-DD'),
|
|
end: date.format(date.add(date.now(), 7, 'day'), 'YYYY-MM-DD')
|
|
}, {
|
|
title: 'Meeting',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T10:30'),
|
|
end: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T12:30')
|
|
}, {
|
|
title: 'Lunch',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T12:00')
|
|
}, {
|
|
title: 'Meeting',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T14:30')
|
|
}, {
|
|
title: 'Happy Hour',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T17:30')
|
|
}, {
|
|
title: 'Dinner',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T20:00')
|
|
}, {
|
|
title: 'Birthday Party',
|
|
start: date.format(date.add(date.now(), 4, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T07:00')
|
|
}, {
|
|
title: 'Click for Google',
|
|
url: 'http://google.com/',
|
|
start: date.format(date.add(date.now(), 14, 'day'), 'YYYY-MM-DD')
|
|
}]
|
|
});
|
|
calendar.render();
|
|
});
|
|
// EXTERNAL MODULE: ../../node_modules/masonry-layout/masonry.js
|
|
var masonry = __webpack_require__(201);
|
|
var masonry_default = /*#__PURE__*/__webpack_require__.n(masonry);
|
|
;// ../assets/scripts/masonry/index.js
|
|
|
|
/* harmony default export */ const scripts_masonry = ((function () {
|
|
window.addEventListener('load', () => {
|
|
const masonryElement = document.querySelector('.masonry');
|
|
if (masonryElement) {
|
|
new (masonry_default())(masonryElement, {
|
|
itemSelector: '.masonry-item',
|
|
columnWidth: '.masonry-sizer',
|
|
percentPosition: true
|
|
});
|
|
}
|
|
});
|
|
})());
|
|
;// ../assets/scripts/popover/index.js
|
|
// Simple vanilla JS tooltip and popover implementation
|
|
/* harmony default export */ const popover = ((function () {
|
|
// 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();
|
|
}
|
|
});
|
|
});
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js
|
|
var perfect_scrollbar_esm = __webpack_require__(530);
|
|
;// ../assets/scripts/scrollbar/index.js
|
|
|
|
/* harmony default export */ const scrollbar = ((function () {
|
|
const scrollables = document.querySelectorAll('.scrollable');
|
|
if (scrollables.length > 0) {
|
|
scrollables.forEach(el => {
|
|
new perfect_scrollbar_esm/* default */.A(el);
|
|
});
|
|
}
|
|
})());
|
|
;// ../assets/scripts/search/index.js
|
|
/* harmony default export */ const search = ((function () {
|
|
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();
|
|
});
|
|
}
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/skycons/skycons.js
|
|
var skycons = __webpack_require__(658);
|
|
var skycons_default = /*#__PURE__*/__webpack_require__.n(skycons);
|
|
;// ../assets/scripts/skycons/index.js
|
|
|
|
|
|
const Skycons = skycons_default()(window);
|
|
/* harmony default export */ const scripts_skycons = ((function () {
|
|
let icons;
|
|
const initSkycons = () => {
|
|
const skyconsColor = theme.getCSSVar('--skycons-color');
|
|
if (icons) {
|
|
icons.pause();
|
|
icons.remove('all');
|
|
}
|
|
icons = new Skycons({
|
|
'color': skyconsColor
|
|
});
|
|
const list = ['clear-day', 'clear-night', 'partly-cloudy-day', 'partly-cloudy-night', 'cloudy', 'rain', 'sleet', 'snow', 'wind', 'fog'];
|
|
let i = list.length;
|
|
while (i--) {
|
|
const weatherType = list[i],
|
|
elements = document.getElementsByClassName(weatherType);
|
|
let j = elements.length;
|
|
while (j--) {
|
|
icons.set(elements[j], weatherType);
|
|
}
|
|
}
|
|
icons.play();
|
|
};
|
|
|
|
// Initialize skycons
|
|
initSkycons();
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', initSkycons);
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/jsvectormap/dist/jsvectormap.esm.js
|
|
var jsvectormap_esm = __webpack_require__(82);
|
|
// EXTERNAL MODULE: ../../node_modules/jsvectormap/dist/maps/world.js
|
|
var world = __webpack_require__(177);
|
|
;// ../assets/scripts/utils/storage.js
|
|
/**
|
|
* Adminator Storage Utilities
|
|
* Safe localStorage wrapper with error handling
|
|
*
|
|
* @module utils/storage
|
|
*/
|
|
|
|
/**
|
|
* Check if localStorage is available
|
|
* @returns {boolean}
|
|
*/
|
|
const isAvailable = () => {
|
|
try {
|
|
const test = '__storage_test__';
|
|
localStorage.setItem(test, test);
|
|
localStorage.removeItem(test);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* In-memory fallback when localStorage is unavailable
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const memoryStorage = new Map();
|
|
|
|
/**
|
|
* Storage utility with safe localStorage access
|
|
* Falls back to in-memory storage when localStorage is unavailable
|
|
* (e.g., private browsing, storage quota exceeded)
|
|
*
|
|
* @namespace
|
|
*/
|
|
const storage_Storage = {
|
|
/**
|
|
* Check if storage is available
|
|
* @returns {boolean}
|
|
*/
|
|
isAvailable,
|
|
/**
|
|
* Get an item from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {string|null} Stored value or null
|
|
*
|
|
* @example
|
|
* const theme = Storage.get('theme');
|
|
*/
|
|
get(key) {
|
|
try {
|
|
if (isAvailable()) {
|
|
return localStorage.getItem(key);
|
|
}
|
|
return memoryStorage.get(key) ?? null;
|
|
} catch {
|
|
return memoryStorage.get(key) ?? null;
|
|
}
|
|
},
|
|
/**
|
|
* Set an item in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {string} value - Value to store
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.set('theme', 'dark');
|
|
*/
|
|
set(key, value) {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.setItem(key, value);
|
|
return true;
|
|
}
|
|
memoryStorage.set(key, value);
|
|
return true;
|
|
} catch {
|
|
// Fallback to memory storage
|
|
memoryStorage.set(key, value);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Remove an item from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.remove('theme');
|
|
*/
|
|
remove(key) {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
memoryStorage.delete(key);
|
|
return true;
|
|
} catch {
|
|
memoryStorage.delete(key);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Clear all storage
|
|
*
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.clear();
|
|
*/
|
|
clear() {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.clear();
|
|
}
|
|
memoryStorage.clear();
|
|
return true;
|
|
} catch {
|
|
memoryStorage.clear();
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Get a JSON object from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {*} [defaultValue=null] - Default value if key doesn't exist or parse fails
|
|
* @returns {*} Parsed object or default value
|
|
*
|
|
* @example
|
|
* const settings = Storage.getJSON('settings', { theme: 'light' });
|
|
*/
|
|
getJSON(key) {
|
|
let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
try {
|
|
const value = this.get(key);
|
|
if (value === null) {
|
|
return defaultValue;
|
|
}
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return defaultValue;
|
|
}
|
|
},
|
|
/**
|
|
* Set a JSON object in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {*} value - Value to store (will be JSON stringified)
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.setJSON('settings', { theme: 'dark', sidebar: 'expanded' });
|
|
*/
|
|
setJSON(key, value) {
|
|
try {
|
|
return this.set(key, JSON.stringify(value));
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
/**
|
|
* Check if a key exists in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {boolean}
|
|
*
|
|
* @example
|
|
* if (Storage.has('theme')) {
|
|
* // Use stored theme
|
|
* }
|
|
*/
|
|
has(key) {
|
|
return this.get(key) !== null;
|
|
},
|
|
/**
|
|
* Get all keys in storage
|
|
*
|
|
* @returns {string[]} Array of keys
|
|
*
|
|
* @example
|
|
* const keys = Storage.keys();
|
|
*/
|
|
keys() {
|
|
try {
|
|
if (isAvailable()) {
|
|
return Object.keys(localStorage);
|
|
}
|
|
return Array.from(memoryStorage.keys());
|
|
} catch {
|
|
return Array.from(memoryStorage.keys());
|
|
}
|
|
},
|
|
/**
|
|
* Get storage size in bytes (approximate)
|
|
*
|
|
* @returns {number} Size in bytes
|
|
*
|
|
* @example
|
|
* console.log(`Storage used: ${Storage.size()} bytes`);
|
|
*/
|
|
size() {
|
|
try {
|
|
let total = 0;
|
|
const keys = this.keys();
|
|
for (const key of keys) {
|
|
const value = this.get(key);
|
|
if (value) {
|
|
total += key.length + value.length;
|
|
}
|
|
}
|
|
return total * 2; // UTF-16 uses 2 bytes per character
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
};
|
|
/* harmony default export */ const storage = ((/* unused pure expression or super */ null && (storage_Storage)));
|
|
;// ../assets/scripts/utils/sanitize.js
|
|
/**
|
|
* Adminator Sanitization Utilities
|
|
* HTML and input sanitization for security
|
|
*
|
|
* @module utils/sanitize
|
|
*/
|
|
|
|
/**
|
|
* HTML entities map for encoding
|
|
* @type {Object<string, string>}
|
|
*/
|
|
const HTML_ENTITIES = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/',
|
|
'`': '`',
|
|
'=': '='
|
|
};
|
|
|
|
/**
|
|
* Sanitization utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Sanitize = {
|
|
/**
|
|
* Escape HTML entities to prevent XSS
|
|
*
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string safe for HTML insertion
|
|
*
|
|
* @example
|
|
* const safe = Sanitize.html('<script>alert("xss")</script>');
|
|
* // Returns: <script>alert("xss")</script>
|
|
*/
|
|
html(str) {
|
|
if (typeof str !== 'string') {
|
|
return String(str ?? '');
|
|
}
|
|
return str.replace(/[&<>"'`=/]/g, char => HTML_ENTITIES[char]);
|
|
},
|
|
/**
|
|
* Escape string for use in HTML attributes
|
|
*
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string safe for attribute values
|
|
*
|
|
* @example
|
|
* element.setAttribute('data-name', Sanitize.attr(userInput));
|
|
*/
|
|
attr(str) {
|
|
return this.html(str);
|
|
},
|
|
/**
|
|
* Sanitize URL to prevent javascript: and data: URLs
|
|
*
|
|
* @param {string} url - URL to sanitize
|
|
* @param {string[]} [allowedProtocols=['http:', 'https:', 'mailto:', 'tel:']] - Allowed protocols
|
|
* @returns {string} Sanitized URL or empty string if invalid
|
|
*
|
|
* @example
|
|
* const safeUrl = Sanitize.url(userProvidedUrl);
|
|
* if (safeUrl) {
|
|
* window.location.href = safeUrl;
|
|
* }
|
|
*/
|
|
url(url) {
|
|
let allowedProtocols = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['http:', 'https:', 'mailto:', 'tel:'];
|
|
if (typeof url !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
// Trim and normalize
|
|
const trimmed = url.trim().toLowerCase();
|
|
|
|
// Block dangerous protocols
|
|
const dangerousProtocols = ['javascript:', 'data:', 'vbscript:'];
|
|
for (const protocol of dangerousProtocols) {
|
|
if (trimmed.startsWith(protocol)) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Check for allowed protocols
|
|
try {
|
|
const parsed = new URL(url, window.location.origin);
|
|
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
// Allow relative URLs
|
|
if (!url.startsWith('/') && !url.startsWith('./') && !url.startsWith('../')) {
|
|
return '';
|
|
}
|
|
}
|
|
return url;
|
|
} catch {
|
|
// If URL parsing fails, check if it's a relative URL
|
|
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../') || url.startsWith('#')) {
|
|
return url;
|
|
}
|
|
return '';
|
|
}
|
|
},
|
|
/**
|
|
* Strip HTML tags from a string
|
|
*
|
|
* @param {string} str - String with HTML
|
|
* @returns {string} String with HTML tags removed
|
|
*
|
|
* @example
|
|
* const text = Sanitize.stripTags('<p>Hello <b>World</b></p>');
|
|
* // Returns: 'Hello World'
|
|
*/
|
|
stripTags(str) {
|
|
if (typeof str !== 'string') {
|
|
return String(str ?? '');
|
|
}
|
|
|
|
// Create a temporary element to leverage browser's HTML parser
|
|
const div = document.createElement('div');
|
|
div.innerHTML = str;
|
|
return div.textContent || div.innerText || '';
|
|
},
|
|
/**
|
|
* Sanitize a string for use in CSS
|
|
*
|
|
* @param {string} str - String to sanitize
|
|
* @returns {string} CSS-safe string
|
|
*
|
|
* @example
|
|
* element.style.setProperty('--custom', Sanitize.css(userInput));
|
|
*/
|
|
css(str) {
|
|
if (typeof str !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
// Remove potentially dangerous CSS values
|
|
return str.replace(/expression\s*\(/gi, '').replace(/url\s*\(/gi, '').replace(/javascript:/gi, '').replace(/[<>"']/g, '');
|
|
},
|
|
/**
|
|
* Sanitize a string for use in a filename
|
|
*
|
|
* @param {string} str - String to sanitize
|
|
* @returns {string} Filename-safe string
|
|
*
|
|
* @example
|
|
* const filename = Sanitize.filename(userInput) + '.txt';
|
|
*/
|
|
filename(str) {
|
|
if (typeof str !== 'string') {
|
|
return '';
|
|
}
|
|
return str.replace(/[/\\?%*:|"<>]/g, '-') // Replace dangerous characters
|
|
.replace(/\.\./g, '-') // Prevent directory traversal
|
|
.replace(/^\./, '_') // Don't start with dot
|
|
.slice(0, 255); // Limit length
|
|
},
|
|
/**
|
|
* Create safe innerHTML by escaping interpolated values
|
|
*
|
|
* @param {TemplateStringsArray} strings - Template literal strings
|
|
* @param {...*} values - Values to interpolate
|
|
* @returns {string} HTML string with escaped values
|
|
*
|
|
* @example
|
|
* element.innerHTML = Sanitize.template`<div>${userInput}</div>`;
|
|
*/
|
|
template(strings) {
|
|
for (var _len = arguments.length, values = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
values[_key - 1] = arguments[_key];
|
|
}
|
|
return strings.reduce((result, str, i) => {
|
|
const value = values[i - 1];
|
|
const escaped = value !== undefined ? this.html(String(value)) : '';
|
|
return result + escaped + str;
|
|
});
|
|
},
|
|
/**
|
|
* Validate and sanitize an email address
|
|
*
|
|
* @param {string} email - Email to validate
|
|
* @returns {string} Sanitized email or empty string if invalid
|
|
*
|
|
* @example
|
|
* const email = Sanitize.email(userInput);
|
|
* if (email) {
|
|
* sendEmail(email);
|
|
* }
|
|
*/
|
|
email(email) {
|
|
if (typeof email !== 'string') {
|
|
return '';
|
|
}
|
|
const trimmed = email.trim().toLowerCase();
|
|
|
|
// Basic email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(trimmed)) {
|
|
return '';
|
|
}
|
|
|
|
// Additional checks
|
|
if (trimmed.length > 254) {
|
|
return '';
|
|
}
|
|
return trimmed;
|
|
},
|
|
/**
|
|
* Sanitize a number input
|
|
*
|
|
* @param {*} value - Value to sanitize
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.min=-Infinity] - Minimum value
|
|
* @param {number} [options.max=Infinity] - Maximum value
|
|
* @param {number} [options.default=0] - Default if invalid
|
|
* @returns {number} Sanitized number
|
|
*
|
|
* @example
|
|
* const age = Sanitize.number(userInput, { min: 0, max: 120, default: 18 });
|
|
*/
|
|
number(value) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
const {
|
|
min = -Infinity,
|
|
max = Infinity,
|
|
default: defaultValue = 0
|
|
} = options;
|
|
const num = parseFloat(value);
|
|
if (isNaN(num) || !isFinite(num)) {
|
|
return defaultValue;
|
|
}
|
|
return Math.max(min, Math.min(max, num));
|
|
},
|
|
/**
|
|
* Sanitize an integer input
|
|
*
|
|
* @param {*} value - Value to sanitize
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.min=-Infinity] - Minimum value
|
|
* @param {number} [options.max=Infinity] - Maximum value
|
|
* @param {number} [options.default=0] - Default if invalid
|
|
* @returns {number} Sanitized integer
|
|
*
|
|
* @example
|
|
* const count = Sanitize.integer(userInput, { min: 1, max: 100 });
|
|
*/
|
|
integer(value) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
return Math.floor(this.number(value, options));
|
|
}
|
|
};
|
|
/* harmony default export */ const sanitize = ((/* unused pure expression or super */ null && (Sanitize)));
|
|
;// ../assets/scripts/utils/index.js
|
|
/**
|
|
* Adminator Utilities Index
|
|
* Re-exports all utility modules for convenient importing
|
|
*
|
|
* @module utils
|
|
*/
|
|
|
|
// Re-export all utilities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy initialization code
|
|
/* harmony default export */ const utils = ((function () {
|
|
// ------------------------------------------------------
|
|
// @Window Resize
|
|
// ------------------------------------------------------
|
|
|
|
/**
|
|
* NOTE: Register resize event for Masonry layout
|
|
*/
|
|
const EVENT = document.createEvent('UIEvents');
|
|
window.EVENT = EVENT;
|
|
EVENT.initUIEvent('resize', true, false, window, 0);
|
|
window.addEventListener('load', () => {
|
|
/**
|
|
* Trigger window resize event after page load
|
|
* for recalculation of masonry layout.
|
|
*/
|
|
window.dispatchEvent(EVENT);
|
|
});
|
|
|
|
// ------------------------------------------------------
|
|
// @External Links
|
|
// ------------------------------------------------------
|
|
|
|
// Open external links in new window
|
|
const externalLinks = document.querySelectorAll('a[href^="http"], a[href^="//"]');
|
|
externalLinks.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href && !href.includes(window.location.host)) {
|
|
link.setAttribute('rel', 'noopener noreferrer');
|
|
link.setAttribute('target', '_blank');
|
|
}
|
|
});
|
|
|
|
// ------------------------------------------------------
|
|
// @Resize Trigger
|
|
// ------------------------------------------------------
|
|
|
|
// Trigger resize on any element click
|
|
document.addEventListener('click', () => {
|
|
window.dispatchEvent(window.EVENT);
|
|
});
|
|
})());
|
|
;// ../assets/scripts/vectorMaps/index.js
|
|
|
|
|
|
|
|
|
|
|
|
/* harmony default export */ const vectorMaps = ((function () {
|
|
// Store map instance for cleanup
|
|
let mapInstance = null;
|
|
|
|
// Main initialization function
|
|
const vectorMapInit = () => {
|
|
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 {
|
|
// 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 = (0,jsvectormap_esm/* default */.A)({
|
|
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,
|
|
stroke: colors.markerStroke,
|
|
'stroke-width': 2,
|
|
'stroke-opacity': 0.4
|
|
},
|
|
hover: {
|
|
r: 10,
|
|
fill: colors.hoverColor,
|
|
'stroke-opacity': 0.8,
|
|
cursor: 'pointer'
|
|
}
|
|
},
|
|
// 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,
|
|
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 loaded successfully
|
|
}
|
|
});
|
|
|
|
// Store instance for theme updates
|
|
worldMapContainer.mapInstance = mapInstance;
|
|
} catch {
|
|
// 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 {
|
|
// Theme update failed, reinitializing map
|
|
vectorMapInit();
|
|
}
|
|
} else {
|
|
vectorMapInit();
|
|
}
|
|
};
|
|
|
|
// Initialize map
|
|
vectorMapInit();
|
|
|
|
// Reinitialize on window resize
|
|
window.addEventListener('resize', events.debounce(vectorMapInit, 300));
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', events.debounce(updateMapTheme, 150));
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (mapInstance) {
|
|
try {
|
|
mapInstance.destroy();
|
|
} catch {
|
|
// Map cleanup on unload
|
|
}
|
|
mapInstance = null;
|
|
}
|
|
});
|
|
|
|
// Return public API
|
|
return {
|
|
init: vectorMapInit,
|
|
updateTheme: updateMapTheme,
|
|
getInstance: () => mapInstance
|
|
};
|
|
})());
|
|
;// ../assets/scripts/chat/index.js
|
|
/* harmony default export */ const chat = ((function () {
|
|
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();
|
|
});
|
|
}
|
|
})());
|
|
;// ../assets/scripts/email/index.js
|
|
/* harmony default export */ const email = ((function () {
|
|
// 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 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();
|
|
});
|
|
});
|
|
}
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/load-google-maps-api/index.js
|
|
var load_google_maps_api = __webpack_require__(95);
|
|
var load_google_maps_api_default = /*#__PURE__*/__webpack_require__.n(load_google_maps_api);
|
|
;// ../assets/scripts/googleMaps/index.js
|
|
|
|
|
|
/* harmony default export */ const googleMaps = ((function () {
|
|
let map, marker;
|
|
const initGoogleMap = () => {
|
|
const googleMapElement = document.getElementById('google-map');
|
|
if (googleMapElement) {
|
|
load_google_maps_api_default()({
|
|
key: 'AIzaSyDW8td30_gj6sGXjiMU0ALeMu1SDEwUnEA'
|
|
}).then(() => {
|
|
const latitude = 26.8206;
|
|
const longitude = 30.8025;
|
|
const mapZoom = 5;
|
|
const {
|
|
google
|
|
} = window;
|
|
const mapOptions = {
|
|
center: new google.maps.LatLng(latitude, longitude),
|
|
zoom: mapZoom,
|
|
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
|
styles: [{
|
|
'featureType': 'landscape',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-landscape-hue')
|
|
}, {
|
|
'saturation': 43.400000000000006
|
|
}, {
|
|
'lightness': 37.599999999999994
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.highway',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-highway-hue')
|
|
}, {
|
|
'saturation': -61.8
|
|
}, {
|
|
'lightness': 45.599999999999994
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.arterial',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-road-hue')
|
|
}, {
|
|
'saturation': -100
|
|
}, {
|
|
'lightness': 51.19999999999999
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.local',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-road-hue')
|
|
}, {
|
|
'saturation': -100
|
|
}, {
|
|
'lightness': 52
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'water',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-water-hue')
|
|
}, {
|
|
'saturation': -13.200000000000003
|
|
}, {
|
|
'lightness': 2.4000000000000057
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'poi',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-poi-hue')
|
|
}, {
|
|
'saturation': -1.0989010989011234
|
|
}, {
|
|
'lightness': 11.200000000000017
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}]
|
|
};
|
|
map = new google.maps.Map(document.getElementById('google-map'), mapOptions);
|
|
if (marker) {
|
|
marker.setMap(null);
|
|
}
|
|
marker = new google.maps.Marker({
|
|
map,
|
|
position: new google.maps.LatLng(latitude, longitude),
|
|
visible: true
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initialize Google Maps
|
|
initGoogleMap();
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', initGoogleMap);
|
|
})());
|
|
;// ../assets/scripts/ui/index.js
|
|
/**
|
|
* UI Page Bootstrap Components
|
|
* Vanilla JavaScript implementations for Bootstrap components
|
|
*/
|
|
|
|
/* harmony default export */ const ui = ((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
|
|
};
|
|
})());
|
|
;// ../assets/scripts/app.js
|
|
/**
|
|
* Modern Adminator Application
|
|
* Main application entry point with enhanced mobile support
|
|
*
|
|
* @module app
|
|
* @version 2.9.0
|
|
*/
|
|
|
|
// Note: Bootstrap 5 CSS is still available via SCSS imports
|
|
// Bootstrap JS components removed to eliminate jQuery dependency
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Import styles
|
|
|
|
|
|
// Import other modules that don't need immediate modernization
|
|
|
|
|
|
|
|
class AdminatorApp {
|
|
constructor() {
|
|
this.components = new Map();
|
|
this.isInitialized = false;
|
|
this.themeManager = theme;
|
|
this.cleanupFunctions = [];
|
|
|
|
// Initialize when DOM is ready
|
|
DOM.ready(() => {
|
|
this.init();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the application
|
|
*/
|
|
init() {
|
|
if (this.isInitialized) return;
|
|
logger.time('Adminator Init');
|
|
try {
|
|
// Initialize core components
|
|
this.initSidebar();
|
|
this.initCharts();
|
|
this.initDataTables();
|
|
this.initDatePickers();
|
|
this.initTheme();
|
|
this.initMobileEnhancements();
|
|
|
|
// Setup global event listeners using event delegation
|
|
this.setupGlobalEvents();
|
|
this.isInitialized = true;
|
|
logger.timeEnd('Adminator Init');
|
|
logger.info('Application initialized successfully');
|
|
|
|
// Dispatch custom event for other scripts
|
|
events.emit(window, 'adminator:ready', {
|
|
app: this
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error initializing Adminator App', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Sidebar component
|
|
*/
|
|
initSidebar() {
|
|
if (DOM.exists('.sidebar')) {
|
|
const sidebar = new components_Sidebar();
|
|
this.components.set('sidebar', sidebar);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Chart components
|
|
*/
|
|
initCharts() {
|
|
// Check if we have any chart elements
|
|
const hasCharts = DOM.exists('#sparklinedash') || DOM.exists('.sparkline') || DOM.exists('.sparkbar') || DOM.exists('.sparktri') || DOM.exists('.sparkdisc') || DOM.exists('.sparkbull') || DOM.exists('.sparkbox') || DOM.exists('.easy-pie-chart') || DOM.exists('#line-chart') || DOM.exists('#area-chart') || DOM.exists('#scatter-chart') || DOM.exists('#bar-chart');
|
|
if (hasCharts) {
|
|
const charts = new components_Chart();
|
|
this.components.set('charts', charts);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize DataTables (modern approach)
|
|
*/
|
|
initDataTables() {
|
|
const dataTableElement = DOM.select('#dataTable');
|
|
if (dataTableElement) {
|
|
// For now, use a lightweight approach
|
|
// In future iterations, we can replace with a modern table library
|
|
this.initBasicDataTable(dataTableElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic DataTable implementation (placeholder for full modernization)
|
|
*/
|
|
initBasicDataTable(table) {
|
|
// Add basic sorting functionality
|
|
const headers = DOM.selectAll('th', table);
|
|
headers.forEach(header => {
|
|
if (header.textContent.trim()) {
|
|
header.style.cursor = 'pointer';
|
|
header.style.userSelect = 'none';
|
|
DOM.on(header, 'click', () => {
|
|
// Basic sort functionality can be added here
|
|
// For now, we'll keep the existing DataTables library
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize Date Pickers (modern approach with Day.js)
|
|
*/
|
|
initDatePickers() {
|
|
const startDatePickers = DOM.selectAll('.start-date');
|
|
const endDatePickers = DOM.selectAll('.end-date');
|
|
[...startDatePickers, ...endDatePickers].forEach(picker => {
|
|
// Convert to HTML5 date input for better UX
|
|
if (picker.type !== 'date') {
|
|
picker.type = 'date';
|
|
picker.classList.add('form-control');
|
|
|
|
// Clear the placeholder since HTML5 date inputs don't need it
|
|
picker.removeAttribute('placeholder');
|
|
|
|
// Set default value to today if no value is set
|
|
if (!picker.value) {
|
|
picker.value = date.form.toInputValue(date.now());
|
|
}
|
|
|
|
// Make sure the input is clickable and focusable
|
|
picker.style.pointerEvents = 'auto';
|
|
picker.style.cursor = 'pointer';
|
|
|
|
// Ensure proper styling for HTML5 date input
|
|
picker.style.minHeight = '38px';
|
|
picker.style.lineHeight = '1.5';
|
|
|
|
// Date picker converted to HTML5 with Day.js support
|
|
}
|
|
});
|
|
|
|
// Add enhanced interaction - handle both field and icon clicks
|
|
[...startDatePickers, ...endDatePickers].forEach(picker => {
|
|
// Handle direct field clicks
|
|
DOM.on(picker, 'click', event => {
|
|
event.target.focus();
|
|
// For mobile browsers, trigger the date picker
|
|
if (event.target.showPicker && typeof event.target.showPicker === 'function') {
|
|
try {
|
|
event.target.showPicker();
|
|
} catch {
|
|
// Fallback if showPicker is not supported
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle calendar icon clicks (find the icon in the input group)
|
|
const inputGroup = picker.closest('.input-group');
|
|
if (inputGroup) {
|
|
const calendarIcon = inputGroup.querySelector('.input-group-text i.ti-calendar');
|
|
if (calendarIcon) {
|
|
DOM.on(calendarIcon, 'click', event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
picker.focus();
|
|
if (picker.showPicker && typeof picker.showPicker === 'function') {
|
|
try {
|
|
picker.showPicker();
|
|
} catch {
|
|
// Date picker opened via icon click
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize theme system with toggle
|
|
*/
|
|
initTheme() {
|
|
// Initializing theme system
|
|
|
|
// Initialize theme system first
|
|
this.themeManager.init();
|
|
|
|
// Inject theme toggle if missing - with retry mechanism
|
|
setTimeout(() => {
|
|
const navRight = DOM.select('.nav-right');
|
|
// Check for nav-right and theme-toggle existence
|
|
|
|
if (navRight && !DOM.exists('#theme-toggle')) {
|
|
const li = document.createElement('li');
|
|
li.className = 'theme-toggle d-flex ai-c';
|
|
const currentTheme = this.themeManager.current();
|
|
const isDark = currentTheme === 'dark';
|
|
li.innerHTML = `
|
|
<div class="form-check form-switch d-flex ai-c" style="margin: 0; padding: 0;" role="group" aria-label="Theme switcher">
|
|
<label class="form-check-label me-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-right: 8px;">
|
|
<i class="ti-sun" aria-hidden="true" style="margin-right: 4px;"></i><span class="theme-label">Light</span>
|
|
</label>
|
|
<input class="form-check-input" type="checkbox" id="theme-toggle"
|
|
role="switch"
|
|
aria-checked="${isDark}"
|
|
aria-label="Toggle dark mode"
|
|
${isDark ? 'checked' : ''}
|
|
style="margin: 0;">
|
|
<label class="form-check-label ms-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-left: 8px;">
|
|
<span class="theme-label">Dark</span><i class="ti-moon" aria-hidden="true" style="margin-left: 4px;"></i>
|
|
</label>
|
|
</div>
|
|
`;
|
|
|
|
// Insert before user dropdown (last item) - safer approach
|
|
const lastItem = navRight.querySelector('li:last-child');
|
|
if (lastItem && lastItem.parentNode === navRight) {
|
|
navRight.insertBefore(li, lastItem);
|
|
// Theme toggle inserted before last item
|
|
} else {
|
|
navRight.appendChild(li);
|
|
// Theme toggle appended to nav-right (safer approach)
|
|
}
|
|
|
|
// Add toggle functionality
|
|
const toggle = DOM.select('#theme-toggle');
|
|
if (toggle) {
|
|
DOM.on(toggle, 'change', () => {
|
|
const newTheme = toggle.checked ? 'dark' : 'light';
|
|
toggle.setAttribute('aria-checked', toggle.checked ? 'true' : 'false');
|
|
this.themeManager.apply(newTheme);
|
|
});
|
|
|
|
// Listen for theme changes from other sources
|
|
window.addEventListener('adminator:themeChanged', event => {
|
|
const isDark = event.detail.theme === 'dark';
|
|
toggle.checked = isDark;
|
|
toggle.setAttribute('aria-checked', isDark ? 'true' : 'false');
|
|
|
|
// Update charts when theme changes
|
|
const charts = this.components.get('charts');
|
|
if (charts) charts.redrawCharts();
|
|
});
|
|
}
|
|
} else {
|
|
// No nav-right found or theme-toggle already exists
|
|
}
|
|
}, 100); // Wait 100ms for DOM to be fully ready
|
|
}
|
|
|
|
/**
|
|
* Initialize mobile-specific enhancements
|
|
*/
|
|
initMobileEnhancements() {
|
|
// Initializing mobile enhancements
|
|
this.enhanceMobileDropdowns();
|
|
this.enhanceMobileSearch();
|
|
|
|
// Prevent horizontal scroll on mobile
|
|
if (this.isMobile()) {
|
|
document.body.style.overflowX = 'hidden';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup global event listeners using event delegation for performance
|
|
*/
|
|
setupGlobalEvents() {
|
|
// Use event delegation for dropdown clicks (single listener instead of many)
|
|
const dropdownCleanup = events.delegate(document, 'click', '.nav-right .dropdown-toggle', (e, toggle) => this.handleDropdownClick(e, toggle));
|
|
this.cleanupFunctions.push(dropdownCleanup);
|
|
|
|
// Global click handler for closing dropdowns/search
|
|
const globalClickCleanup = events.on(document, 'click', event => {
|
|
this.handleGlobalClick(event);
|
|
});
|
|
this.cleanupFunctions.push(globalClickCleanup);
|
|
|
|
// Window resize handler with debouncing using Events utility
|
|
const debouncedResize = events.debounce(() => this.handleResize(), 250);
|
|
const resizeCleanup = events.on(window, 'resize', debouncedResize);
|
|
this.cleanupFunctions.push(resizeCleanup);
|
|
|
|
// Escape key handler using delegation
|
|
const escapeCleanup = events.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
this.closeAllOverlays();
|
|
}
|
|
});
|
|
this.cleanupFunctions.push(escapeCleanup);
|
|
logger.debug('Global event listeners set up with delegation');
|
|
}
|
|
|
|
/**
|
|
* Handle dropdown toggle clicks
|
|
* @param {Event} e - Click event
|
|
* @param {Element} toggle - The clicked toggle element
|
|
*/
|
|
handleDropdownClick(e, toggle) {
|
|
if (!this.isMobile()) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const dropdown = toggle.closest('.dropdown');
|
|
const menu = dropdown === null || dropdown === void 0 ? void 0 : dropdown.querySelector('.dropdown-menu');
|
|
if (!dropdown || !menu) return;
|
|
|
|
// Close search if open
|
|
this.closeSearch();
|
|
|
|
// Close other dropdowns
|
|
DOM.selectAll('.nav-right .dropdown').forEach(d => {
|
|
if (d !== dropdown) {
|
|
var _d$querySelector;
|
|
d.classList.remove('show');
|
|
(_d$querySelector = d.querySelector('.dropdown-menu')) === null || _d$querySelector === void 0 || _d$querySelector.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Toggle current dropdown
|
|
const isOpen = dropdown.classList.contains('show');
|
|
dropdown.classList.toggle('show', !isOpen);
|
|
menu.classList.toggle('show', !isOpen);
|
|
document.body.style.overflow = isOpen ? '' : 'hidden';
|
|
document.body.classList.toggle('mobile-menu-open', !isOpen);
|
|
}
|
|
|
|
/**
|
|
* Close all overlays (dropdowns, search)
|
|
*/
|
|
closeAllOverlays() {
|
|
// Close dropdowns
|
|
DOM.selectAll('.nav-right .dropdown').forEach(dropdown => {
|
|
var _dropdown$querySelect;
|
|
dropdown.classList.remove('show');
|
|
(_dropdown$querySelect = dropdown.querySelector('.dropdown-menu')) === null || _dropdown$querySelect === void 0 || _dropdown$querySelect.classList.remove('show');
|
|
});
|
|
|
|
// Close search
|
|
this.closeSearch();
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
|
|
/**
|
|
* Close the search overlay
|
|
*/
|
|
closeSearch() {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Reset icon
|
|
const searchIcon = searchBox.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
const field = searchInput.querySelector('input');
|
|
if (field) {
|
|
field.value = '';
|
|
field.blur();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle window resize events
|
|
*/
|
|
handleResize() {
|
|
// Window resized, updating mobile features
|
|
|
|
// Close all mobile-specific overlays when switching to desktop
|
|
if (!this.isMobile()) {
|
|
document.body.style.overflow = '';
|
|
document.body.style.overflowX = '';
|
|
|
|
// Close dropdowns
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
|
|
// Close search
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
}
|
|
} else {
|
|
// Re-enable mobile overflow protection
|
|
document.body.style.overflowX = 'hidden';
|
|
}
|
|
|
|
// Re-apply mobile enhancements
|
|
this.enhanceMobileDropdowns();
|
|
this.enhanceMobileSearch();
|
|
}
|
|
|
|
/**
|
|
* Handle global click events
|
|
*/
|
|
handleGlobalClick(event) {
|
|
// Close mobile dropdowns when clicking outside
|
|
if (!event.target.closest('.dropdown')) {
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Close search when clicking outside
|
|
if (!event.target.closest('.search-box') && !event.target.closest('.search-input')) {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if we're on a mobile device
|
|
*/
|
|
isMobile() {
|
|
return window.innerWidth <= 768;
|
|
}
|
|
|
|
/**
|
|
* Enhanced mobile dropdown handling with improved email layout
|
|
*/
|
|
enhanceMobileDropdowns() {
|
|
if (!this.isMobile()) return;
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
const toggle = dropdown.querySelector('.dropdown-toggle');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (toggle && menu) {
|
|
// Remove existing listeners to prevent duplicates
|
|
const newToggle = toggle.cloneNode(true);
|
|
toggle.replaceWith(newToggle);
|
|
|
|
// Add click functionality for mobile dropdowns
|
|
DOM.on(newToggle, 'click', e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Close search if open
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
}
|
|
|
|
// Close other dropdowns first
|
|
dropdowns.forEach(otherDropdown => {
|
|
if (otherDropdown !== dropdown) {
|
|
otherDropdown.classList.remove('show');
|
|
const otherMenu = otherDropdown.querySelector('.dropdown-menu');
|
|
if (otherMenu) otherMenu.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Toggle current dropdown
|
|
const isOpen = dropdown.classList.contains('show');
|
|
if (isOpen) {
|
|
dropdown.classList.remove('show');
|
|
menu.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
} else {
|
|
dropdown.classList.add('show');
|
|
menu.classList.add('show');
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.classList.add('mobile-menu-open');
|
|
}
|
|
});
|
|
|
|
// Enhanced mobile close button functionality
|
|
DOM.on(menu, 'click', e => {
|
|
// Check if clicked on the close area (::before pseudo-element area)
|
|
const rect = menu.getBoundingClientRect();
|
|
const clickY = e.clientY - rect.top;
|
|
|
|
// If clicked in top 50px (close button area)
|
|
if (clickY <= 50) {
|
|
dropdown.classList.remove('show');
|
|
menu.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Close dropdowns on escape key
|
|
DOM.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enhanced mobile search handling - Full-width search bar
|
|
*/
|
|
enhanceMobileSearch() {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
const searchToggle = searchBox.querySelector('a');
|
|
const searchField = searchInput.querySelector('input');
|
|
if (searchToggle && searchField) {
|
|
// Setting up full-width search functionality
|
|
|
|
// Remove existing listeners to prevent duplication
|
|
const newSearchToggle = searchToggle.cloneNode(true);
|
|
searchToggle.replaceWith(newSearchToggle);
|
|
DOM.on(newSearchToggle, 'click', e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Full-width search toggle clicked
|
|
|
|
// Close any open dropdowns first
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
|
|
// Toggle search state
|
|
const isActive = searchInput.classList.contains('active');
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (isActive) {
|
|
// Close search
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Change icon back to search
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
if (searchField) {
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
|
|
// Full-width search closed
|
|
} else {
|
|
// Open search
|
|
searchInput.classList.add('active');
|
|
document.body.classList.add('search-open');
|
|
|
|
// Change icon to close
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-close';
|
|
}
|
|
|
|
// Focus the input after a short delay
|
|
setTimeout(() => {
|
|
if (searchField) {
|
|
searchField.focus();
|
|
// Search field focused
|
|
}
|
|
}, 100);
|
|
|
|
// Full-width search opened
|
|
}
|
|
});
|
|
|
|
// Close search on escape
|
|
DOM.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape' && searchInput.classList.contains('active')) {
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Reset icon
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
if (searchField) {
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
|
|
// Full-width search closed via escape
|
|
}
|
|
});
|
|
|
|
// Handle search input
|
|
DOM.on(searchField, 'keypress', e => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const query = searchField.value.trim();
|
|
if (query) {
|
|
// Search query submitted
|
|
// Implement your search logic here
|
|
|
|
// For demo, close search after "searching"
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Full-width search functionality initialized
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a component by name
|
|
*/
|
|
getComponent(name) {
|
|
return this.components.get(name);
|
|
}
|
|
|
|
/**
|
|
* Check if app is ready
|
|
*/
|
|
isReady() {
|
|
return this.isInitialized;
|
|
}
|
|
|
|
/**
|
|
* Destroy the application and clean up all resources
|
|
*/
|
|
destroy() {
|
|
logger.info('Destroying Adminator App');
|
|
|
|
// Clean up all event listeners
|
|
this.cleanupFunctions.forEach(cleanup => {
|
|
if (typeof cleanup === 'function') {
|
|
cleanup();
|
|
}
|
|
});
|
|
this.cleanupFunctions = [];
|
|
|
|
// Destroy all components
|
|
this.components.forEach((component, name) => {
|
|
if (typeof component.destroy === 'function') {
|
|
component.destroy();
|
|
logger.debug(`Component destroyed: ${name}`);
|
|
}
|
|
});
|
|
this.components.clear();
|
|
|
|
// Cleanup performance observers
|
|
utils_performance.cleanup();
|
|
this.isInitialized = false;
|
|
logger.info('Adminator App destroyed');
|
|
}
|
|
|
|
/**
|
|
* Refresh/reinitialize the application
|
|
*/
|
|
refresh() {
|
|
logger.info('Refreshing Adminator App');
|
|
if (this.isInitialized) {
|
|
this.destroy();
|
|
}
|
|
setTimeout(() => {
|
|
this.init();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// Initialize the application
|
|
const app = new AdminatorApp();
|
|
|
|
// Make app globally available for debugging
|
|
window.AdminatorApp = app;
|
|
|
|
// Export for module usage
|
|
/* harmony default export */ const scripts_app = ((/* unused pure expression or super */ null && (app)));
|
|
;// ../assets/scripts/datatable/index.js
|
|
// DataTable implementation
|
|
|
|
/* harmony default export */ const datatable = ((function () {
|
|
// Vanilla JS DataTable implementation
|
|
class VanillaDataTable {
|
|
constructor(element) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
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
|
|
};
|
|
})());
|
|
;// ../assets/scripts/index.js
|
|
/**
|
|
* Adminator Admin Template
|
|
* Modern Entry Point - Phase 2 Modernization
|
|
*/
|
|
|
|
// Import the modern application
|
|
|
|
|
|
// Legacy imports that haven't been modernized yet
|
|
// These will be gradually replaced in future iterations
|
|
|
|
// import './datepicker'; // REMOVED: Replaced with modern day.js implementation in app.js
|
|
|
|
// Note: The following have been modernized and are now handled by app.js:
|
|
// - sidebar (now Sidebar component)
|
|
// - charts (now ChartComponent using Chart.js instead of jQuery Sparkline)
|
|
// - Basic DOM utilities (now DOM utils)
|
|
|
|
/***/ }
|
|
|
|
},
|
|
/******/ __webpack_require__ => { // webpackRuntimeModules
|
|
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
|
|
/******/ __webpack_require__.O(0, [707,311,96], () => (__webpack_exec__(170)));
|
|
/******/ var __webpack_exports__ = __webpack_require__.O();
|
|
/******/ }
|
|
]); |