fix: implement Blazor-native login form to properly update authentication state
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s

Problem: JavaScript login form saved tokens to localStorage but didn't notify
CustomAuthenticationStateProvider, causing [Authorize] pages to remain in
'loading' state indefinitely. The provider only reads tokens when:
1. GetAuthenticationStateAsync() is called (page load)
2. NotifyAuthenticationStateChanged() is triggered (UI updates)

But JavaScript login didn't trigger either, leaving the authentication state
stale.

Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component.
Now the login flow is:
1. User enters credentials in Blazor form
2. HttpClient POST to /api/auth/login
3. Save tokens to localStorage
4. Call CustomAuthenticationStateProvider.LoginAsync() directly
5. Blazor detects auth state change and re-evaluates [Authorize] pages
6. Dashboard [Authorize] page renders successfully

Result: Immediate authentication state update, no loading timeout on protected pages.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:03:53 +09:00
parent 041d3cae96
commit 40cffb3beb
326 changed files with 327714 additions and 47 deletions
@@ -0,0 +1,257 @@
'use strict';
var htmlRoot = document.getElementsByTagName('HTML')[0],
//save states
savePanelStateEnabled = true,
//mobile operator on
mobileOperator = function () {
// Check user agent
const userAgent = navigator.userAgent.toLowerCase();
const isMobileUserAgent = /iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(userAgent);
// Check for touch support
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// Check screen size (optional)
const isSmallScreen = window.innerWidth <= 992; // Adjust the breakpoint as needed
// Return true if any of the conditions are met
return isMobileUserAgent || isTouchDevice || isSmallScreen;
},
//filter
filterClass = function (t, e) {
return String(t).split(/[^\w-]+/).filter(function (t) {
return e.test(t)
}).join(' ')
},
//load
loadSettings = function () {
var t = sessionStorage.getItem('layoutSettings') || '',
e = t ? JSON.parse(t) : {};
// Load theme setting
var savedTheme = e.theme || 'light';
htmlRoot.setAttribute('data-bs-theme', savedTheme);
// Load theme style CSS file only if one was saved
var themeStyle = e.themeStyle || '';
if (themeStyle) {
loadThemeStyle(themeStyle);
}
return Object.assign({
htmlRoot: '',
theme: savedTheme,
themeStyle: themeStyle
}, e)
},
//save
saveSettings = function () {
// Save root HTML classes
layoutSettings.htmlRoot = filterClass(htmlRoot.className, /^(set)-/i);
// Save theme attribute
layoutSettings.theme = htmlRoot.getAttribute('data-bs-theme') || 'light';
// Save theme style CSS path
var themeStyleElement = document.getElementById('theme-style');
if (themeStyleElement && themeStyleElement.getAttribute('href')) {
// Get complete href attribute
layoutSettings.themeStyle = themeStyleElement.getAttribute('href');
console.log('Saved theme style:', layoutSettings.themeStyle);
} else {
layoutSettings.themeStyle = '';
console.log('No theme style to save');
}
// Log the full settings object before saving
console.log('Saving layout settings:', JSON.stringify(layoutSettings));
// Save to sessionStorage
sessionStorage.setItem("layoutSettings", JSON.stringify(layoutSettings));
// Show saving indicator
savingIndicator();
},
// reset
resetSettings = function () {
sessionStorage.setItem("layoutSettings", "");
// reset data-bs-theme
htmlRoot.setAttribute('data-bs-theme', 'light');
// reset theme style element if it exists
const themeStyleElement = document.getElementById('theme-style')
if (themeStyleElement) {
themeStyleElement.setAttribute('href', '');
}
// refresh page
window.location.reload();
},
//load theme style
loadThemeStyle = function (themeStyle) {
if (!themeStyle) return;
// Don't do anything if the URL is empty
if (!themeStyle.trim()) return;
// Get existing theme style if it exists
var existingThemeStyle = document.getElementById('theme-style');
if (existingThemeStyle) {
// Update existing theme style's href
existingThemeStyle.href = themeStyle;
} else {
// Create new theme style element if none exists
var linkElement = document.createElement('link');
linkElement.id = 'theme-style'; // Use the standard ID
linkElement.rel = 'stylesheet';
linkElement.media = 'screen';
linkElement.href = themeStyle;
document.head.appendChild(linkElement);
// Flag to indicate this was loaded from sessionStorage
linkElement.setAttribute('data-loaded-from-storage', 'true');
}
},
//get page id
getPageIdentifier = function () {
return window.location.pathname.split('/').pop() || 'index.html';
},
//save panel state
savePanelState = function () {
if (!savePanelStateEnabled) return;
var state = [];
var columns = document.querySelectorAll('.main-content > .row > [class^="col-"]');
columns.forEach(function (column, columnIndex) {
var panels = column.querySelectorAll('.panel');
panels.forEach(function (panel, position) {
var panelHeader = panel.querySelector('.panel-hdr');
// Save panel classes excluding 'panel' and 'panel-fullscreen'
var panelClasses = panel.className.split(' ').filter(function (cls) {
return cls !== 'panel' && cls !== 'panel-fullscreen';
}).join(' ');
// Save header classes excluding 'panel-hdr'
var headerClasses = panelHeader ? panelHeader.className.split(' ').filter(function (cls) {
return cls !== 'panel-hdr';
}).join(' ') : '';
state.push({
id: panel.id,
column: columnIndex,
position: position, // Save position within column
classes: panelClasses,
headerClasses: headerClasses
});
});
});
var pageId = getPageIdentifier();
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
allStates[pageId] = state;
sessionStorage.setItem('allPanelStates', JSON.stringify(allStates));
savingIndicator();
},
loadPanelState = function () {
var pageId = getPageIdentifier();
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
var savedState = allStates[pageId];
if (!savedState) return;
// Use same selector as save function
var columns = Array.from(document.querySelectorAll('.main-content > .row > [class^="col-"]'));
// Store all existing panels in a map before removing them
var panelMap = {};
columns.forEach(function (column) {
var existingPanels = Array.from(column.querySelectorAll('.panel'));
existingPanels.forEach(function (panel) {
panelMap[panel.id] = panel;
panel.remove();
});
});
// Sort state by column and position
savedState.sort(function (a, b) {
if (a.column === b.column) {
return a.position - b.position;
}
return a.column - b.column;
});
// Reinsert panels in correct order
savedState.forEach(function (item) {
var panel = panelMap[item.id];
if (panel && columns[item.column]) {
// Update panel classes
panel.className = 'panel ' + (item.classes || '');
// Update header classes
var panelHeader = panel.querySelector('.panel-hdr');
if (panelHeader && item.headerClasses) {
panelHeader.className = 'panel-hdr ' + item.headerClasses;
}
// Append to correct column
columns[item.column].appendChild(panel);
}
});
},
// Reset panel state
resetPanelState = function () {
var pageId = getPageIdentifier();
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
delete allStates[pageId];
sessionStorage.setItem('allPanelStates', JSON.stringify(allStates));
//refresh page
window.location.reload();
},
savingIndicator = function () {
// Create or get the indicator element
let indicator = document.getElementById('saving-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'saving-indicator';
document.body.appendChild(indicator);
}
// Show saving animation
//indicator.textContent = '';
indicator.className = 'saving-indicator spinner-border show';
// After a brief delay, show success and hide
setTimeout(() => {
//indicator.textContent = '';
indicator.className = 'saving-indicator spinner-border show success';
setTimeout(() => {
indicator.className = 'saving-indicator spinner-border success';
}, 500);
}, 300);
},
//load page layout settings
layoutSettings = loadSettings();
layoutSettings.htmlRoot && (htmlRoot.className = layoutSettings.htmlRoot);
// Load panel settings is triggered just before <script> tag
loadPanelState();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,662 @@
// ListFilter - A JavaScript plugin for filtering lists with fuzzy search
;(function(window) {
'use strict';
// Cache frequently used selectors
var SELECTORS = {
NAV_MENU: 'nav-menu',
NAV_TITLE: 'nav-title',
NAV_LINK_TEXT: '.nav-link-text',
FILTER_HIDE: 'js-filter-hide',
FILTER_SHOW: 'js-filter-show',
LIST_FILTER: 'js-list-filter',
LIST_ACTIVE: 'js-list-active'
};
// Constants for configuration
var CONSTANTS = {
MAX_INPUT_LENGTH: 100,
RATE_LIMIT_MS: 100,
MAX_MATCHES: 1000,
MAX_CACHE_SIZE: 10000,
MATCH_WEIGHTS: {
EXACT: 3,
SUBSTRING: 2,
CONSECUTIVE: 0.5,
CONSECUTIVE_BONUS: 0.1,
NON_CONSECUTIVE: 0.3,
PREFIX_BONUS: 0.2,
CASE_MATCH_BONUS: 0.1
}
};
// Debounce function implementation with immediate option and rate limiting
function debounce(func, wait, immediate) {
var timeout;
var lastRun = 0;
return function executedFunction() {
var context = this;
var args = arguments;
var now = Date.now();
// Rate limiting
if (now - lastRun < CONSTANTS.RATE_LIMIT_MS) {
return;
}
var later = function() {
timeout = null;
if (!immediate) {
lastRun = Date.now();
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
lastRun = Date.now();
func.apply(context, args);
}
};
}
// Improved fuzzy search implementation with caching and enhanced scoring
var fuzzyMatchCache = new Map();
function fuzzyMatch(pattern, str) {
try {
if (!pattern || !str) return 0;
// Check cache first
var cacheKey = pattern + '|' + str;
if (fuzzyMatchCache.has(cacheKey)) {
return fuzzyMatchCache.get(cacheKey);
}
// Clear cache if it gets too large
if (fuzzyMatchCache.size > CONSTANTS.MAX_CACHE_SIZE) {
fuzzyMatchCache.clear();
}
var lowerPattern = pattern.toLowerCase();
var lowerStr = str.toLowerCase();
var score = 0;
// Exact match gets highest score
if (str === pattern) {
score = CONSTANTS.MATCH_WEIGHTS.EXACT;
fuzzyMatchCache.set(cacheKey, score);
return score;
}
// Substring match gets second highest score
if (lowerStr.includes(lowerPattern)) {
score = CONSTANTS.MATCH_WEIGHTS.SUBSTRING;
// Bonus for case-sensitive match
if (str.includes(pattern)) {
score += CONSTANTS.MATCH_WEIGHTS.CASE_MATCH_BONUS;
}
// Bonus for prefix match
if (lowerStr.startsWith(lowerPattern)) {
score += CONSTANTS.MATCH_WEIGHTS.PREFIX_BONUS;
}
fuzzyMatchCache.set(cacheKey, score);
return score;
}
var patternIdx = 0;
var lastMatchIdx = -1;
var consecutiveMatches = 0;
var firstMatch = true;
for (var strIdx = 0; strIdx < str.length && patternIdx < pattern.length; strIdx++) {
if (lowerPattern[patternIdx] === lowerStr[strIdx]) {
// First match bonus
if (firstMatch) {
score += CONSTANTS.MATCH_WEIGHTS.PREFIX_BONUS * (1 - strIdx / str.length);
firstMatch = false;
}
// Consecutive matches get bonus
if (lastMatchIdx === strIdx - 1) {
consecutiveMatches++;
score += CONSTANTS.MATCH_WEIGHTS.CONSECUTIVE +
(consecutiveMatches * CONSTANTS.MATCH_WEIGHTS.CONSECUTIVE_BONUS);
} else {
consecutiveMatches = 0;
score += CONSTANTS.MATCH_WEIGHTS.NON_CONSECUTIVE;
}
// Case matching bonus
if (pattern[patternIdx] === str[strIdx]) {
score += CONSTANTS.MATCH_WEIGHTS.CASE_MATCH_BONUS;
}
lastMatchIdx = strIdx;
patternIdx++;
}
}
// Only count as match if all pattern characters were found
var finalScore = patternIdx === pattern.length ? score : 0;
// Normalize score based on string lengths
if (finalScore > 0) {
finalScore = finalScore * (pattern.length / str.length);
}
fuzzyMatchCache.set(cacheKey, finalScore);
return finalScore;
} catch (error) {
console.error('Fuzzy match error:', error);
return 0;
}
}
function getNavItemText(element) {
try {
if (!element || !(element instanceof Element)) {
throw new Error('Invalid element provided to getNavItemText');
}
var navText = element.querySelector(SELECTORS.NAV_LINK_TEXT);
return navText ? navText.textContent.trim() : '';
} catch (error) {
console.error('getNavItemText error:', error);
return '';
}
}
// Main plugin constructor with validation
function ListFilter(list, input, options) {
try {
if (!(this instanceof ListFilter)) {
throw new Error('ListFilter must be called with new keyword');
}
// Validate parameters
if (!list || !input) {
throw new Error('List and input parameters are required');
}
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.input = typeof input === 'string' ? document.querySelector(input) : input;
if (!this.list || !this.input) {
throw new Error('Required elements not found. Check your selectors.');
}
if (!(this.list instanceof Element) || !(this.input instanceof Element)) {
throw new Error('Invalid DOM elements provided');
}
var defaults = {
anchor: null,
messageSelector: '.js-filter-message',
debounceWait: 250,
minLength: 1,
maxLength: CONSTANTS.MAX_INPUT_LENGTH,
caseSensitive: false,
onFilter: null,
onReset: null,
onError: null
};
// Validate options
this.settings = Object.assign({}, defaults, this._validateOptions(options));
this.anchor = typeof this.settings.anchor === 'string' ?
document.querySelector(this.settings.anchor) : this.settings.anchor;
// Initialize cache with size monitoring
this._cache = {
items: null,
titles: null,
messageElement: null,
size: 0
};
this._errorCount = 0;
this._lastErrorTime = 0;
this.init();
} catch (error) {
console.error('ListFilter constructor error:', error);
if (options && typeof options.onError === 'function') {
options.onError(error);
}
throw error;
}
}
// Plugin prototype with improved methods
ListFilter.prototype = {
_validateOptions: function(options) {
if (!options) return {};
var validated = {};
try {
if (typeof options !== 'object') {
throw new Error('Options must be an object');
}
// Validate each option
if (options.minLength !== undefined) {
validated.minLength = Math.max(1, parseInt(options.minLength, 10) || 1);
}
if (options.maxLength !== undefined) {
validated.maxLength = Math.min(
CONSTANTS.MAX_INPUT_LENGTH,
parseInt(options.maxLength, 10) || CONSTANTS.MAX_INPUT_LENGTH
);
}
if (options.debounceWait !== undefined) {
validated.debounceWait = Math.max(50, parseInt(options.debounceWait, 10) || 250);
}
validated.caseSensitive = !!options.caseSensitive;
// Validate callbacks
if (options.onFilter && typeof options.onFilter === 'function') {
validated.onFilter = options.onFilter;
}
if (options.onReset && typeof options.onReset === 'function') {
validated.onReset = options.onReset;
}
if (options.onError && typeof options.onError === 'function') {
validated.onError = options.onError;
}
// Validate selectors
if (options.messageSelector && typeof options.messageSelector === 'string') {
validated.messageSelector = options.messageSelector;
}
if (options.anchor) {
validated.anchor = options.anchor;
}
} catch (error) {
console.error('Options validation error:', error);
return {};
}
return validated;
},
init: function() {
try {
if (this.anchor) {
this.anchor.classList.add(SELECTORS.LIST_FILTER);
} else {
this.list.classList.add(SELECTORS.LIST_FILTER);
}
// Pre-cache elements with validation
var items = this.list.getElementsByTagName('a');
var titles = this.list.getElementsByTagName('li');
if (!items.length || !titles.length) {
throw new Error('No filterable items found in the list');
}
this._cache.items = items;
this._cache.titles = titles;
this._cache.messageElement = document.querySelector(this.settings.messageSelector);
this._updateCacheSize();
this.bindEvents();
} catch (error) {
this._handleError(error);
throw error;
}
},
_updateCacheSize: function() {
try {
this._cache.size = 0;
if (this._cache.items) this._cache.size += this._cache.items.length;
if (this._cache.titles) this._cache.size += this._cache.titles.length;
// Clear fuzzy match cache if total cache size is too large
if (this._cache.size > CONSTANTS.MAX_CACHE_SIZE) {
fuzzyMatchCache.clear();
}
} catch (error) {
this._handleError(error);
}
},
_handleError: function(error) {
console.error('ListFilter error:', error);
var now = Date.now();
if (now - this._lastErrorTime > 60000) { // Reset error count after 1 minute
this._errorCount = 0;
}
this._errorCount++;
this._lastErrorTime = now;
if (this.settings.onError) {
this.settings.onError(error);
}
// Auto-recovery if too many errors
if (this._errorCount > 10) {
this.reset();
this._errorCount = 0;
}
},
bindEvents: function() {
try {
var self = this;
this.handleFilter = debounce(this.filter.bind(this), this.settings.debounceWait);
// Use passive event listeners for better performance
this.input.addEventListener('input', this._validateEvent.bind(this), { passive: true });
this.input.addEventListener('change', this._validateEvent.bind(this), { passive: true });
// Add ESC key handler
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' || e.key === 'Esc') {
self.reset();
}
});
if (this._cache.messageElement) {
this._cache.messageElement.addEventListener('click', function(e) {
e.preventDefault();
if (e.target === self._cache.messageElement || self._cache.messageElement.contains(e.target)) {
// Get the Bootstrap tooltip instance and dispose of it
var tooltip = bootstrap.Tooltip.getInstance(self._cache.messageElement);
if (tooltip) {
tooltip.dispose();
// Reinitialize the tooltip
new bootstrap.Tooltip(self._cache.messageElement);
}
self.reset();
}
});
}
// Handle window resize for responsive layouts
this._resizeHandler = debounce(function() {
if (self.input.value.length > self.settings.minLength) {
self.filter();
}
}, 250);
window.addEventListener('resize', this._resizeHandler, { passive: true });
// Add input maxlength attribute
this.input.setAttribute('maxlength', this.settings.maxLength.toString());
} catch (error) {
this._handleError(error);
}
},
_validateEvent: function(event) {
try {
// Validate event source
if (!event || !(event instanceof Event) || event.target !== this.input) {
return;
}
var value = this.input.value;
// Validate input length
if (value.length > this.settings.maxLength) {
this.input.value = value.slice(0, this.settings.maxLength);
return;
}
this.handleFilter(event);
} catch (error) {
this._handleError(error);
}
},
findNavTitle: function(element) {
try {
if (!element || !(element instanceof Element)) {
throw new Error('Invalid element provided to findNavTitle');
}
var current = element;
while (current && !current.classList.contains(SELECTORS.NAV_MENU)) {
var prev = current.previousElementSibling;
while (prev) {
if (prev.classList.contains(SELECTORS.NAV_TITLE)) {
return prev;
}
prev = prev.previousElementSibling;
}
current = current.parentElement;
}
return null;
} catch (error) {
this._handleError(error);
return null;
}
},
showParents: function(element) {
try {
if (!element || !(element instanceof Element)) {
throw new Error('Invalid element provided to showParents');
}
var parent = element.parentElement;
while (parent && !parent.classList.contains(SELECTORS.NAV_MENU)) {
if (parent.tagName === 'LI') {
parent.classList.remove(SELECTORS.FILTER_HIDE);
parent.classList.add(SELECTORS.FILTER_SHOW);
}
parent = parent.parentElement;
}
} catch (error) {
this._handleError(error);
}
},
showChildren: function(element) {
try {
if (!element || !(element instanceof Element)) {
throw new Error('Invalid element provided to showChildren');
}
var children = element.querySelectorAll('li');
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (!child.classList.contains(SELECTORS.NAV_TITLE)) {
child.classList.remove(SELECTORS.FILTER_HIDE);
child.classList.add(SELECTORS.FILTER_SHOW);
}
}
} catch (error) {
this._handleError(error);
}
},
filter: function() {
try {
var filter = this.input.value;
// Input validation
if (filter.length > this.settings.maxLength) {
filter = filter.slice(0, this.settings.maxLength);
this.input.value = filter;
}
if (!this.settings.caseSensitive) {
filter = filter.toLowerCase();
}
if (filter.length > this.settings.minLength) {
// Add active class when filter is active
this.list.classList.add(SELECTORS.LIST_ACTIVE);
this._performFilter(filter);
} else {
// Remove active class and reset
this.list.classList.remove(SELECTORS.LIST_ACTIVE);
this._resetFilter();
}
// Callback
if (typeof this.settings.onFilter === 'function') {
this.settings.onFilter.call(this, filter);
}
} catch (error) {
this._handleError(error);
this._resetFilter(); // Recover from error
}
},
_performFilter: function(filter) {
try {
// Hide all items first
for (var i = 0; i < this._cache.titles.length; i++) {
this._cache.titles[i].classList.remove(SELECTORS.FILTER_SHOW);
this._cache.titles[i].classList.add(SELECTORS.FILTER_HIDE);
}
var matchCount = 0;
var matchedElements = new Set();
var relevantTitles = new Set();
var matches = [];
// Collect matches with limit
for (var j = 0; j < this._cache.items.length && matches.length < CONSTANTS.MAX_MATCHES; j++) {
var item = this._cache.items[j];
var text = getNavItemText(item);
var title = item.getAttribute('title') || '';
var textScore = fuzzyMatch(filter, text);
var titleScore = fuzzyMatch(filter, title);
var score = Math.max(textScore, titleScore);
if (score > 0) {
matches.push({
element: item,
score: score
});
}
}
// Sort by score
matches.sort(function(a, b) { return b.score - a.score; });
// Process matches
for (var k = 0; k < matches.length; k++) {
var match = matches[k];
var parentLi = match.element.closest('li');
if (parentLi && !matchedElements.has(parentLi)) {
parentLi.classList.remove(SELECTORS.FILTER_HIDE);
parentLi.classList.add(SELECTORS.FILTER_SHOW);
this.showParents(parentLi);
this.showChildren(parentLi);
var navTitle = this.findNavTitle(parentLi);
if (navTitle && !relevantTitles.has(navTitle)) {
navTitle.classList.remove(SELECTORS.FILTER_HIDE);
navTitle.classList.add(SELECTORS.FILTER_SHOW);
relevantTitles.add(navTitle);
}
matchedElements.add(parentLi);
matchCount++;
}
}
// Update message
if (this._cache.messageElement) {
this._cache.messageElement.innerHTML = matchCount;
this._cache.messageElement.style.cursor = 'pointer';
}
// Clear match cache if it's too large
if (fuzzyMatchCache.size > CONSTANTS.MAX_CACHE_SIZE) {
fuzzyMatchCache.clear();
}
} catch (error) {
this._handleError(error);
this._resetFilter(); // Recover from error
}
},
_resetFilter: function() {
try {
// Remove active class when resetting
this.list.classList.remove(SELECTORS.LIST_ACTIVE);
for (var i = 0; i < this._cache.titles.length; i++) {
this._cache.titles[i].classList.remove(SELECTORS.FILTER_HIDE, SELECTORS.FILTER_SHOW);
}
if (this._cache.messageElement) {
this._cache.messageElement.textContent = '';
}
} catch (error) {
this._handleError(error);
}
},
reset: function() {
try {
this.input.value = '';
this._resetFilter();
// Callback
if (typeof this.settings.onReset === 'function') {
this.settings.onReset.call(this);
}
} catch (error) {
this._handleError(error);
}
},
// Public method to destroy the plugin and clean up
destroy: function() {
try {
// Remove event listeners
this.input.removeEventListener('input', this._validateEvent);
this.input.removeEventListener('change', this._validateEvent);
window.removeEventListener('resize', this._resizeHandler);
if (this._cache.messageElement) {
this._cache.messageElement.removeEventListener('click', this.reset);
}
// Reset state
this._resetFilter();
// Clear caches
fuzzyMatchCache.clear();
this._cache = null;
// Remove attributes
this.input.removeAttribute('maxlength');
// Remove classes including active class
if (this.anchor) {
this.anchor.classList.remove(SELECTORS.LIST_FILTER, SELECTORS.LIST_ACTIVE);
} else {
this.list.classList.remove(SELECTORS.LIST_FILTER, SELECTORS.LIST_ACTIVE);
}
} catch (error) {
this._handleError(error);
}
}
};
// Add to global namespace
window.ListFilter = ListFilter;
})(window);
@@ -0,0 +1,764 @@
/**
* Navigation v1.6.1
* Enhanced secure version with improved performance, accessibility, usability,
* security, and maintainability - Fixed null error and adjusted to flex display
*/
class Navigation {
static instances = new WeakMap();
static getInstance(element) {
const el = typeof element === 'string' ? document.querySelector(element) : element;
return el instanceof Element ? Navigation.instances.get(el) : null;
}
static initAll(selector, options = {}) {
if (typeof selector !== 'string') throw new Error('Invalid selector');
return Array.from(document.querySelectorAll(selector))
.map(element => new Navigation(element, options));
}
static initOnLoad(options = {}) {
document.addEventListener('DOMContentLoaded', () => {
Navigation.initAll('.navigation', options);
});
}
static destroyAll() {
Navigation.instances.forEach(instance => instance.destroy());
}
static sanitizeHTML(str) {
if (typeof str !== 'string') return '';
const temp = document.createElement('div');
temp.textContent = str;
return temp.innerHTML.replace(/(on\w+)=["'][^"']*["']/gi, '');
}
static isValidUrl(url) {
if (typeof url !== 'string') return false;
if (url === '#') return true;
try {
new URL(url, window.location.origin);
return true;
} catch {
return false;
}
}
constructor(element, options = {}) {
if (!(element instanceof Element)) throw new Error('Invalid element provided');
if (!element.querySelector('ul')) throw new Error('Navigation element must contain a <ul>');
this.defaults = Object.freeze({
accordion: true,
slideUpSpeed: 200,
slideDownSpeed: 200,
closedSign: '<i class="sa sa-chevron-down"></i>',
openedSign: '<i class="sa sa-chevron-up"></i>',
initClass: 'js-nav-built',
debug: false,
instanceId: `nav-${this.generateUniqueId()}`,
maxDepth: 10,
sanitize: true,
animationTiming: 'easeOutExpo',
debounceTime: 0,
onError: null,
showMore: {
enabled: true,
itemsToShow: 15, //has a bug when you show parent in upper tree
minItemsForToggle: 2,
moreText: 'Show {count} more',
lessText: 'Show less'
}
});
this.options = Object.freeze(this.validateOptions({...this.defaults, ...options}));
this.element = element;
this.state = new Proxy({
isInitialized: false,
lastInteraction: 0,
activeSubmenu: null,
isAnimating: false
}, {
set: (target, prop, value) => {
target[prop] = value;
this._debug(`State updated: ${prop}`, 'info', {value});
return true;
}
});
this.cache = new WeakMap();
if (Navigation.instances.has(element)) return Navigation.instances.get(element);
Navigation.instances.set(element, this);
this.handleClick = this.debounce(this.handleClick.bind(this), this.options.debounceTime);
this.handleKeydown = this.handleKeydown.bind(this);
this.init();
}
validateOptions(options) {
const validators = {
accordion: v => typeof v === 'boolean',
slideUpSpeed: v => typeof v === 'number' && v >= 0 && v <= 1000,
slideDownSpeed: v => typeof v === 'number' && v >= 0 && v <= 1000,
closedSign: v => typeof v === 'string' && !/<script/i.test(v),
openedSign: v => typeof v === 'string' && !/<script/i.test(v),
initClass: v => typeof v === 'string',
debug: v => typeof v === 'boolean',
maxDepth: v => typeof v === 'number' && v > 0,
sanitize: v => typeof v === 'boolean',
animationTiming: v => typeof v === 'string',
debounceTime: v => typeof v === 'number' && v >= 0,
onError: v => typeof v === 'function' || v === null
};
for (const [key, value] of Object.entries(options)) {
if (!(key in validators)) continue;
if (!validators[key](value)) {
throw new TypeError(`Invalid ${key}: ${value}`);
}
}
return options;
}
_debug(message, level = 'log', data = {}) {
if (!this.options.debug) return;
const timestamp = new Date().toISOString();
const secureData = JSON.parse(JSON.stringify(data, (k, v) =>
typeof v === 'string' ? Navigation.sanitizeHTML(v) : v));
const prefix = `[Navigation ${this.options.instanceId} ${timestamp}]`;
const logMethod = {error: console.error, warn: console.warn, info: console.info}[level] || console.log;
logMethod(prefix, message, secureData, level === 'error' ? new Error().stack : undefined);
}
init() {
if (this.state.isInitialized) return this._debug('Already initialized', 'warn');
if (!this.element) throw new Error('Navigation element is null');
try {
this._debug('Initializing navigation', 'info');
Object.assign(this.element, {
role: 'navigation',
tabIndex: 0,
'aria-label': 'Main navigation'
});
// Find the top-level menu
const topMenu = this.element.querySelector('ul');
if (topMenu) {
// Set the top-level menu to have role="menu"
topMenu.setAttribute('role', 'menu');
}
this.element.classList.add(this.options.initClass);
this.element.dataset.instanceId = this.options.instanceId;
this.setupSecureCache();
this.setupTitleSeparators();
this.setupMenuItems();
this.setupActiveItems();
// Ensure at least one menu is visible if none are currently displayed
this._ensureMenuVisibility();
this.bindSecureEvents();
this.state.isInitialized = true;
} catch (error) {
this._debug('Initialization failed', 'error', {error});
this.options.onError?.(error);
throw error;
}
}
setupSecureCache() {
const cache = this.buildCache(this.element);
if (this.validateDOMStructure(cache)) {
this.cache.set(this.element, cache);
} else {
throw new Error('Invalid DOM structure');
}
}
buildCache(root) {
const items = [], links = [], subMenus = [];
const traverse = (el, depth = 0) => {
if (depth > this.options.maxDepth) return;
if (el.tagName === 'LI') items.push(el);
if (el.tagName === 'A') links.push(el);
if (el.tagName === 'UL') subMenus.push(el);
Array.from(el.children).forEach(child => traverse(child, depth + 1));
};
traverse(root);
return {items, links, subMenus};
}
validateDOMStructure(cache) {
return cache.items.every(item => this.getElementDepth(item) <= this.options.maxDepth) &&
cache.links.every(link => Navigation.isValidUrl(link.getAttribute('href') || '#'));
}
getElementDepth(element) {
let depth = 0, current = element;
while (current && current !== this.element && depth <= this.options.maxDepth) {
depth++;
current = current.parentElement;
}
return depth;
}
setupMenuItems() {
const cache = this.cache.get(this.element);
const showMoreConfig = this.options.showMore;
if (!showMoreConfig?.enabled) return;
const ITEMS_TO_SHOW = showMoreConfig.itemsToShow || 15;
const MIN_ITEMS_FOR_TOGGLE = showMoreConfig.minItemsForToggle || 2;
cache?.items.forEach(item => {
// Add proper accessibility attributes to nav-title items
if (item.classList.contains('nav-title')) {
const titleSpan = item.querySelector('span');
const titleText = titleSpan?.textContent || 'Section';
// Set role="separator" and aria-label with the title text
item.setAttribute('role', 'separator');
item.setAttribute('aria-label', titleText);
// Remove menuitem role from nav-title items since they're separators
return;
}
const subMenu = item.querySelector('ul');
if (subMenu && this.getElementDepth(subMenu) <= this.options.maxDepth) {
item.classList.add('has-ul');
Object.assign(subMenu, {
role: 'menu',
'data-depth': this.getElementDepth(subMenu)
// Remove the display: none so menus are visible by default
});
// Only hide the menu if explicitly needed
if (!item.classList.contains('active') && !this._hasActiveDescendant(item)) {
subMenu.style.display = 'none';
} else {
// If item is active or has active children, show it
subMenu.style.display = 'flex';
subMenu.style.height = 'auto';
item.classList.add('open');
item.setAttribute('aria-expanded', 'true');
}
// Filter out parent items (items with submenus) from being hidden
const children = Array.from(subMenu.children);
const nonParentItems = children.filter(child => !child.querySelector('ul'));
const parentItems = children.filter(child => child.querySelector('ul'));
// Calculate remaining items only from non-parent items
const visibleNonParentItems = nonParentItems.slice(0, ITEMS_TO_SHOW);
const hiddenNonParentItems = nonParentItems.slice(ITEMS_TO_SHOW);
const remainingItems = hiddenNonParentItems.length;
if (remainingItems >= MIN_ITEMS_FOR_TOGGLE) {
// Create container for hidden items
const hiddenContainer = document.createElement('div');
hiddenContainer.className = 'nav-hidden-container';
// Check if any hidden items are active
const hasActiveItem = hiddenNonParentItems.some(child =>
child.classList.contains('active')
);
// Set initial state based on active items
if (hasActiveItem) {
hiddenContainer.style.display = 'flex';
hiddenContainer.style.height = 'auto';
} else {
hiddenContainer.style.display = 'none';
hiddenContainer.style.height = '0';
}
hiddenContainer.style.overflow = 'hidden';
// Reorder items: visible non-parents, parent items, then hidden non-parents
subMenu.innerHTML = ''; // Clear submenu
visibleNonParentItems.forEach(child => subMenu.appendChild(child));
parentItems.forEach(child => subMenu.appendChild(child));
hiddenNonParentItems.forEach(child => hiddenContainer.appendChild(child));
subMenu.appendChild(hiddenContainer);
// Create "Show more" link with collapse sign
const showMoreLi = document.createElement('li');
showMoreLi.className = 'nav-show-more';
showMoreLi.setAttribute('role', 'presentation');
const showMoreLink = document.createElement('a');
showMoreLink.href = '#';
showMoreLink.className = 'nav-more-link';
showMoreLink.setAttribute('role', 'button');
showMoreLink.setAttribute('aria-expanded', hasActiveItem ? 'true' : 'false');
showMoreLink.setAttribute('aria-controls', `nav-toggle-${this.generateUniqueId().substring(0, 8)}`);
// Add text and collapse sign
const textSpan = document.createElement('span');
const moreText = showMoreConfig.moreText.replace('{count}', remainingItems);
textSpan.textContent = hasActiveItem ? showMoreConfig.lessText : moreText;
showMoreLink.appendChild(textSpan);
const collapseSign = document.createElement('span');
collapseSign.className = 'collapse-sign';
collapseSign.setAttribute('aria-hidden', 'true');
const icon = document.createElement('i');
icon.className = hasActiveItem ? 'sa sa-chevron-up' : 'sa sa-chevron-down';
collapseSign.appendChild(icon);
showMoreLink.appendChild(collapseSign);
if (hasActiveItem) {
showMoreLink.classList.add('showing-more');
}
showMoreLi.appendChild(showMoreLink);
subMenu.appendChild(showMoreLi);
// Set ID on the hidden container for aria-controls reference
const containerId = showMoreLink.getAttribute('aria-controls');
hiddenContainer.id = containerId;
// Add click handler with animation
showMoreLink.addEventListener('click', (e) => {
e.preventDefault();
const isShowingMore = showMoreLink.classList.contains('showing-more');
if (isShowingMore) {
this.animateHeight(hiddenContainer, 'up', () => {
hiddenContainer.style.display = 'none';
textSpan.textContent = moreText;
showMoreLink.classList.remove('showing-more');
showMoreLink.setAttribute('aria-expanded', 'false');
icon.className = 'sa sa-chevron-down';
});
} else {
hiddenContainer.style.display = 'flex';
this.animateHeight(hiddenContainer, 'down', () => {
textSpan.textContent = showMoreConfig.lessText;
showMoreLink.classList.add('showing-more');
showMoreLink.setAttribute('aria-expanded', 'true');
icon.className = 'sa sa-chevron-up';
});
}
});
}
const link = item.querySelector('a:first-child');
if (link) this.setupSecureLink(link);
}
// Only set menuitem role for non-separator items
if (!item.classList.contains('nav-title')) {
item.role = 'menuitem';
}
});
}
setupSecureLink(link) {
// Create collapse sign container
const collapseSign = document.createElement('span');
collapseSign.className = 'collapse-sign';
collapseSign.setAttribute('aria-hidden', 'true');
// Create icon element
const icon = document.createElement('i');
icon.className = 'sa sa-chevron-down';
collapseSign.appendChild(icon);
// Get the parent li element
const parentLi = link.closest('li');
// Set ARIA attributes on the LI instead of the link
if (parentLi) {
parentLi.setAttribute('aria-expanded', 'false');
parentLi.setAttribute('aria-haspopup', 'true');
}
// Add click prevention for empty links
if (link.getAttribute('href') === '#') {
link.addEventListener('click', e => e.preventDefault());
}
link.appendChild(collapseSign);
}
setupActiveItems() {
const cache = this.cache.get(this.element);
// Process all active items to set up expanded state
cache?.items.forEach(item => {
if (item.classList.contains('active')) {
// First expand all parent ULs
let parent = item.parentElement;
while (parent && parent !== this.element) {
if (parent.tagName === 'UL') {
parent.style.display = 'flex';
parent.style.height = 'auto';
// Handle the parent LI
const parentLi = parent.parentElement;
if (parentLi && parentLi.tagName === 'LI') {
parentLi.classList.add('open', 'has-ul');
// Set ARIA attributes on the LI
parentLi.setAttribute('aria-expanded', 'true');
parentLi.setAttribute('aria-haspopup', 'true');
const parentLink = parentLi.querySelector('a');
if (parentLink) {
const collapseSign = parentLink.querySelector('.collapse-sign i');
if (collapseSign) {
collapseSign.className = 'sa sa-chevron-up';
}
}
}
}
parent = parent.parentElement;
}
// Then handle the active item's own submenu if it exists
const submenu = item.querySelector('ul');
if (submenu) {
submenu.style.display = 'flex';
submenu.style.height = 'auto';
item.classList.add('open', 'has-ul');
// Set ARIA attributes on the LI
item.setAttribute('aria-expanded', 'true');
item.setAttribute('aria-haspopup', 'true');
const link = item.querySelector('a');
if (link) {
const collapseSign = link.querySelector('.collapse-sign i');
if (collapseSign) {
collapseSign.className = 'sa sa-chevron-up';
}
}
this.state.activeSubmenu = submenu;
} else {
// This is a leaf item (no submenu)
item.setAttribute('aria-current', 'page');
}
}
});
}
bindSecureEvents() {
this.element.removeEventListener('mousedown', this.handleClick);
this.element.removeEventListener('keydown', this.handleKeydown);
this.element.addEventListener('mousedown', this.handleClick, {passive: true});
this.element.addEventListener('keydown', this.handleKeydown);
}
handleClick(event) {
const link = event.target.closest('a');
if (!link || !this.element.contains(link)) return;
const li = link.parentElement;
const ul = li.querySelector('ul');
if (!ul) return;
event.preventDefault();
this.toggleSubmenu(li, link, ul);
}
handleKeydown(event) {
const link = event.target.closest('a');
if (!link || !this.element.contains(link)) return;
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
this.handleClick(event);
break;
case 'Escape':
event.preventDefault();
this.closeAllSubmenus();
link.focus();
break;
case 'ArrowDown':
case 'ArrowUp':
event.preventDefault();
this.navigateMenu(link, event.key === 'ArrowDown' ? 'next' : 'prev');
break;
}
}
toggleSubmenu(li, link, ul) {
if (this.state.isAnimating) {
this._debug('Animation in progress, ignoring toggle request');
return;
}
// Check the expanded state from the LI, not the link
const isExpanded = li.getAttribute('aria-expanded') === 'true';
if (this.options.accordion) {
this.handleAccordion(li);
}
const icon = link.querySelector('.collapse-sign i');
this.state.isAnimating = true;
if (isExpanded) {
ul.style.display = 'flex';
this.animateHeight(ul, 'up', () => {
li.classList.remove('open');
// Update aria-expanded on the LI
li.setAttribute('aria-expanded', 'false');
if (icon) {
icon.className = 'sa sa-chevron-down';
}
this.state.activeSubmenu = null;
this.state.isAnimating = false;
});
} else {
ul.style.display = 'flex';
this.animateHeight(ul, 'down', () => {
li.classList.add('open');
// Update aria-expanded on the LI
li.setAttribute('aria-expanded', 'true');
if (icon) {
icon.className = 'sa sa-chevron-up';
}
this.state.activeSubmenu = ul;
this.state.isAnimating = false;
});
}
}
animateHeight(element, direction, callback) {
const existingListener = element._transitionEndListener;
if (existingListener) {
element.removeEventListener('transitionend', existingListener);
}
const duration = direction === 'up' ? this.options.slideUpSpeed : this.options.slideDownSpeed;
const timing = this.options.animationTiming === 'easeOutExpo' ?
'cubic-bezier(0.16, 1, 0.3, 1)' : this.options.animationTiming;
element.style.removeProperty('transition');
element.style.removeProperty('height');
element.style.removeProperty('overflow');
if (direction === 'down') {
element.style.display = 'flex';
element.style.overflow = 'hidden';
element.style.height = '0';
element.offsetHeight;
const height = element.scrollHeight;
element.style.transition = `height ${duration}ms ${timing}`;
element.style.height = `${height}px`;
const onTransitionEnd = () => {
element.removeEventListener('transitionend', onTransitionEnd);
element._transitionEndListener = null;
element.style.removeProperty('height');
element.style.removeProperty('overflow');
element.style.removeProperty('transition');
callback();
};
element._transitionEndListener = onTransitionEnd;
element.addEventListener('transitionend', onTransitionEnd, {once: true});
} else {
element.style.overflow = 'hidden';
element.style.height = `${element.scrollHeight}px`;
element.offsetHeight;
element.style.transition = `height ${duration}ms ${timing}`;
element.style.height = '0';
const onTransitionEnd = () => {
element.removeEventListener('transitionend', onTransitionEnd);
element._transitionEndListener = null;
element.style.display = 'none';
element.style.removeProperty('height');
element.style.removeProperty('overflow');
element.style.removeProperty('transition');
callback();
};
element._transitionEndListener = onTransitionEnd;
element.addEventListener('transitionend', onTransitionEnd, {once: true});
}
}
handleAccordion(activeLi) {
const siblings = Array.from(activeLi.parentElement.children);
siblings.forEach(sibling => {
if (sibling !== activeLi) {
const submenu = sibling.querySelector('ul');
const link = sibling.querySelector('a');
if (submenu && getComputedStyle(submenu).display !== 'none') {
this.animateHeight(submenu, 'up', () => {
sibling.classList.remove('open');
// Update aria-expanded on the LI
sibling.setAttribute('aria-expanded', 'false');
const icon = link?.querySelector('.collapse-sign i');
if (icon) {
icon.className = 'sa sa-chevron-down';
}
});
}
}
});
}
navigateMenu(currentLink, direction) {
const links = Array.from(this.element.querySelectorAll('li a'));
const currentIndex = links.indexOf(currentLink);
const nextIndex = direction === 'next'
? (currentIndex + 1) % links.length
: (currentIndex - 1 + links.length) % links.length;
links[nextIndex].focus();
}
closeAllSubmenus() {
const cache = this.cache.get(this.element);
cache?.subMenus.forEach(submenu => {
if (submenu.style.display !== 'none') {
const parent = submenu.parentElement;
this.animateHeight(submenu, 'up', () => {
parent.classList.remove('open');
// Update aria-expanded on the LI
parent.setAttribute('aria-expanded', 'false');
const link = parent.querySelector('a');
if (link) {
const icon = link.querySelector('.collapse-sign i');
if (icon) {
icon.className = 'sa sa-chevron-down';
}
}
});
}
});
this.state.activeSubmenu = null;
}
debounce(fn, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), wait);
};
}
destroy() {
if (!this.state.isInitialized) return this._debug('Cannot destroy - not initialized', 'warn');
try {
this._debug('Starting destruction', 'info');
this.element.removeEventListener('click', this.handleClick);
this.element.removeEventListener('keydown', this.handleKeydown);
this.resetItems();
this.cleanLinks();
this.resetSubmenus();
this.element.removeAttribute('role');
this.element.removeAttribute('tabIndex');
this.element.removeAttribute('aria-label');
this.element.removeAttribute('data-instance-id');
this.element.classList.remove(this.options.initClass);
this.cache.delete(this.element);
Navigation.instances.delete(this.element);
this.state.isInitialized = false;
this._debug('Navigation destroyed', 'info');
} catch (error) {
this._debug('Destruction failed', 'error', {error});
this.options.onError?.(error);
throw error;
}
}
resetItems() {
const cache = this.cache.get(this.element);
cache?.items.forEach(item => {
item.classList.remove('active', 'open');
item.removeAttribute('role');
});
}
cleanLinks() {
const cache = this.cache.get(this.element);
cache?.links.forEach(link => {
const newLink = link.cloneNode(true);
link.parentNode?.replaceChild(newLink, link);
newLink.classList.remove('active');
const sign = newLink.querySelector('.collapse-sign');
sign?.remove();
});
}
resetSubmenus() {
const cache = this.cache.get(this.element);
cache?.subMenus.forEach(menu => {
menu.removeAttribute('style');
menu.removeAttribute('role');
});
}
generateUniqueId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
setupTitleSeparators() {
// Find all nav-title items and add appropriate accessibility attributes
const navTitles = this.element.querySelectorAll('li.nav-title');
navTitles.forEach(item => {
const titleSpan = item.querySelector('span');
const titleText = titleSpan?.textContent || 'Section';
// Set role="separator" and aria-label with the title text
item.setAttribute('role', 'separator');
item.setAttribute('aria-label', titleText);
// Remove any previously set menuitem role
item.removeAttribute('role');
item.setAttribute('role', 'separator');
});
}
// Helper to check if an item has any active descendant items
_hasActiveDescendant(item) {
return !!item.querySelector('li.active');
}
// Ensures at least one menu is visible if none are currently displayed
_ensureMenuVisibility() {
const cache = this.cache.get(this.element);
if (!cache) return;
// Check if any submenus are already visible
const visibleSubmenu = cache.subMenus.find(menu =>
menu.style.display === 'flex' || menu.style.display === 'block'
);
// If no submenus are visible, show the first one
if (!visibleSubmenu && cache.subMenus.length > 0) {
const firstSubmenu = cache.subMenus[0];
firstSubmenu.style.display = 'flex';
firstSubmenu.style.height = 'auto';
const parentLi = firstSubmenu.closest('li');
if (parentLi) {
parentLi.classList.add('open', 'has-ul');
parentLi.setAttribute('aria-expanded', 'true');
const link = parentLi.querySelector('a');
if (link) {
const collapseSign = link.querySelector('.collapse-sign i');
if (collapseSign) {
collapseSign.className = 'sa sa-chevron-up';
}
}
}
this.state.activeSubmenu = firstSubmenu;
}
}
}
@@ -0,0 +1,618 @@
/*!
smartSlimscroll
Security:
Added input sanitization for user-provided options
Protected against potential XSS attacks
Added bounds checking for touch/scroll inputs
Performance:
Added event throttling for scroll and touch events
Used passive event listeners where appropriate
Implemented IntersectionObserver for visibility checks
Memory Management:
Used WeakMap for private state management
Added proper cleanup with a destroy method
Improved event listener cleanup
Error Handling:
Added try-catch blocks for error boundaries
Improved error logging
Added graceful degradation
Modern JavaScript Features:
Used const/let instead of var
Added optional chaining
Used modern DOM APIs
Code Organization:
Better separation of concerns
More modular function structure
Improved state management
*/
(function (window, document) {
'use strict';
var smartSlimScroll = function (selector, options) {
// Handle multiple elements
var elements = typeof selector === 'string' ?
document.querySelectorAll(selector) :
[selector];
// Create instance for each element
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (!element) {
console.error('smartSlimScroll: Element not found');
continue;
}
// Skip if already initialized
if (element.getAttribute('data-slimscroll-initialized')) {
continue;
}
// Mark as initialized
element.setAttribute('data-slimscroll-initialized', 'true');
this.element = element;
this.options = {
width: 'auto',
height: '250px',
size: '7px',
color: '#000',
position: 'right',
offsetX: '1px',
offsetY: '0px',
start: 'top',
opacity: 0.4,
fadeOutSpeed: 500,
alwaysVisible: false,
disableFadeOut: false,
railVisible: false,
railColor: '#333',
railOpacity: 0.2,
railClass: 'slimScrollRail',
barClass: 'slimScrollBar',
wrapperClass: 'slimScrollDiv',
allowPageScroll: false,
wheelStep: 20,
touchScrollStep: 200,
borderRadius: '7px',
railBorderRadius: '7px'
};
// Merge options
for (var key in options) {
if (options.hasOwnProperty(key)) {
this.options[key] = options[key];
}
}
this.init();
}
};
smartSlimScroll.prototype = {
init: function () {
// Store this reference
const self = this;
// Use WeakMap for private variables to prevent memory leaks
const privateState = new WeakMap();
privateState.set(self, {
isOverPanel: false,
isOverBar: false,
isDragg: false,
queueHide: null,
touchDif: 0,
barHeight: 0,
percentScroll: 0,
lastScroll: 0,
minBarHeight: 30,
releaseScroll: false,
isScrolling: false,
scrollTimeout: null,
isOverRail: false,
firstHover: true
});
const o = this.options;
const me = this.element;
let wrapper, rail, bar;
// Guard against XSS by sanitizing user inputs
const sanitizeInput = (input) => {
return input.replace(/[<>]/g, '');
};
// Sanitize user-provided options
o.width = sanitizeInput(o.width);
o.height = sanitizeInput(o.height);
o.color = sanitizeInput(o.color);
o.railColor = sanitizeInput(o.railColor);
// Use passive event listeners for better performance
const passiveOptions = {
passive: false // Allow preventDefault
};
// Improved touch handling with bounds checking
const handleTouchMove = (e) => {
const state = privateState.get(self);
if (!state.releaseScroll) {
e.preventDefault(); // Now works with passive: false
}
if (e.touches?.length) {
const diff = (state.touchDif - e.touches[0].pageY) / o.touchScrollStep;
scrollContent(Math.min(Math.max(diff, -50), 50), true);
state.touchDif = e.touches[0].pageY;
}
};
// Improved mouse wheel handling
const handleWheel = function (e) {
const state = privateState.get(self);
if (!state.isOverPanel) return;
// Set scrolling state
state.isScrolling = true;
showBar();
// Clear any existing scroll timeout
clearTimeout(state.scrollTimeout);
// Normalize delta
let delta = 0;
if (e.wheelDelta) {
delta = -e.wheelDelta / 120;
}
if (e.detail) {
delta = e.detail / 3;
}
if (e.deltaY) {
delta = e.deltaY / 120;
}
// Calculate scroll boundaries
const contentHeight = me.scrollHeight;
const visibleHeight = wrapper.offsetHeight;
const scrollTop = me.scrollTop;
const maxScroll = contentHeight - visibleHeight;
// Determine scroll direction and boundaries
const scrollingUp = delta < 0;
const scrollingDown = delta > 0;
const atTop = scrollTop <= 0;
const atBottom = scrollTop >= maxScroll;
// Check if scroll should be prevented
const shouldPreventScroll =
(scrollingUp && !atTop) ||
(scrollingDown && !atBottom) ||
(contentHeight > visibleHeight);
if (shouldPreventScroll) {
// Stop all propagation and prevent default
e.preventDefault();
e.stopPropagation();
e.returnValue = false;
// Apply the scroll
scrollContent(delta, true);
}
// Set timeout to clear scrolling state
state.scrollTimeout = setTimeout(function () {
state.isScrolling = false;
hideBar();
}, 1000);
};
// Use Intersection Observer for visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
showBar();
} else {
hideBar();
}
});
});
observer.observe(me);
// Clean up function
const cleanup = () => {
observer.disconnect();
wrapper.removeEventListener('touchmove', handleTouchMove);
wrapper.removeEventListener('wheel', handleWheel);
// ... remove other event listeners
};
// Store cleanup function for potential future use
privateState.get(self).cleanup = cleanup;
// Add error boundary
try {
// Ensure we are not binding it again
if (me.parentNode.classList.contains(o.wrapperClass)) {
return;
}
// Create wrapper
wrapper = document.createElement('div');
wrapper.className = o.wrapperClass;
wrapper.style.position = 'relative';
wrapper.style.overflow = 'hidden';
wrapper.style.width = o.width;
wrapper.style.height = o.height;
// Update style for the div
me.style.overflow = 'hidden';
me.style.width = o.width;
me.style.height = o.height;
// Create scrollbar
bar = document.createElement('div');
bar.className = o.barClass;
bar.style.position = 'absolute';
bar.style.top = o.offsetY;
bar.style.width = o.size;
bar.style.opacity = '0';
bar.style.background = o.color;
bar.style.borderRadius = o.borderRadius;
bar.style.transition = 'opacity 0.2s linear';
bar.style.zIndex = '99';
// Create rail
rail = document.createElement('div');
rail.className = o.railClass;
rail.style.position = 'absolute';
rail.style.top = o.offsetY;
rail.style.width = o.size;
rail.style.height = `calc(100% - ${2 * parseInt(o.offsetY)}px)`;
rail.style.opacity = '0';
rail.style.background = o.railColor;
rail.style.borderRadius = o.railBorderRadius;
rail.style.transition = 'opacity 0.2s linear';
rail.style.zIndex = '90';
// Set position
var posCss = (o.position === 'right') ?
{right: o.offsetX} :
{left: o.offsetX};
for (var pos in posCss) {
rail.style[pos] = posCss[pos];
bar.style[pos] = posCss[pos];
}
// Wrap element
me.parentNode.insertBefore(wrapper, me);
wrapper.appendChild(me);
wrapper.appendChild(bar);
wrapper.appendChild(rail);
var getBarHeight = function () {
const state = privateState.get(self);
const offsetHeight = parseInt(o.offsetY) || 0;
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
// Calculate scrollbar height and make sure it is not too small
state.barHeight = Math.max(
(availableHeight / me.scrollHeight) * availableHeight,
state.minBarHeight
);
bar.style.height = state.barHeight + 'px';
// Hide scrollbar if content is not long enough
var display = state.barHeight >= availableHeight ? 'none' : 'block';
bar.style.display = display;
};
getBarHeight();
var scrollContent = function (y, isWheel, isJump) {
const state = privateState.get(self);
state.releaseScroll = false;
var delta = y;
const offsetHeight = parseInt(o.offsetY) || 0;
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
const maxTop = availableHeight - bar.offsetHeight;
if (isWheel) {
// Move bar with mouse wheel
delta = parseInt(bar.style.top || offsetHeight) + y * parseInt(o.wheelStep) / 100 * bar.offsetHeight;
// Constrain within offsetY boundaries
delta = Math.min(Math.max(delta, offsetHeight), maxTop + offsetHeight);
delta = (y > 0) ? Math.ceil(delta) : Math.floor(delta);
bar.style.top = delta + 'px';
}
// Calculate actual scroll amount
state.percentScroll = (parseInt(bar.style.top || 0) - offsetHeight) / (availableHeight - bar.offsetHeight);
delta = state.percentScroll * (me.scrollHeight - wrapper.offsetHeight);
if (isJump) {
delta = y;
var offsetTop = (delta / me.scrollHeight * availableHeight) + offsetHeight;
offsetTop = Math.min(Math.max(offsetTop, offsetHeight), maxTop + offsetHeight);
bar.style.top = offsetTop + 'px';
}
// Scroll content
me.scrollTop = delta;
// Ensure bar is visible
showBar();
hideBar();
};
var showBar = function () {
const state = privateState.get(self);
getBarHeight();
clearTimeout(state.queueHide);
// Show bar if content is scrollable and we're either:
// 1. Over the content panel
// 2. Over the bar/rail
// 3. Currently dragging
// 4. Currently scrolling
if (me.scrollHeight > wrapper.offsetHeight &&
(state.isOverPanel || state.isOverBar || state.isOverRail || state.isDragg || state.isScrolling)) {
bar.style.opacity = o.opacity;
rail.style.opacity = o.railOpacity;
// If only hovering over panel (not interacting), hide after delay
if (state.isOverPanel &&
!state.isOverBar &&
!state.isOverRail &&
!state.isDragg &&
!state.isScrolling) {
clearTimeout(state.queueHide);
state.queueHide = setTimeout(function () {
hideBar();
}, o.fadeOutSpeed);
}
}
};
var hideBar = function () {
const state = privateState.get(self);
if (!o.alwaysVisible) {
clearTimeout(state.queueHide);
state.queueHide = setTimeout(function () {
if (!state.isOverBar && !state.isOverRail &&
!state.isDragg && !state.isScrolling && !o.disableFadeOut) {
bar.style.opacity = 0;
rail.style.opacity = 0;
}
}, o.fadeOutSpeed);
}
};
// Update event listeners
if (window.addEventListener) {
// Bind to the wrapper with capture phase
wrapper.addEventListener('mousewheel', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('DOMMouseScroll', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('wheel', handleWheel, {
passive: false,
capture: true
});
// Also bind to the content element itself
me.addEventListener('mousewheel', handleWheel, {
passive: false,
capture: true
});
me.addEventListener('DOMMouseScroll', handleWheel, {
passive: false,
capture: true
});
me.addEventListener('wheel', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('touchmove', handleTouchMove, {passive: false, capture: true});
} else {
// IE8 and below
wrapper.attachEvent('onmousewheel', handleWheel);
me.attachEvent('onmousewheel', handleWheel);
wrapper.attachEvent('touchmove', handleTouchMove);
}
// Set up initial height
getBarHeight();
// Check start position
if (o.start === 'bottom') {
bar.style.top = (wrapper.offsetHeight - bar.offsetHeight) + 'px';
scrollContent(0, true);
} else if (o.start !== 'top') {
scrollContent(document.querySelector(o.start).offsetTop, null, true);
if (!o.alwaysVisible) {
bar.style.display = 'none';
}
}
// Attach events
var onMouseDown = function (e) {
const state = privateState.get(self);
state.isDragg = true;
showBar();
// Add dragging class
bar.classList.add('dragging');
const offsetY = parseInt(o.offsetY) || 0;
var top = parseFloat(bar.style.top) || offsetY;
var pageY = e.pageY;
var moveHandler = function (e) {
var currTop = top + e.pageY - pageY;
// Account for offsetY in both min and max boundaries
currTop = Math.min(
Math.max(currTop, offsetY),
wrapper.offsetHeight - bar.offsetHeight - offsetY
);
bar.style.top = currTop + 'px';
scrollContent(0, currTop, false);
};
var upHandler = function () {
state.isDragg = false;
// Remove dragging class
bar.classList.remove('dragging');
if (!state.isOverBar) {
hideBar();
}
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('mouseup', upHandler);
};
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
return false;
};
bar.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverBar = true;
showBar();
});
bar.addEventListener('mouseleave', function () {
const state = privateState.get(self);
state.isOverBar = false;
if (!state.isDragg) {
hideBar();
}
});
wrapper.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverPanel = true;
// Only show scrollbar if content is scrollable
if (me.scrollHeight > wrapper.offsetHeight) {
showBar();
}
});
wrapper.addEventListener('mouseleave', function (e) {
const state = privateState.get(self);
// Check if we're not moving to the scrollbar or rail
if (!e.relatedTarget ||
(!e.relatedTarget.closest('.' + o.barClass) &&
!e.relatedTarget.closest('.' + o.railClass))) {
state.isOverPanel = false;
hideBar();
}
});
// Support for mobile devices
wrapper.addEventListener('touchstart', function (e) {
if (e.touches.length) {
const state = privateState.get(self);
state.touchDif = e.touches[0].pageY;
}
});
// Add back the mousedown event listener after the mouseenter/mouseleave events
bar.addEventListener('mousedown', onMouseDown);
// Add rail hover events
rail.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverRail = true;
showBar();
});
rail.addEventListener('mouseleave', function () {
const state = privateState.get(self);
state.isOverRail = false;
if (!state.isDragg) {
hideBar();
}
});
// Handle rail clicks
rail.addEventListener('mousedown', function (e) {
const state = privateState.get(self);
// Calculate relative click position
const railOffset = rail.getBoundingClientRect();
const clickPos = e.clientY - railOffset.top;
const offsetY = parseInt(o.offsetY) || 0;
// Calculate target position (accounting for bar height)
const availableHeight = wrapper.offsetHeight - (2 * offsetY);
const barHalfHeight = bar.offsetHeight / 2;
let targetPos = Math.min(
Math.max(clickPos - barHalfHeight, 0),
availableHeight - bar.offsetHeight
);
// Update bar position
bar.style.top = (targetPos + offsetY) + 'px';
// Scroll content
const scrollRatio = (targetPos) / (availableHeight - bar.offsetHeight);
const scrollPos = scrollRatio * (me.scrollHeight - wrapper.offsetHeight);
me.scrollTop = scrollPos;
showBar();
});
// Replace the try-catch block with a single mousemove listener
document.addEventListener('mousemove', function initCheck(e) {
const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY);
if (elementAtPoint && (wrapper.contains(elementAtPoint) || wrapper === elementAtPoint)) {
const state = privateState.get(self);
state.isOverPanel = true;
if (me.scrollHeight > wrapper.offsetHeight) {
showBar();
}
}
document.removeEventListener('mousemove', initCheck);
}, {once: true});
} catch (error) {
console.error('smartSlimScroll initialization failed:', error);
cleanup();
throw error;
}
},
destroy: function () {
try {
const state = privateState.get(this);
if (state.cleanup) {
state.cleanup();
}
// Remove DOM elements and restore original state
this.element.style = '';
this.element.parentNode.replaceChild(this.element, this.element.parentNode);
} catch (error) {
console.error('smartSlimScroll destruction failed:', error);
}
}
};
// Expose to window
window.smartSlimScroll = smartSlimScroll;
})(window, document);
@@ -0,0 +1,777 @@
var NotesApp = {
currentNoteId: null,
autoSaveTimeout: null,
activeFormatting: {
bold: false,
italic: false,
underline: false
},
init: function() {
this.renderNotesList();
this.setupAutoSave();
this.setupKeyboardShortcuts();
this.setupEditorEvents();
},
setupAutoSave: function() {
var self = this;
document.getElementById('note-content').addEventListener('input', function() {
self.scheduleAutoSave();
self.updateNoteTitle();
});
},
setupEditorEvents: function() {
var self = this;
var editor = document.getElementById('note-content');
// Monitor formatting changes
editor.addEventListener('keyup', function(e) {
self.checkActiveFormatting();
// When Enter is pressed, make sure new paragraph has normal styling
if (e.key === 'Enter') {
self.normalizeNewParagraph();
}
});
editor.addEventListener('click', function() {
self.checkActiveFormatting();
});
// Initial input handling
editor.addEventListener('input', function(e) {
// If this is a new note (empty), format the first line as it's typed
if (editor.childNodes.length === 1 && editor.firstChild.nodeType === Node.TEXT_NODE) {
// Wrap text in a paragraph with heading style
const p = document.createElement('h3');
p.style.fontWeight = 'bold';
p.style.fontSize = '1.2em';
p.style.marginBottom = '0.5em';
p.appendChild(editor.firstChild);
editor.appendChild(p);
// Place cursor at the end
const range = document.createRange();
const sel = window.getSelection();
range.setStart(p.firstChild, p.firstChild.textContent.length);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
});
},
checkActiveFormatting: function() {
try {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const parent = range.commonAncestorContainer;
// Check for formatting by analyzing the parent elements
this.activeFormatting.bold = this.hasParentWithStyle(parent, 'fontWeight', 'bold') ||
this.hasParentWithTag(parent, 'B') ||
this.hasParentWithTag(parent, 'STRONG');
this.activeFormatting.italic = this.hasParentWithStyle(parent, 'fontStyle', 'italic') ||
this.hasParentWithTag(parent, 'I') ||
this.hasParentWithTag(parent, 'EM');
this.activeFormatting.underline = this.hasParentWithStyle(parent, 'textDecoration', 'underline') ||
this.hasParentWithTag(parent, 'U');
}
} catch (e) {
// Use simpler DOM-based detection instead of queryCommandState
console.warn("Selection-based formatting detection failed:", e);
// Default to inactive formatting
this.activeFormatting.bold = false;
this.activeFormatting.italic = false;
this.activeFormatting.underline = false;
// Try to detect based on current cursor position
try {
const editor = document.getElementById('note-content');
if (editor) {
// Check for formatting by looking at styles around cursor
const selectedNode = document.getSelection().focusNode || editor;
this.activeFormatting.bold = this.isNodeFormatted(selectedNode, 'bold');
this.activeFormatting.italic = this.isNodeFormatted(selectedNode, 'italic');
this.activeFormatting.underline = this.isNodeFormatted(selectedNode, 'underline');
}
} catch (innerError) {
console.error("Fallback formatting detection failed:", innerError);
}
}
// Update UI to reflect active formatting
this.updateFormattingButtons();
},
isNodeFormatted: function(node, format) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
if (node && node.parentNode) {
return this.isNodeFormatted(node.parentNode, format);
}
return false;
}
const element = node;
switch(format) {
case 'bold':
return element.tagName === 'B' ||
element.tagName === 'STRONG' ||
window.getComputedStyle(element).fontWeight === 'bold' ||
parseInt(window.getComputedStyle(element).fontWeight) >= 700;
case 'italic':
return element.tagName === 'I' ||
element.tagName === 'EM' ||
window.getComputedStyle(element).fontStyle === 'italic';
case 'underline':
return element.tagName === 'U' ||
window.getComputedStyle(element).textDecoration.includes('underline');
default:
return false;
}
},
hasParentWithStyle(node, style, value) {
while (node && node.nodeType === 1) {
const computedStyle = window.getComputedStyle(node);
if (computedStyle[style] === value ||
(style === 'textDecoration' && computedStyle[style].includes(value))) {
return true;
}
node = node.parentNode;
}
return false;
},
hasParentWithTag(node, tagName) {
while (node && node.nodeType === 1) {
if (node.tagName === tagName) {
return true;
}
node = node.parentNode;
}
return false;
},
updateFormattingButtons: function() {
document.querySelectorAll('.formatting-btn').forEach(btn => {
const format = btn.getAttribute('data-format');
if (this.activeFormatting[format]) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
},
setupKeyboardShortcuts: function() {
document.addEventListener('keydown', function(e) {
if (!NotesApp.currentNoteId) return;
if (e.ctrlKey) {
switch(e.key.toLowerCase()) {
case 'b':
e.preventDefault();
NotesApp.formatText('bold');
break;
case 'i':
e.preventDefault();
NotesApp.formatText('italic');
break;
case 'u':
e.preventDefault();
NotesApp.formatText('underline');
break;
}
}
});
},
scheduleAutoSave: function() {
var self = this;
clearTimeout(this.autoSaveTimeout);
this.autoSaveTimeout = setTimeout(function() {
self.saveCurrentNote();
}, 500);
},
getAllNotes: function() {
try {
var notes = localStorage.getItem('notes');
return notes ? JSON.parse(notes) : {};
} catch (error) {
console.error('Error reading notes from localStorage:', error);
return {};
}
},
sanitizeHtml: function(html) {
// Check if DOMPurify is available
if (typeof DOMPurify !== 'undefined') {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'u', 'strong', 'em', 'h6', 'p', 'br', 'div', 'span'],
ALLOWED_ATTR: ['style']
});
} else {
// Fallback sanitization - very basic, not as secure
console.warn('DOMPurify not loaded. Using basic sanitization fallback.');
// Create a temporary div to handle the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Remove potentially dangerous elements
const scripts = tempDiv.querySelectorAll('script, iframe, object, embed, form, input, button, textarea, style, link, meta');
scripts.forEach(element => element.remove());
// Remove potentially dangerous attributes
const allElements = tempDiv.querySelectorAll('*');
for (let i = 0; i < allElements.length; i++) {
const element = allElements[i];
const attrs = element.attributes;
for (let j = attrs.length - 1; j >= 0; j--) {
const attr = attrs[j];
const attrName = attr.name.toLowerCase();
// Keep only style attribute
if (attrName !== 'style' &&
(attrName.startsWith('on') ||
attrName === 'href' ||
attrName === 'src' ||
attrName === 'srcset' ||
attrName === 'data')) {
element.removeAttribute(attr.name);
}
}
}
return tempDiv.innerHTML;
}
},
extractTitle: function(content) {
if (!content) {
return 'Untitled Note';
}
try {
const div = document.createElement('div');
div.innerHTML = content;
// Look for the first heading or paragraph
const firstElement = div.querySelector('h1, h2, h3, h4, h5, h6, p, div');
if (firstElement) {
const title = firstElement.textContent.trim();
return title.substring(0, 50) + (title.length > 50 ? '...' : '');
}
// Fallback to first line if no elements found
const firstLine = div.textContent.split('\n')[0] || 'Untitled Note';
return firstLine.substring(0, 50) + (firstLine.length > 50 ? '...' : '');
} catch (error) {
console.error('Error extracting title:', error);
return 'Untitled Note';
}
},
updateNoteTitle: function() {
if (!this.currentNoteId) return;
const content = document.getElementById('note-content').innerHTML;
const title = this.extractTitle(content);
var notes = this.getAllNotes();
if (notes[this.currentNoteId]) {
notes[this.currentNoteId].title = title;
localStorage.setItem('notes', JSON.stringify(notes));
this.renderNotesList();
}
},
saveNote: function(noteId, content) {
if (!noteId) {
console.error('Cannot save note: Missing note ID');
return false;
}
// Don't save if content is empty or just the placeholder
const cleanContent = this.cleanContent(content);
if (!cleanContent) {
return false;
}
try {
var notes = this.getAllNotes();
const sanitizedContent = this.sanitizeHtml(content);
notes[noteId] = {
id: noteId,
title: this.extractTitle(sanitizedContent),
content: sanitizedContent,
lastModified: new Date().toISOString()
};
localStorage.setItem('notes', JSON.stringify(notes));
return true;
} catch (error) {
console.error('Error saving note:', error);
return false;
}
},
cleanContent: function(content) {
// Strip all HTML tags to check if there's actual content
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
const textContent = tempDiv.textContent.trim();
return textContent.length > 0 ? content : '';
},
deleteNote: function(noteId) {
if (confirm('Are you sure you want to delete this note?')) {
var notes = this.getAllNotes();
delete notes[noteId];
localStorage.setItem('notes', JSON.stringify(notes));
this.renderNotesList();
}
},
clearAllNotes: function() {
if (confirm('Are you sure you want to delete ALL notes? This cannot be undone.')) {
localStorage.removeItem('notes');
this.renderNotesList();
}
},
createNewNote: function() {
var noteId = 'note_' + new Date().getTime();
this.currentNoteId = noteId;
// Create an empty note first
var notes = this.getAllNotes();
notes[noteId] = {
id: noteId,
title: 'Untitled Note',
content: '',
lastModified: new Date().toISOString()
};
localStorage.setItem('notes', JSON.stringify(notes));
// Now show the editor
this.showNoteEditor(noteId);
},
showNoteEditor: function(noteId) {
var notes = this.getAllNotes();
var note = notes[noteId];
// Safety check to ensure the note exists
if (!note) {
console.error('Note not found:', noteId);
return;
}
document.getElementById('note-content').innerHTML = note.content;
document.getElementById('notes-list-view').style.display = 'none';
document.getElementById('note-edit-view').style.display = 'block';
this.currentNoteId = noteId;
document.getElementById('note-content').focus();
// Ensure the first line is properly formatted as a heading
this.ensureFirstLineIsHeading();
// Check formatting when opening a note
this.checkActiveFormatting();
},
showNotesList: function() {
document.getElementById('notes-list-view').style.display = 'block';
document.getElementById('note-edit-view').style.display = 'none';
this.currentNoteId = null;
this.renderNotesList();
},
saveCurrentNote: function() {
if (!this.currentNoteId) return;
var content = document.getElementById('note-content').innerHTML;
// Get notes and check if currentNoteId exists
var notes = this.getAllNotes();
if (!notes[this.currentNoteId]) {
console.error('Cannot save note: Note ID not found in storage', this.currentNoteId);
return;
}
// Only update the UI if the save was successful (not empty)
if (this.saveNote(this.currentNoteId, content)) {
this.renderNotesList();
}
},
formatText: function(format) {
try {
const editor = document.getElementById('note-content');
const selection = window.getSelection();
if (selection.rangeCount === 0) {
editor.focus();
return;
}
const range = selection.getRangeAt(0);
if (range.collapsed) {
// If no text is selected, don't apply formatting
editor.focus();
return;
}
// Apply the formatting with appropriate HTML elements
let wrapperTag;
switch(format) {
case 'bold':
wrapperTag = document.createElement('strong');
break;
case 'italic':
wrapperTag = document.createElement('em');
break;
case 'underline':
wrapperTag = document.createElement('u');
break;
default:
// For unsupported formats, use alternative method
console.warn(`Format '${format}' is not directly supported`);
editor.focus();
this.scheduleAutoSave();
this.checkActiveFormatting();
return;
}
// Get existing formatting state
const isFormatted = this.activeFormatting[format];
if (isFormatted) {
// If already formatted, remove the formatting by unwrapping content
this.removeFormatting(selection, format);
} else {
// Apply new formatting - surround with appropriate tag
const fragment = range.extractContents();
wrapperTag.appendChild(fragment);
range.insertNode(wrapperTag);
// Preserve selection
selection.removeAllRanges();
const newRange = document.createRange();
newRange.selectNodeContents(wrapperTag);
selection.addRange(newRange);
}
editor.focus();
this.scheduleAutoSave();
this.checkActiveFormatting();
} catch (e) {
// Fallback to simplified approach if error occurs
console.warn("Modern formatting failed:", e);
// Create a temporary error notification
const editor = document.getElementById('note-content');
const notification = document.createElement('div');
notification.textContent = "Formatting could not be applied at this time.";
notification.style.cssText = "position:absolute; top:10px; right:10px; background:#fff3cd; color:#856404; padding:10px; border-radius:5px; z-index:1000;";
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 3000);
editor.focus();
this.scheduleAutoSave();
this.checkActiveFormatting();
}
},
removeFormatting: function(selection, format) {
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
let targetNodes = [];
// Find all nodes with the specified format within the selection
const findFormattedNodes = (node) => {
if (!node) return;
// Check if this node has the formatting we want to remove
let isFormatted = false;
switch(format) {
case 'bold':
isFormatted = node.nodeName === 'STRONG' || node.nodeName === 'B';
break;
case 'italic':
isFormatted = node.nodeName === 'EM' || node.nodeName === 'I';
break;
case 'underline':
isFormatted = node.nodeName === 'U';
break;
}
if (isFormatted) {
targetNodes.push(node);
return;
}
// If not a match, check children
if (node.hasChildNodes()) {
Array.from(node.childNodes).forEach(child => findFormattedNodes(child));
}
};
// Start from the common ancestor container
findFormattedNodes(range.commonAncestorContainer);
// If we didn't find any direct nodes, look at the selection
if (targetNodes.length === 0) {
const fragment = range.cloneContents();
findFormattedNodes(fragment);
// If we found formatting in the fragment, we need to unwrap in the actual DOM
// We'll use the simpler approach of just replacing with text content
if (targetNodes.length > 0) {
const contentText = range.toString();
range.deleteContents();
range.insertNode(document.createTextNode(contentText));
// Reset selection to the inserted text
selection.removeAllRanges();
selection.addRange(range);
return;
}
}
// Unwrap each formatted node (replace it with its contents)
targetNodes.forEach(node => {
if (node.parentNode) {
while (node.firstChild) {
node.parentNode.insertBefore(node.firstChild, node);
}
node.parentNode.removeChild(node);
}
});
},
renderNotesList: function() {
var notes = this.getAllNotes();
var container = document.getElementById('notes-container');
if (Object.keys(notes).length === 0) {
container.innerHTML = '<p class="text-center text-muted mt-4">No notes yet. Create one by clicking the <u>New Note</u> button!</p>';
return;
}
// Sort notes by date (newest first)
var sortedNoteIds = Object.keys(notes).sort(function(a, b) {
return new Date(notes[b].lastModified) - new Date(notes[a].lastModified);
});
// Group notes by date
var groupedNotes = {};
var today = new Date();
today.setHours(0, 0, 0, 0);
var yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
sortedNoteIds.forEach(function(noteId) {
var note = notes[noteId];
var noteDate = new Date(note.lastModified);
noteDate.setHours(0, 0, 0, 0);
var dateKey;
if (noteDate.getTime() === today.getTime()) {
dateKey = 'Today';
} else if (noteDate.getTime() === yesterday.getTime()) {
dateKey = 'Yesterday';
} else {
dateKey = noteDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
if (!groupedNotes[dateKey]) {
groupedNotes[dateKey] = [];
}
groupedNotes[dateKey].push(note);
});
// Build HTML for grouped notes
var notesHtml = '';
Object.keys(groupedNotes).forEach(function(dateGroup) {
notesHtml += `<h6 class="mt-2 mb-3">${dateGroup}</h6>`;
groupedNotes[dateGroup].forEach(function(note) {
// Create a temporary div to parse the content
var contentDiv = document.createElement('div');
contentDiv.innerHTML = note.content;
var title = '';
var contentText = '';
// Extract title from the first heading or paragraph
var firstElement = contentDiv.querySelector('h1, h2, h3, h4, h5, h6, p, div');
if (firstElement) {
// Use the first element as the title
title = firstElement.textContent.trim();
// Remove the first element from consideration for content
firstElement.remove();
// Get content from the remaining elements
contentText = contentDiv.textContent.trim();
} else {
// Fallback: use the note's title property
title = note.title || 'Untitled Note';
}
// Trim and limit the preview
var preview = contentText.substring(0, 100) + (contentText.length > 100 ? '...' : '');
// If there's no content after the title, show a placeholder preview
if (!preview) {
preview = '<span class="text-muted fst-italic">No additional content</span>';
}
notesHtml += `
<div class="info-container p-3 mb-2 note-item position-relative rounded">
<div class="d-flex w-100" onclick="NotesApp.showNoteEditor('${note.id}')">
<div class="w-100">
<h6 class="mb-1 text-break">${title}</h6>
<p class="mb-0 text-muted preview-text fs-sm text-break">${preview}</p>
</div>
</div>
<button class="btn btn-outline-danger btn-xs position-absolute end-0 top-0 m-2 rounded-pill shadow-0"
onclick="event.stopPropagation(); NotesApp.deleteNote('${note.id}')">
<i class="fal fa-trash"></i>
</button>
</div>`;
});
});
container.innerHTML = notesHtml;
},
normalizeNewParagraph: function() {
const editor = document.getElementById('note-content');
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const currentNode = range.startContainer;
// Find the current paragraph
let currentParagraph = currentNode;
while (currentParagraph && currentParagraph.nodeType !== Node.ELEMENT_NODE) {
currentParagraph = currentParagraph.parentNode;
}
// If this is not the first paragraph, ensure it has normal styling
if (currentParagraph && currentParagraph.previousElementSibling) {
// Remove heading styling if present
currentParagraph.style.fontWeight = 'normal';
currentParagraph.style.fontSize = '1em';
// If it's an H tag, convert to a paragraph
if (currentParagraph.tagName.match(/^H[1-6]$/)) {
const newP = document.createElement('p');
while (currentParagraph.firstChild) {
newP.appendChild(currentParagraph.firstChild);
}
currentParagraph.parentNode.replaceChild(newP, currentParagraph);
// Reset selection to maintain cursor position
const newRange = document.createRange();
newRange.setStart(newP.firstChild || newP, 0);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
}
}
},
ensureFirstLineIsHeading: function() {
const editor = document.getElementById('note-content');
if (!editor.innerHTML.trim()) {
return;
}
// Look for the first element
const firstElement = editor.querySelector('*');
if (firstElement) {
// If the first line is not already a heading, make it one
if (!(['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(firstElement.tagName))) {
// Convert to proper H3 element
const h3 = document.createElement('h3');
h3.style.fontWeight = 'bold';
h3.style.fontSize = '1.2em';
h3.style.marginBottom = '0.5em';
// Move content to new heading
while (firstElement.firstChild) {
h3.appendChild(firstElement.firstChild);
}
// Replace the element with our heading
firstElement.parentNode.replaceChild(h3, firstElement);
}
// Make sure subsequent elements have normal styling
let nextElement = firstElement.nextElementSibling;
while (nextElement) {
if (!(['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(nextElement.tagName))) {
nextElement.style.fontWeight = 'normal';
nextElement.style.fontSize = '1em';
}
nextElement = nextElement.nextElementSibling;
}
}
}
};
// Global functions for HTML onclick handlers
function createNewNote() {
NotesApp.createNewNote();
}
function showNotesList() {
NotesApp.showNotesList();
}
function formatText(format) {
NotesApp.formatText(format);
}
function clearAllNotes() {
NotesApp.clearAllNotes();
}
// Initialize the app when the page loads
window.onload = function() {
NotesApp.init();
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Box & Whisker Chart
if (document.getElementById('basic-box-whisker-chart')) {
const basicBoxOptions = {
series: [{
type: 'boxPlot',
data: [
{
x: 'Jan 2015',
y: [54, 66, 69, 75, 88]
},
{
x: 'Jan 2016',
y: [43, 65, 69, 76, 81]
},
{
x: 'Jan 2017',
y: [31, 39, 45, 51, 59]
},
{
x: 'Jan 2018',
y: [39, 46, 55, 65, 71]
},
{
x: 'Jan 2019',
y: [29, 31, 35, 39, 44]
},
{
x: 'Jan 2020',
y: [41, 49, 58, 61, 67]
},
{
x: 'Jan 2021',
y: [54, 59, 66, 71, 88]
}
]
}],
chart: {
type: 'boxPlot',
height: 350,
toolbar: {
show: true
}
},
plotOptions: {
boxPlot: {
colors: {
upper: window.colorMap.primary[500].hex,
lower: window.colorMap.primary[300].hex
}
}
},
stroke: {
colors: [window.colorMap.bootstrapVars.bodyColor.hex]
},
xaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
padding: {
left: -5,
right: 0,
top: -20,
bottom: -5
},
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
theme: 'dark'
}
};
const basicBoxChart = new ApexCharts(
document.getElementById('basic-box-whisker-chart'),
basicBoxOptions
);
basicBoxChart.render();
}
// Horizontal Boxplot Chart
if (document.getElementById('horizontal-boxplot-chart')) {
const horizontalBoxplotOptions = {
series: [{
data: [
{
x: 'Category A',
y: [54, 66, 69, 75, 88]
},
{
x: 'Category B',
y: [43, 65, 69, 76, 81]
},
{
x: 'Category C',
y: [31, 39, 45, 51, 59]
},
{
x: 'Category D',
y: [39, 46, 55, 65, 71]
},
{
x: 'Category E',
y: [29, 31, 35, 39, 44]
},
{
x: 'Category F',
y: [41, 49, 58, 61, 67]
},
{
x: 'Category G',
y: [54, 59, 66, 71, 88]
}
]
}],
chart: {
type: 'boxPlot',
height: 350,
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true,
barHeight: '50%'
},
boxPlot: {
colors: {
upper: window.colorMap.primary[500].hex,
lower: window.colorMap.primary[300].hex
},
horizontal: true
}
},
stroke: {
colors: [window.colorMap.bootstrapVars.bodyColor.hex]
},
xaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
theme: 'dark'
}
};
const horizontalBoxplotChart = new ApexCharts(
document.getElementById('horizontal-boxplot-chart'),
horizontalBoxplotOptions
);
horizontalBoxplotChart.render();
}
});
@@ -0,0 +1,180 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Function to generate bubble chart data
function generateBubbleData(baseval, count, yrange) {
let i = 0;
const series = [];
while (i < count) {
const x = Math.floor(Math.random() * (45 - 20 + 1)) + 20;
const y = Math.floor(Math.random() * (yrange.max - yrange.min + 1)) + yrange.min;
const z = Math.floor(Math.random() * (75 - 15 + 1)) + 15;
series.push([x, y, z]);
baseval += 86400000;
i++;
}
return series;
}
// Simple Bubble Chart
if (document.getElementById('simple-bubble-chart')) {
const simpleBubbleOptions = {
series: [{
name: 'Bubble 1',
data: generateBubbleData(new Date('11 Feb 2022 GMT').getTime(), 20, {
min: 10,
max: 60
})
}, {
name: 'Bubble 2',
data: generateBubbleData(new Date('11 Feb 2022 GMT').getTime(), 20, {
min: 10,
max: 60
})
}, {
name: 'Bubble 3',
data: generateBubbleData(new Date('11 Feb 2022 GMT').getTime(), 20, {
min: 10,
max: 60
})
}],
chart: {
height: 350,
type: 'bubble',
toolbar: {
show: true
}
},
dataLabels: {
enabled: false
},
fill: {
opacity: 0.8
},
xaxis: {
tickAmount: 12,
type: 'category',
},
yaxis: {
max: 70,
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex],
legend: {
position: 'bottom',
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const simpleBubbleChart = new ApexCharts(
document.getElementById('simple-bubble-chart'),
simpleBubbleOptions
);
simpleBubbleChart.render();
}
// 3D Bubble Chart
if (document.getElementById('3d-bubble-chart')) {
// Generate data for 3D bubble chart
function generateData(count, yrange) {
let i = 0;
const series = [];
while (i < count) {
const x = (Math.floor(Math.random() * 100) + 1).toString();
const y = Math.floor(Math.random() * (yrange.max - yrange.min + 1)) + yrange.min;
const z = Math.floor(Math.random() * 20) + 10;
series.push({
x: x,
y: y,
z: z
});
i++;
}
return series;
}
const bubbleChart3dOptions = {
series: [{
name: 'Product 1',
data: generateData(30, {
min: 10,
max: 80
})
}, {
name: 'Product 2',
data: generateData(30, {
min: 10,
max: 80
})
}, {
name: 'Product 3',
data: generateData(30, {
min: 10,
max: 80
})
}],
chart: {
height: 350,
type: 'bubble',
toolbar: {
show: true
}
},
dataLabels: {
enabled: false
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
gradientToColors: [window.colorMap.bootstrapVars.bodyBg.hex],
opacityFrom: 0.8,
opacityTo: 0.5
}
},
xaxis: {
type: 'category',
tickAmount: 10,
},
yaxis: {
max: 90,
tickAmount: 7,
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex],
legend: {
position: 'bottom',
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const bubbleChart3d = new ApexCharts(
document.getElementById('3d-bubble-chart'),
bubbleChart3dOptions
);
bubbleChart3d.render();
}
});
@@ -0,0 +1,519 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Sample data for candlestick charts
const seriesData = [{
x: new Date(2016, 1, 1),
y: [51.98, 56.29, 51.59, 53.85]
}, {
x: new Date(2016, 2, 1),
y: [53.66, 54.99, 51.35, 52.95]
}, {
x: new Date(2016, 3, 1),
y: [52.96, 53.78, 51.54, 52.48]
}, {
x: new Date(2016, 4, 1),
y: [52.54, 52.79, 47.88, 49.24]
}, {
x: new Date(2016, 5, 1),
y: [49.10, 52.86, 47.70, 52.78]
}, {
x: new Date(2016, 6, 1),
y: [52.83, 53.48, 50.32, 52.29]
}, {
x: new Date(2016, 7, 1),
y: [52.20, 54.48, 51.64, 52.58]
}, {
x: new Date(2016, 8, 1),
y: [52.76, 57.35, 52.15, 57.03]
}, {
x: new Date(2016, 9, 1),
y: [57.04, 58.15, 48.88, 56.19]
}, {
x: new Date(2016, 10, 1),
y: [56.09, 58.85, 55.48, 58.79]
}, {
x: new Date(2016, 11, 1),
y: [58.78, 59.65, 58.23, 59.05]
}, {
x: new Date(2017, 0, 1),
y: [59.37, 61.11, 59.35, 60.34]
}, {
x: new Date(2017, 1, 1),
y: [60.40, 60.52, 56.71, 56.93]
}, {
x: new Date(2017, 2, 1),
y: [57.02, 59.71, 56.04, 56.82]
}, {
x: new Date(2017, 3, 1),
y: [56.97, 59.62, 54.77, 59.30]
}, {
x: new Date(2017, 4, 1),
y: [59.11, 62.29, 59.10, 59.85]
}, {
x: new Date(2017, 5, 1),
y: [59.97, 60.11, 55.66, 58.42]
}, {
x: new Date(2017, 6, 1),
y: [58.34, 60.93, 56.75, 57.42]
}, {
x: new Date(2017, 7, 1),
y: [57.76, 58.08, 51.18, 54.71]
}, {
x: new Date(2017, 8, 1),
y: [54.80, 61.42, 53.18, 57.35]
}, {
x: new Date(2017, 9, 1),
y: [57.56, 63.09, 57.00, 62.99]
}, {
x: new Date(2017, 10, 1),
y: [62.89, 63.42, 59.72, 61.76]
}, {
x: new Date(2017, 11, 1),
y: [61.71, 64.15, 61.29, 63.04]
}];
// Basic Candlestick Chart
if (document.getElementById('basic-candlestick-chart')) {
const basicCandlestickOptions = {
series: [{
data: seriesData
}],
chart: {
type: 'candlestick',
height: 350,
toolbar: {
show: false,
},
zoom: {
enabled: false,
}
},
// title: {
// text: 'Basic Candlestick Chart',
// align: 'left'
// },
tooltip: {
theme: 'dark',
y: {
formatter: function(y) {
if (typeof y !== "undefined") {
return "$" + y.toFixed(2);
}
return y;
}
}
},
xaxis: {
type: 'datetime',
labels: {
formatter: function(val) {
return new Date(val).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
tooltip: {
enabled: true
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
plotOptions: {
candlestick: {
colors: {
upward: window.colorMap.primary[500].hex,
downward: window.colorMap.danger[500].hex
}
}
}
};
const basicCandlestickChart = new ApexCharts(
document.getElementById('basic-candlestick-chart'),
basicCandlestickOptions
);
basicCandlestickChart.render();
}
// Combo Candlestick Chart
if (document.getElementById('combo-candlestick-chart')) {
// Generate volume data
const volumeData = [];
for (let i = 0; i < seriesData.length; i++) {
volumeData.push({
x: seriesData[i].x,
y: Math.floor(Math.random() * 1000000) + 200000
});
}
const comboCandlestickOptions = {
series: [{
name: 'candle',
type: 'candlestick',
data: seriesData
}, {
name: 'volume',
type: 'bar',
data: volumeData
}],
chart: {
height: 350,
type: 'line',
stacked: false,
toolbar: {
show: false,
},
zoom: {
enabled: false,
}
},
stroke: {
width: [1, 1]
},
xaxis: {
type: 'datetime',
labels: {
formatter: function(val) {
return new Date(val).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
},
}
},
yaxis: [{
seriesName: 'candle',
title: {
text: 'Price',
style: {
color: window.colorMap.primary[500].hex
}
},
labels: {
formatter: function(val) {
return '$' + val.toFixed(2);
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
tooltip: {
enabled: true
}
}
}, {
seriesName: 'volume',
opposite: true,
title: {
text: 'Volume',
style: {
color: window.colorMap.success[500].hex
}
},
labels: {
formatter: function(val) {
return Intl.NumberFormat('en-US', { notation: 'compact', compactDisplay: 'short' }).format(val);
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}],
tooltip: {
enabled: true,
shared: true
},
plotOptions: {
candlestick: {
colors: {
upward: window.colorMap.primary[500].hex,
downward: window.colorMap.danger[500].hex
}
},
bar: {
columnWidth: '80%',
colors: {
ranges: [{
from: 0,
to: 999999999,
color: window.colorMap.success[100].hex
}]
}
}
},
legend: {
position: 'top',
},
colors: [ window.colorMap.primary[500].hex, window.colorMap.success[100].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const comboCandlestickChart = new ApexCharts(
document.getElementById('combo-candlestick-chart'),
comboCandlestickOptions
);
comboCandlestickChart.render();
}
// Category X-axis Candlestick Chart
if (document.getElementById('category-x-axis-candlestick-chart')) {
// Data with categories
const categorySeriesData = [
{
x: 'Jan',
y: [1500, 1700, 1400, 1650]
},
{
x: 'Feb',
y: [1650, 1850, 1600, 1700]
},
{
x: 'Mar',
y: [1700, 2000, 1680, 1880]
},
{
x: 'Apr',
y: [1880, 2050, 1750, 1800]
},
{
x: 'May',
y: [1800, 1980, 1750, 1890]
},
{
x: 'Jun',
y: [1890, 2100, 1850, 2050]
},
{
x: 'Jul',
y: [2050, 2200, 1900, 2100]
},
{
x: 'Aug',
y: [2100, 2300, 2050, 2200]
},
{
x: 'Sep',
y: [2200, 2400, 2150, 2300]
},
{
x: 'Oct',
y: [2300, 2500, 2200, 2450]
},
{
x: 'Nov',
y: [2450, 2600, 2350, 2550]
},
{
x: 'Dec',
y: [2550, 2750, 2500, 2700]
}
];
const categoryXAxisOptions = {
series: [{
name: 'Product A',
data: categorySeriesData
}],
chart: {
type: 'candlestick',
height: 350,
toolbar: {
show: false,
},
zoom: {
enabled: false,
}
},
title: {
text: 'Monthly Performance Candlestick',
align: 'left'
},
tooltip: {
theme: 'dark',
custom: function({ seriesIndex, dataPointIndex, w }) {
const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
return '<div class="p-2">' +
'<span>Month: ' + data.x + '</span><br>' +
'<span>Open: $' + data.y[0] + '</span><br>' +
'<span>High: $' + data.y[1] + '</span><br>' +
'<span>Low: $' + data.y[2] + '</span><br>' +
'<span>Close: $' + data.y[3] + '</span>' +
'</div>';
}
},
plotOptions: {
candlestick: {
colors: {
upward: window.colorMap.primary[500].hex,
downward: window.colorMap.danger[500].hex
}
}
},
xaxis: {
type: 'category',
labels: {
rotate: -45,
rotateAlways: false,
}
},
yaxis: {
tooltip: {
enabled: true
},
labels: {
formatter: function(val) {
return '$' + val;
},
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const categoryXAxisChart = new ApexCharts(
document.getElementById('category-x-axis-candlestick-chart'),
categoryXAxisOptions
);
categoryXAxisChart.render();
}
// Candlestick with Line Chart
if (document.getElementById('candlestick-with-line-chart')) {
// Generate line series data (moving average)
const lineData = [];
for (let i = 0; i < seriesData.length; i++) {
if (i >= 4) { // 5-day moving average
let sum = 0;
for (let j = i; j > i - 5; j--) {
sum += seriesData[j].y[3]; // use the closing price
}
lineData.push({
x: seriesData[i].x,
y: +(sum / 5).toFixed(2)
});
} else {
lineData.push({
x: seriesData[i].x,
y: seriesData[i].y[3]
});
}
}
const candlestickWithLineOptions = {
series: [{
name: 'Candle',
type: 'candlestick',
data: seriesData
}, {
name: '5-day MA',
type: 'line',
data: lineData
}],
chart: {
height: 350,
type: 'line',
toolbar: {
show: false,
},
zoom: {
enabled: false,
}
},
stroke: {
width: [1, 3]
},
tooltip: {
shared: true
},
xaxis: {
type: 'datetime',
labels: {
formatter: function(val) {
return new Date(val).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: [{
tooltip: {
enabled: true
},
labels: {
formatter: function(val) {
return '$' + val.toFixed(2);
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}],
legend: {
position: 'top',
},
plotOptions: {
candlestick: {
colors: {
upward: window.colorMap.primary[500].hex,
downward: window.colorMap.danger[500].hex
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.success[300].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const candlestickWithLineChart = new ApexCharts(
document.getElementById('candlestick-with-line-chart'),
candlestickWithLineOptions
);
candlestickWithLineChart.render();
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,262 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Funnel Chart
if (document.getElementById('funnel-chart')) {
const funnelOptions = {
series: [
{
name: "Funnel",
data: [1380, 1100, 690, 580, 380, 170]
}
],
chart: {
type: 'bar',
height: 350,
toolbar: {
show: true
}
},
plotOptions: {
bar: {
borderRadius: 0,
horizontal: true,
distributed: true,
barHeight: '80%',
isFunnel: true,
}
},
states: {
hover: {
filter: {
type: 'darken',
value: 0.85
}
}
},
dataLabels: {
enabled: true,
formatter: function(val, opt) {
return opt.w.globals.labels[opt.dataPointIndex] + ': ' + val
},
style: {
fontSize: '12px',
fontWeight: 600,
colors: ['#fff']
},
dropShadow: {
enabled: false
}
},
title: {
text: 'Website Conversion Funnel',
align: 'left',
style: {
fontSize: '16px',
fontWeight: 600,
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
subtitle: {
text: 'Sales Conversion Path Analysis',
align: 'left',
style: {
fontSize: '12px',
fontWeight: 400,
color: window.colorMap.bootstrapVars.bodyColor.rgba(0.6)
}
},
xaxis: {
categories: [
'Visits',
'Unique Visitors',
'Signup',
'Demo Requests',
'Trials',
'Purchases'
],
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
show: false
}
},
legend: {
show: false,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function(value) {
return value.toLocaleString();
}
}
},
colors: [
window.colorMap.primary[500].hex,
window.colorMap.primary[400].hex,
window.colorMap.primary[300].hex,
window.colorMap.primary[200].hex,
window.colorMap.primary[100].hex,
window.colorMap.danger[500].hex
],
grid: {
padding: {
left: 20,
right: 20
},
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
}
};
const funnelChart = new ApexCharts(
document.getElementById('funnel-chart'),
funnelOptions
);
funnelChart.render();
}
// Pyramid Chart
if (document.getElementById('pyramid-chart')) {
const pyramidOptions = {
series: [
{
name: "Pyramid",
data: [120, 220, 350, 480, 580, 690, 1100]
}
],
chart: {
type: 'bar',
height: 380,
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true,
distributed: true,
barHeight: '85%',
isFunnel: true,
flipFunnel: true
}
},
states: {
hover: {
filter: {
type: 'darken',
value: 0.85
}
}
},
dataLabels: {
enabled: true,
formatter: function(val, opt) {
return opt.w.globals.labels[opt.dataPointIndex] + ': ' + val
},
style: {
fontSize: '12px',
fontWeight: 600,
colors: ['#fff']
},
offsetX: 15,
dropShadow: {
enabled: false
}
},
title: {
text: 'Organization Structure',
align: 'left',
style: {
fontSize: '16px',
fontWeight: 600,
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
subtitle: {
text: 'Employee Distribution by Level',
align: 'left',
style: {
fontSize: '12px',
fontWeight: 400,
color: window.colorMap.bootstrapVars.bodyColor.rgba(0.6)
}
},
legend: {
show: false,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
grid: {
show: false,
padding: {
left: 20,
right: 20
},
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
},
xaxis: {
categories: [
'CEO',
'Directors',
'Managers',
'Team Leads',
'Senior Staff',
'Junior Staff',
'Interns'
],
labels: {
show: false,
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
labels: {
show: false
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function(value) {
return value + ' employees';
}
}
},
colors: [
window.colorMap.danger[500].hex,
window.colorMap.primary[100].hex,
window.colorMap.primary[200].hex,
window.colorMap.primary[300].hex,
window.colorMap.primary[400].hex,
window.colorMap.primary[500].hex,
window.colorMap.primary[600].hex
]
};
const pyramidChart = new ApexCharts(
document.getElementById('pyramid-chart'),
pyramidOptions
);
pyramidChart.render();
}
});
@@ -0,0 +1,527 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
function generateData(count, yrange) {
var i = 0;
var series = [];
while (i < count) {
var x = 'w' + (i + 1).toString();
var y = Math.floor(Math.random() * (yrange.max - yrange.min + 1)) + yrange.min;
series.push({
x: x,
y: y
});
i++;
}
return series;
}
// Basic Heatmap Chart
if (document.getElementById('basic-heatmap-chart')) {
const basicHeatmapOptions = {
series: [{
name: 'Metric 1',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 2',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 3',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 4',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 5',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 6',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 7',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 8',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 9',
data: generateData(18, {
min: 0,
max: 90
})
}
],
chart: {
height: 350,
type: 'heatmap',
toolbar: {
show: true
}
},
dataLabels: {
enabled: false
},
colors: [window.colorMap.primary[500].hex],
tooltip: {
theme: 'dark'
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
},
xaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
legend: {
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
};
const basicHeatmapChart = new ApexCharts(
document.getElementById('basic-heatmap-chart'),
basicHeatmapOptions
);
basicHeatmapChart.render();
}
// Heatmap Chart with Multiple Series
if (document.getElementById('multiple-series-heatmap-chart')) {
const multipleSeriesHeatmapOptions = {
series: [{
name: 'Jan',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Feb',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Mar',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Apr',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'May',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Jun',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Jul',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Aug',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Sep',
data: generateData(20, {
min: -30,
max: 55
})
}
],
chart: {
height: 350,
type: 'heatmap',
toolbar: {
show: true
}
},
dataLabels: {
enabled: false
},
colors: [window.colorMap.primary[500].hex],
xaxis: {
type: 'category',
categories: ['10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '01:00', '01:30', '02:00', '02:30', '03:00', '03:30', '04:00', '04:30', '05:00', '05:30', '06:00', '06:30', '07:00', '07:30'],
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
},
tooltip: {
theme: 'dark'
},
legend: {
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
};
const multipleSeriesHeatmapChart = new ApexCharts(
document.getElementById('multiple-series-heatmap-chart'),
multipleSeriesHeatmapOptions
);
multipleSeriesHeatmapChart.render();
}
// Heatmap Chart with Color Range
if (document.getElementById('color-range-heatmap-chart')) {
const colorRangeHeatmapOptions = {
series: [{
name: 'Jan',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Feb',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Mar',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Apr',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'May',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Jun',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Jul',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Aug',
data: generateData(20, {
min: -30,
max: 55
})
},
{
name: 'Sep',
data: generateData(20, {
min: -30,
max: 55
})
}
],
chart: {
height: 350,
type: 'heatmap',
toolbar: {
show: true
}
},
plotOptions: {
heatmap: {
shadeIntensity: 0.5,
radius: 0,
useFillColorAsStroke: true,
colorScale: {
ranges: [{
from: -30,
to: 5,
name: 'Low',
color: window.colorMap.success[100].hex
},
{
from: 6,
to: 20,
name: 'Medium',
color: window.colorMap.primary[300].hex
},
{
from: 21,
to: 45,
name: 'High',
color: window.colorMap.primary[500].hex
},
{
from: 46,
to: 55,
name: 'Extreme',
color: window.colorMap.danger[500].hex
}
]
}
}
},
dataLabels: {
enabled: false
},
xaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
},
tooltip: {
theme: 'dark'
},
legend: {
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
};
const colorRangeHeatmapChart = new ApexCharts(
document.getElementById('color-range-heatmap-chart'),
colorRangeHeatmapOptions
);
colorRangeHeatmapChart.render();
}
// Heatmap Chart with Rounded Corners
if (document.getElementById('rounded-heatmap-chart')) {
const roundedHeatmapOptions = {
series: [{
name: 'Metric 1',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 2',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 3',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 4',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 5',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 6',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 7',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 8',
data: generateData(18, {
min: 0,
max: 90
})
},
{
name: 'Metric 9',
data: generateData(18, {
min: 0,
max: 90
})
}
],
chart: {
height: 350,
type: 'heatmap',
toolbar: {
show: true
}
},
plotOptions: {
heatmap: {
radius: 15,
enableShades: false,
colorScale: {
ranges: [{
from: 0,
to: 30,
color: window.colorMap.primary[300].hex
},
{
from: 31,
to: 60,
color: window.colorMap.primary[500].hex
},
{
from: 61,
to: 90,
color: window.colorMap.primary[700].hex
}
],
}
}
},
dataLabels: {
enabled: false
},
xaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
yaxis: {
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.2)
},
tooltip: {
theme: 'dark'
},
legend: {
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
};
const roundedHeatmapChart = new ApexCharts(
document.getElementById('rounded-heatmap-chart'),
roundedHeatmapOptions
);
roundedHeatmapChart.render();
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,770 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Line and Column Chart
if (document.getElementById('line-column-chart')) {
const lineColumnOptions = {
series: [{
name: 'Website',
type: 'column',
data: [440, 505, 414, 671, 227, 413, 201, 352, 752, 320, 257, 160]
}, {
name: 'Social Media',
type: 'line',
data: [23, 42, 35, 27, 43, 22, 17, 31, 22, 22, 12, 16]
}],
chart: {
height: 350,
type: 'line',
toolbar: {
show: true
}
},
stroke: {
width: [0, 4]
},
title: {
text: 'Traffic Sources',
align: 'left',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
dataLabels: {
enabled: true,
enabledOnSeries: [1]
},
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
xaxis: {
type: 'category',
},
yaxis: [{
title: {
text: 'Website Visits',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}, {
opposite: true,
title: {
text: 'Social Media',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const lineColumnChart = new ApexCharts(
document.getElementById('line-column-chart'),
lineColumnOptions
);
lineColumnChart.render();
}
// Multiple Y-Axis Chart
if (document.getElementById('multiple-y-axis-chart')) {
const multipleYAxisOptions = {
series: [{
name: 'Income',
type: 'column',
data: [1.4, 2.1, 4.9, 6.5, 8.2, 7.1, 5.6, 4.2, 3.5, 2.8, 2.3, 1.8]
}, {
name: 'Cashflow',
type: 'column',
data: [1.1, 3.2, 4.3, 5.8, 8.4, 6.5, 4.9, 3.9, 3.1, 2.5, 2.0, 1.5]
}, {
name: 'Revenue',
type: 'line',
data: [20, 29, 37, 46, 56, 42, 35, 30, 25, 22, 17, 15]
}],
chart: {
height: 350,
type: 'line',
stacked: false,
toolbar: {
show: true
}
},
dataLabels: {
enabled: false
},
stroke: {
width: [1, 1, 4]
},
title: {
text: 'Financial Overview (Multiple Y-axis)',
align: 'left',
},
xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
},
yaxis: [
{
axisTicks: {
show: true,
},
axisBorder: {
show: true,
color: window.colorMap.primary[500].hex
},
labels: {
style: {
colors: window.colorMap.primary[500].hex,
}
},
title: {
text: "Income (thousand $)",
style: {
color: window.colorMap.primary[500].hex,
}
},
tooltip: {
enabled: true
}
},
{
seriesName: 'Income',
opposite: true,
axisTicks: {
show: true,
},
axisBorder: {
show: true,
color: window.colorMap.primary[300].hex
},
labels: {
style: {
colors: window.colorMap.primary[300].hex,
}
},
title: {
text: "Cashflow (thousand $)",
style: {
color: window.colorMap.primary[300].hex,
}
},
},
{
seriesName: 'Revenue',
opposite: true,
axisTicks: {
show: true,
},
axisBorder: {
show: true,
color: window.colorMap.danger[500].hex
},
labels: {
style: {
colors: window.colorMap.danger[500].hex,
},
},
title: {
text: "Revenue (thousand $)",
style: {
color: window.colorMap.danger[500].hex,
}
}
},
],
tooltip: {
y: {
formatter: function (val) {
return val + " thousand $";
}
}
},
legend: {
horizontalAlign: 'center',
position: 'bottom',
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const multipleYAxisChart = new ApexCharts(
document.getElementById('multiple-y-axis-chart'),
multipleYAxisOptions
);
multipleYAxisChart.render();
}
// Line and Area Chart
if (document.getElementById('line-area-chart')) {
const lineAreaOptions = {
series: [{
name: 'TEAM A',
type: 'area',
data: [44, 55, 31, 47, 31, 43, 26, 41, 31, 47, 33]
}, {
name: 'TEAM B',
type: 'line',
data: [55, 69, 45, 61, 43, 54, 37, 52, 44, 61, 43]
}],
chart: {
height: 350,
type: 'line',
toolbar: {
show: true
}
},
stroke: {
curve: 'smooth',
width: [3, 3]
},
fill: {
type: ['gradient', 'solid'],
opacity: [0.35, 1],
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
gradientToColors: [window.colorMap.bootstrapVars.bodyBg.hex],
inverseColors: false,
opacityFrom: 0.7,
opacityTo: 0.2,
stops: [0, 100]
}
},
labels: ['Dec 01', 'Dec 02', 'Dec 03', 'Dec 04', 'Dec 05', 'Dec 06', 'Dec 07', 'Dec 08', 'Dec 09', 'Dec 10', 'Dec 11'],
markers: {
size: 0
},
title: {
text: 'Team Performance',
align: 'left',
},
yaxis: [
{
title: {
text: 'Series A',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
},
{
opposite: true,
title: {
text: 'Series B',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
},
],
tooltip: {
theme: 'dark',
shared: true,
intersect: false,
y: {
formatter: function (val) {
return val.toFixed(0) + " points";
}
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const lineAreaChart = new ApexCharts(
document.getElementById('line-area-chart'),
lineAreaOptions
);
lineAreaChart.render();
}
// Line Column Area Chart
if (document.getElementById('line-column-area-chart')) {
const lineColumnAreaOptions = {
series: [{
name: 'Revenue',
type: 'column',
data: [1.4, 2.3, 3.5, 4.2, 5.1, 4.3, 3.8, 3.2, 2.9, 3.5, 4.2, 4.8]
}, {
name: 'Free Cash Flow',
type: 'area',
data: [0.9, 1.5, 2.2, 3.1, 3.8, 3.5, 3.2, 2.8, 2.5, 2.9, 3.5, 4.0]
}, {
name: 'Operating Margin',
type: 'line',
data: [15, 18, 20, 22, 25, 23, 21, 19, 20, 22, 24, 26]
}],
chart: {
height: 350,
type: 'line',
stacked: false,
toolbar: {
show: true
}
},
stroke: {
width: [0, 2, 4],
curve: 'smooth'
},
plotOptions: {
bar: {
columnWidth: '50%'
}
},
fill: {
type: ['solid', 'gradient', 'solid'],
opacity: [0.85, 0.25, 1],
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
gradientToColors: [window.colorMap.bootstrapVars.bodyBg.hex],
inverseColors: false,
opacityFrom: 0.7,
opacityTo: 0.2,
stops: [0, 100]
}
},
markers: {
size: 0
},
xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
},
yaxis: [
{
title: {
text: 'Revenue (billions)',
style: {
color: window.colorMap.primary[500].hex
}
},
labels: {
formatter: function(val) {
return '$' + val.toFixed(1) + 'B';
},
style: {
colors: window.colorMap.primary[500].hex
}
}
},
{
seriesName: 'Free Cash Flow',
title: {
text: 'Free Cash Flow (billions)',
style: {
color: window.colorMap.primary[300].hex
}
},
opposite: true,
labels: {
formatter: function(val) {
return '$' + val.toFixed(1) + 'B';
},
style: {
colors: window.colorMap.primary[300].hex
}
}
},
{
seriesName: 'Operating Margin',
title: {
text: 'Operating Margin (%)',
style: {
color: window.colorMap.danger[500].hex
}
},
opposite: true,
labels: {
formatter: function(val) {
return val.toFixed(0) + '%';
},
style: {
colors: window.colorMap.danger[500].hex
}
}
}
],
tooltip: {
shared: true,
intersect: false,
y: [
{
formatter: function(y) {
if (typeof y !== "undefined") {
return '$' + y.toFixed(2) + ' billion';
}
return y;
}
},
{
formatter: function(y) {
if (typeof y !== "undefined") {
return '$' + y.toFixed(2) + ' billion';
}
return y;
}
},
{
formatter: function(y) {
if (typeof y !== "undefined") {
return y.toFixed(1) + '%';
}
return y;
}
}
]
},
title: {
text: 'Financial Performance Overview',
align: 'left',
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const lineColumnAreaChart = new ApexCharts(
document.getElementById('line-column-area-chart'),
lineColumnAreaOptions
);
lineColumnAreaChart.render();
}
// Candlestick Line Chart
if (document.getElementById('candlestick-line-chart')) {
const candlestickLineOptions = {
series: [{
name: 'Candle',
type: 'candlestick',
data: [
{ x: new Date(2016, 1, 1), y: [51.98, 56.29, 51.59, 53.85] },
{ x: new Date(2016, 2, 1), y: [53.66, 54.99, 51.35, 52.95] },
{ x: new Date(2016, 3, 1), y: [52.96, 53.78, 51.54, 52.48] },
{ x: new Date(2016, 4, 1), y: [52.54, 52.79, 47.88, 49.24] },
{ x: new Date(2016, 5, 1), y: [49.10, 52.86, 47.70, 52.78] },
{ x: new Date(2016, 6, 1), y: [52.83, 53.48, 50.32, 52.29] },
{ x: new Date(2016, 7, 1), y: [52.20, 54.48, 51.64, 52.58] },
{ x: new Date(2016, 8, 1), y: [52.76, 57.35, 52.15, 57.03] },
{ x: new Date(2016, 9, 1), y: [57.04, 58.15, 48.88, 56.19] },
{ x: new Date(2016, 10, 1), y: [56.09, 58.85, 55.48, 58.79] },
{ x: new Date(2016, 11, 1), y: [58.78, 59.65, 58.23, 59.05] },
{ x: new Date(2017, 0, 1), y: [59.37, 61.11, 59.35, 60.34] },
{ x: new Date(2017, 1, 1), y: [60.40, 60.52, 56.71, 56.93] },
{ x: new Date(2017, 2, 1), y: [57.02, 59.71, 56.04, 56.82] },
{ x: new Date(2017, 3, 1), y: [56.97, 59.62, 54.77, 59.30] },
{ x: new Date(2017, 4, 1), y: [59.11, 62.29, 59.10, 59.85] }
]
}, {
name: 'Moving Average',
type: 'line',
data: [
{ x: new Date(2016, 1, 1), y: 53.85 },
{ x: new Date(2016, 2, 1), y: 53.40 },
{ x: new Date(2016, 3, 1), y: 52.93 },
{ x: new Date(2016, 4, 1), y: 51.08 },
{ x: new Date(2016, 5, 1), y: 51.94 },
{ x: new Date(2016, 6, 1), y: 52.05 },
{ x: new Date(2016, 7, 1), y: 52.32 },
{ x: new Date(2016, 8, 1), y: 54.71 },
{ x: new Date(2016, 9, 1), y: 55.44 },
{ x: new Date(2016, 10, 1), y: 57.14 },
{ x: new Date(2016, 11, 1), y: 58.02 },
{ x: new Date(2017, 0, 1), y: 59.17 },
{ x: new Date(2017, 1, 1), y: 58.63 },
{ x: new Date(2017, 2, 1), y: 56.93 },
{ x: new Date(2017, 3, 1), y: 57.41 },
{ x: new Date(2017, 4, 1), y: 59.63 }
]
}],
chart: {
height: 350,
type: 'line',
toolbar: {
show: true
}
},
title: {
text: 'Stock Market Analysis',
align: 'left',
},
stroke: {
width: [0, 3]
},
xaxis: {
type: 'datetime'
},
yaxis: {
tooltip: {
enabled: true
},
labels: {
formatter: function(val) {
return '$' + val.toFixed(2);
}
}
},
tooltip: {
shared: true,
custom: [{
seriesIndex: 0,
formatter: function(opts) {
const data = opts.ctx.w.globals.initialSeries[opts.seriesIndex].data[opts.dataPointIndex];
return `
<div class="p-2">
<div>Date: ${new Date(data.x).toLocaleDateString()}</div>
<div>Open: $${data.y[0].toFixed(2)}</div>
<div>High: $${data.y[1].toFixed(2)}</div>
<div>Low: $${data.y[2].toFixed(2)}</div>
<div>Close: $${data.y[3].toFixed(2)}</div>
</div>
`;
}
}, {
seriesIndex: 1,
formatter: function(opts) {
const data = opts.ctx.w.globals.initialSeries[opts.seriesIndex].data[opts.dataPointIndex];
return `
<div class="p-2">
<div>Date: ${new Date(data.x).toLocaleDateString()}</div>
<div>MA: $${data.y.toFixed(2)}</div>
</div>
`;
}
}],
},
plotOptions: {
candlestick: {
colors: {
upward: window.colorMap.primary[500].hex,
downward: window.colorMap.danger[500].hex
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
}
};
const candlestickLineChart = new ApexCharts(
document.getElementById('candlestick-line-chart'),
candlestickLineOptions
);
candlestickLineChart.render();
}
// Line Scatter Chart
if (document.getElementById('line-scatter-chart')) {
// Generate sample data for the scatter plot
const generateScatterData = (count, min, max) => {
const data = [];
for (let i = 0; i < count; i++) {
const x = min + Math.random() * (max - min);
const y = min + Math.random() * (max - min);
data.push({ x, y });
}
return data;
};
const lineScatterOptions = {
series: [{
name: 'Trend Line',
type: 'line',
data: [
{ x: 0, y: 2 },
{ x: 1, y: 3 },
{ x: 2, y: 5 },
{ x: 3, y: 6 },
{ x: 4, y: 8 },
{ x: 5, y: 9 },
{ x: 6, y: 11 },
{ x: 7, y: 12 },
{ x: 8, y: 14 },
{ x: 9, y: 15 },
{ x: 10, y: 16 }
]
}, {
name: 'Group A',
type: 'scatter',
data: generateScatterData(20, 0, 5)
}, {
name: 'Group B',
type: 'scatter',
data: generateScatterData(15, 3, 8)
}, {
name: 'Group C',
type: 'scatter',
data: generateScatterData(10, 6, 10)
}],
chart: {
height: 350,
type: 'line',
toolbar: {
show: true
},
zoom: {
enabled: true,
type: 'xy'
}
},
stroke: {
width: [3, 0, 0, 0],
curve: 'straight'
},
fill: {
opacity: [1, 0.7, 0.7, 0.7]
},
markers: {
size: [0, 6, 6, 6],
hover: {
size: 8
}
},
title: {
text: 'Line and Scatter Combination',
align: 'left',
},
subtitle: {
text: 'Linear Trend with Data Points',
align: 'left',
},
xaxis: {
type: 'numeric',
min: 0,
max: 10,
tickAmount: 10,
title: {
text: 'X Axis',
},
},
yaxis: {
min: 0,
max: 20,
title: {
text: 'Y Axis',
},
labels: {
formatter: function(val) {
return val.toFixed(1);
},
}
},
tooltip: {
shared: false,
intersect: true,
x: {
show: true,
format: '0.0'
},
y: {
formatter: function(val) {
return val.toFixed(2);
}
},
marker: {
show: true
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.danger[500].hex]
};
const lineScatterChart = new ApexCharts(
document.getElementById('line-scatter-chart'),
lineScatterOptions
);
lineScatterChart.render();
}
});
@@ -0,0 +1,748 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Simple Pie Chart
if (document.getElementById('simple-pie-chart')) {
const simplePieOptions = {
series: [44, 55, 13, 43, 22],
chart: {
width: '100%',
height: 380,
type: 'pie',
},
title: {
text: 'Simple Pie Chart',
align: 'left',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'],
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex],
};
const simplePieChart = new ApexCharts(
document.getElementById('simple-pie-chart'),
simplePieOptions
);
simplePieChart.render();
}
// Simple Donut Chart
if (document.getElementById('simple-donut-chart')) {
const simpleDonutOptions = {
series: [44, 55, 41, 17, 15],
chart: {
width: '100%',
height: 380,
type: 'donut',
},
title: {
text: 'Simple Donut Chart',
align: 'left',
},
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'],
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex],
tooltip: {
theme: 'dark'
}
};
const simpleDonutChart = new ApexCharts(
document.getElementById('simple-donut-chart'),
simpleDonutOptions
);
simpleDonutChart.render();
}
// Donut Update Chart
if (document.getElementById('donut-update-chart')) {
const donutUpdateOptions = {
series: [44, 55, 13, 33],
chart: {
width: '100%',
height: 380,
type: 'donut',
},
title: {
text: 'Donut Update Chart (Click to Update)',
align: 'left',
},
labels: ['Team A', 'Team B', 'Team C', 'Team D'],
dataLabels: {
enabled: true,
formatter: function(val, opts) {
return opts.w.config.series[opts.seriesIndex] + '%';
}
},
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex],
};
const donutUpdateChart = new ApexCharts(
document.getElementById('donut-update-chart'),
donutUpdateOptions
);
donutUpdateChart.render();
// Add click event for updating the chart
document.getElementById('donut-update-chart').addEventListener('click', function() {
// Generate random values for the update
donutUpdateChart.updateSeries([
Math.floor(Math.random() * 91) + 10,
Math.floor(Math.random() * 91) + 10,
Math.floor(Math.random() * 91) + 10,
Math.floor(Math.random() * 91) + 10
]);
});
}
// Monochrome Pie Chart - keep only the bottom example
if (document.getElementById('monochrome-pie-chart')) {
const monochromePieOptions = {
series: [12.7, 7.6, 22.3, 27.9, 20.8, 8.5],
chart: {
width: '100%',
height: 380,
type: 'pie',
},
title: {
text: 'Monochrome Pie Chart',
align: 'left',
},
labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
theme: {
monochrome: {
enabled: true,
color: window.colorMap.primary[300].hex,
shadeTo: 'dark',
shadeIntensity: 0.65
}
},
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
dataLabels: {
formatter: function(val, opts) {
return val.toFixed(1) + '%';
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}]
};
const monochromePieChart = new ApexCharts(
document.getElementById('monochrome-pie-chart'),
monochromePieOptions
);
monochromePieChart.render();
}
// Gradient Donut Chart - make gradient much more visible
if (document.getElementById('gradient-donut-chart')) {
const gradientDonutOptions = {
series: [25.5, 32, 21.8, 9.9, 8.7],
chart: {
width: '100%',
height: 380,
type: 'donut',
},
plotOptions: {
pie: {
startAngle: 0,
endAngle: 360,
donut: {
size: '65%',
labels: {
show: true,
name: {
show: true,
},
value: {
show: true,
color: window.colorMap.bootstrapVars.bodyColor.hex,
formatter: function (val) {
return val.toFixed(1) + '%';
}
},
total: {
show: true,
label: 'Total',
color: window.colorMap.bootstrapVars.bodyColor.hex,
formatter: function (w) {
return w.globals.seriesTotals.reduce((a, b) => {
return a + b;
}, 0).toFixed(1) + '%';
}
}
}
}
}
},
dataLabels: {
enabled: false
},
fill: {
type: 'gradient',
gradient: {
gradientToColors: ['var(--primary-900)', 'var(--success-900)', 'var(--info-900)', 'var(--danger-900)', 'var(--warning-900)'],
shade: 'dark',
type: 'vertical',
inverseColors: true,
opacityFrom: 1,
opacityTo: 0.6,
stops: [0, 100]
}
},
stroke: {
width: 0
},
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
title: {
text: 'Gradient Donut Chart',
align: 'left',
},
labels: ['Products', 'Services', 'Support', 'Marketing', 'R&D'],
colors: [window.colorMap.primary[500].hex, window.colorMap.success[500].hex, window.colorMap.info[500].hex, window.colorMap.danger[500].hex, window.colorMap.warning[500].hex],
};
const gradientDonutChart = new ApexCharts(
document.getElementById('gradient-donut-chart'),
gradientDonutOptions
);
gradientDonutChart.render();
}
// Donut with Pattern chart
if (document.getElementById('donut-with-pattern-chart')) {
const patternDonutOptions = {
series: [44, 55, 41, 17, 15],
chart: {
width: '100%',
height: 380,
type: 'donut',
dropShadow: {
enabled: true,
color: '#111',
top: -1,
left: 3,
blur: 3,
opacity: 0.2
}
},
stroke: {
width: 0,
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
show: true,
color: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '14px'
},
value: {
show: true,
fontSize: '16px',
formatter: function (val) {
return val + '%';
},
color: window.colorMap.bootstrapVars.bodyColor.hex,
background: {
enabled: true,
foreColor: window.colorMap.bootstrapVars.bodyColor.hex
}
},
total: {
show: true,
showAlways: true,
label: 'Total',
fontSize: '16px',
fontWeight: 600,
color: window.colorMap.bootstrapVars.bodyColor.hex,
formatter: function (w) {
return w.globals.seriesTotals.reduce((a, b) => {
return a + b;
}, 0);
}
}
}
}
}
},
labels: ['Comedy', 'Action', 'SciFi', 'Drama', 'Horror'],
dataLabels: {
enabled: true,
dropShadow: {
blur: 3,
opacity: 0.8
},
background: {
enabled: true,
foreColor: '#000',
padding: 4,
borderRadius: 2,
borderWidth: 1,
opacity: 0.9
},
style: {
fontSize: '14px',
fontWeight: 'bold',
colors: ['#fff']
}
},
fill: {
type: 'pattern',
opacity: 1,
pattern: {
enabled: true,
style: ['verticalLines', 'squares', 'horizontalLines', 'circles', 'slantedLines'],
}
},
states: {
hover: {
filter: 'none'
}
},
theme: {
palette: 'palette2'
},
colors: [
window.colorMap.primary[500].hex,
window.colorMap.info[500].hex,
window.colorMap.success[500].hex,
window.colorMap.danger[500].hex,
window.colorMap.warning[500].hex
],
title: {
text: 'Favorite Movie Genre',
align: 'left',
style: {
color: window.colorMap.bootstrapVars.bodyColor.hex
}
},
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}]
};
const patternDonutChart = new ApexCharts(
document.getElementById('donut-with-pattern-chart'),
patternDonutOptions
);
patternDonutChart.render();
}
// Basic Pie Chart
if (document.getElementById('basic-pie-chart')) {
const basicPieOptions = {
series: [44, 55, 13, 43, 22],
chart: {
width: 380,
type: 'pie',
toolbar: {
show: true
}
},
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'],
title: {
text: 'Basic Pie Chart',
align: 'left'
},
tooltip: {
theme: 'dark'
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex]
};
const basicPieChart = new ApexCharts(
document.getElementById('basic-pie-chart'),
basicPieOptions
);
basicPieChart.render();
}
// Pie Chart with Labels
if (document.getElementById('pie-with-labels-chart')) {
const pieWithLabelsOptions = {
series: [44, 55, 13, 43, 22],
chart: {
width: 380,
type: 'pie',
toolbar: {
show: true
}
},
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'],
title: {
text: 'Pie Chart with Custom Labels',
align: 'left'
},
tooltip: {
theme: 'dark'
},
dataLabels: {
enabled: true,
formatter: function(val, opts) {
return opts.w.config.series[opts.seriesIndex] + ' (' + val.toFixed(1) + '%)';
},
style: {
fontSize: '12px',
fontWeight: 'normal'
},
dropShadow: {
enabled: false
}
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex]
};
const pieWithLabelsChart = new ApexCharts(
document.getElementById('pie-with-labels-chart'),
pieWithLabelsOptions
);
pieWithLabelsChart.render();
}
// Basic Donut Chart
if (document.getElementById('basic-donut-chart')) {
const basicDonutOptions = {
series: [44, 55, 41, 17, 15],
chart: {
width: 380,
type: 'donut',
toolbar: {
show: true
}
},
labels: ['Products', 'Services', 'Support', 'Marketing', 'R&D'],
title: {
text: 'Basic Donut Chart',
align: 'left'
},
tooltip: {
theme: 'dark'
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex]
};
const basicDonutChart = new ApexCharts(
document.getElementById('basic-donut-chart'),
basicDonutOptions
);
basicDonutChart.render();
}
// Donut Chart with Pattern
if (document.getElementById('donut-pattern-chart')) {
const donutPatternOptions = {
series: [44, 55, 41, 17, 15],
chart: {
width: 380,
type: 'donut',
dropShadow: {
enabled: true,
color: '#111',
top: -1,
left: 3,
blur: 3,
opacity: 0.2
},
toolbar: {
show: true
}
},
labels: ['Products', 'Services', 'Support', 'Marketing', 'R&D'],
title: {
text: 'Donut Chart with Pattern',
align: 'left'
},
stroke: {
width: 0,
},
tooltip: {
theme: 'dark'
},
fill: {
type: 'pattern',
opacity: 1,
pattern: {
enabled: true,
style: ['verticalLines', 'squares', 'horizontalLines', 'circles', 'slantedLines']
}
},
states: {
hover: {
filter: {
type: 'none'
}
}
},
legend: {
position: 'bottom'
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 300
},
legend: {
position: 'bottom'
}
}
}],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex]
};
const donutPatternChart = new ApexCharts(
document.getElementById('donut-pattern-chart'),
donutPatternOptions
);
donutPatternChart.render();
}
// Semi Donut Chart - fix spacing issue
if (document.getElementById('semi-donut-chart')) {
const semiDonutOptions = {
series: [44, 55, 41, 17, 15],
chart: {
width: '100%',
height: 300,
type: 'donut',
},
title: {
text: 'Semi Donut Chart',
align: 'left',
},
plotOptions: {
pie: {
startAngle: -90,
endAngle: 90,
offsetY: -20,
donut: {
size: '65%',
labels: {
show: true,
name: {
show: true,
},
value: {
show: true,
formatter: function (val) {
return val + '%';
}
}
}
}
}
},
grid: {
padding: {
bottom: -100
}
},
legend: {
position: 'right',
offsetY: 40,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'],
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex, window.colorMap.primary[600].hex],
tooltip: {
theme: 'dark'
}
};
const semiDonutChart = new ApexCharts(
document.getElementById('semi-donut-chart'),
semiDonutOptions
);
semiDonutChart.render();
}
// Pie with Image Chart
if (document.getElementById('pie-with-image-chart')) {
const pieWithImageOptions = {
series: [44, 33, 54, 45],
chart: {
width: '100%',
height: 380,
type: 'pie',
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex],
fill: {
type: 'image',
opacity: 0.85,
image: {
src: ['img/demo/gallery/15.jpg', 'img/demo/gallery/7.jpg', 'img/demo/gallery/10.jpg', 'img/demo/gallery/20.jpg']
}
},
stroke: {
width: 4
},
dataLabels: {
enabled: true,
style: {
colors: ['#111']
},
background: {
enabled: true,
foreColor: '#fff',
borderWidth: 0
}
},
legend: {
position: 'right',
offsetY: 0,
height: 230,
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
},
title: {
text: "Image Fill Pie Chart",
align: "left",
}
};
const pieWithImageChart = new ApexCharts(
document.getElementById('pie-with-image-chart'),
pieWithImageOptions
);
pieWithImageChart.render();
}
});
@@ -0,0 +1,185 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function () {
'use strict';
// Basic Polar Area Chart
if (document.getElementById('basic-polar-area-chart')) {
const basicPolarAreaOptions = {
series: [14, 23, 21, 17, 15, 10, 12, 17, 21],
chart: {
height: 350,
type: 'polarArea',
toolbar: {
show: true
}
},
stroke: {
colors: [window.colorMap.bootstrapVars.bodyBg.hex]
},
fill: {
opacity: 0.8
},
responsive: [{
breakpoint: 480,
options: {
chart: {
width: 200
},
legend: {
position: 'bottom',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}
}],
labels: ['Category A', 'Category B', 'Category C', 'Category D', 'Category E', 'Category F', 'Category G', 'Category H', 'Category I'],
colors: [
window.colorMap.primary[50].hex,
window.colorMap.primary[100].hex,
window.colorMap.primary[200].hex,
window.colorMap.primary[300].hex,
window.colorMap.primary[400].hex,
window.colorMap.primary[500].hex,
window.colorMap.primary[600].hex,
window.colorMap.info[50].hex,
window.colorMap.info[100].hex
],
};
const basicPolarAreaChart = new ApexCharts(
document.getElementById('basic-polar-area-chart'),
basicPolarAreaOptions
);
basicPolarAreaChart.render();
}
// Polar Area with Monochrome Theme
if (document.getElementById('monochrome-polar-area-chart')) {
const monochromePolarAreaOptions = {
series: [42, 47, 52, 58, 65],
chart: {
height: 350,
type: 'polarArea',
toolbar: {
show: true
}
},
labels: ['Market Research', 'Direct Sales', 'Email Marketing', 'Social Media', 'Referrals'],
fill: {
opacity: 1
},
stroke: {
width: 1,
colors: undefined
},
yaxis: {
show: false
},
legend: {
position: 'bottom',
},
plotOptions: {
polarArea: {
rings: {
strokeWidth: 0
},
spokes: {
strokeWidth: 0
},
}
},
theme: {
monochrome: {
enabled: true,
shadeTo: 'light',
shadeIntensity: 0.6,
color: window.colorMap.primary[500].hex
}
}
};
const monochromePolarAreaChart = new ApexCharts(
document.getElementById('monochrome-polar-area-chart'),
monochromePolarAreaOptions
);
monochromePolarAreaChart.render();
}
// Polar Area with Gradient
if (document.getElementById('gradient-polar-area-chart')) {
const gradientPolarAreaOptions = {
series: [30, 40, 35, 50, 49, 60, 70, 91, 125],
chart: {
height: 350,
type: 'polarArea',
toolbar: {
show: true
}
},
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September'],
fill: {
opacity: 1,
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.5,
gradientToColors: undefined,
inverseColors: false,
opacityFrom: 0.8,
opacityTo: 0.5,
stops: [0, 100]
}
},
stroke: {
width: 1,
colors: [window.colorMap.bootstrapVars.bodyBg.hex]
},
yaxis: {
show: false
},
legend: {
position: 'bottom',
},
plotOptions: {
polarArea: {
rings: {
strokeWidth: 1,
strokeColor: window.colorMap.bootstrapVars.bodyBg.hex
},
spokes: {
strokeWidth: 1,
connectorColors: window.colorMap.bootstrapVars.bodyBg.hex
}
}
},
tooltip: {
y: {
formatter: function (val) {
return "$" + val + "K";
}
}
},
colors: [
window.colorMap.primary[50].hex,
window.colorMap.primary[100].hex,
window.colorMap.primary[200].hex,
window.colorMap.primary[300].hex,
window.colorMap.primary[400].hex,
window.colorMap.primary[500].hex,
window.colorMap.primary[600].hex,
window.colorMap.primary[700].hex,
window.colorMap.primary[800].hex
]
};
const gradientPolarAreaChart = new ApexCharts(
document.getElementById('gradient-polar-area-chart'),
gradientPolarAreaOptions
);
gradientPolarAreaChart.render();
}
});
@@ -0,0 +1,161 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Radar Chart
if (document.getElementById('basic-radar-chart')) {
const basicRadarOptions = {
series: [{
name: 'Basic Radar',
data: [80, 50, 30, 40, 100, 20],
}],
chart: {
height: 350,
type: 'radar',
toolbar: {
show: true
}
},
xaxis: {
categories: ['January', 'February', 'March', 'April', 'May', 'June'],
},
colors: [window.colorMap.primary[500].hex],
tooltip: {
y: {
formatter: function(val) {
return val;
}
}
},
markers: {
size: 4,
colors: [window.colorMap.primary[500].hex],
strokeColors: '#fff',
strokeWidth: 2
},
};
const basicRadarChart = new ApexCharts(
document.getElementById('basic-radar-chart'),
basicRadarOptions
);
basicRadarChart.render();
}
// Multi-series Radar Chart
if (document.getElementById('multi-series-radar-chart')) {
const multiSeriesRadarOptions = {
series: [{
name: 'Series 1',
data: [80, 50, 30, 40, 100, 20],
}, {
name: 'Series 2',
data: [20, 30, 40, 80, 20, 80],
}, {
name: 'Series 3',
data: [44, 76, 78, 13, 43, 10],
}],
chart: {
height: 350,
type: 'radar',
dropShadow: {
enabled: true,
blur: 1,
left: 1,
top: 1
},
toolbar: {
show: true
}
},
stroke: {
width: 2
},
fill: {
opacity: 0.1
},
markers: {
size: 0
},
xaxis: {
categories: ['January', 'February', 'March', 'April', 'May', 'June'],
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[300].hex, window.colorMap.primary[200].hex],
tooltip: {
y: {
formatter: function(val) {
return val;
}
}
},
legend: {
position: 'top',
}
};
const multiSeriesRadarChart = new ApexCharts(
document.getElementById('multi-series-radar-chart'),
multiSeriesRadarOptions
);
multiSeriesRadarChart.render();
}
// Polygon Fill Radar Chart
if (document.getElementById('polygon-fill-radar-chart')) {
const polygonFillRadarOptions = {
series: [{
name: 'Series 1',
data: [20, 100, 40, 30, 50, 80, 33],
}],
chart: {
height: 350,
type: 'radar',
toolbar: {
show: true
}
},
dataLabels: {
enabled: true
},
plotOptions: {
radar: {
size: 140,
polygons: {
strokeColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.2),
fill: {
colors: [window.colorMap.bootstrapVars.bodyBg.rgba(0.1), '#fff']
}
}
}
},
colors: [window.colorMap.primary[500].hex],
markers: {
size: 4,
colors: [window.colorMap.primary[500].hex],
strokeColors: '#fff',
strokeWidth: 2
},
tooltip: {
y: {
formatter: function(val) {
return val;
}
}
},
xaxis: {
categories: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
},
yaxis: {
tickAmount: 7,
}
};
const polygonFillRadarChart = new ApexCharts(
document.getElementById('polygon-fill-radar-chart'),
polygonFillRadarOptions
);
polygonFillRadarChart.render();
}
});
@@ -0,0 +1,452 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic RadialBar Chart
if (document.getElementById('basic-radialbar-chart')) {
const basicRadialbarOptions = {
series: [70],
chart: {
height: 350,
type: 'radialBar',
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
hollow: {
size: '70%',
}
},
},
labels: ['Progress'],
colors: [window.colorMap.success[500].hex]
};
const basicRadialbarChart = new ApexCharts(
document.getElementById('basic-radialbar-chart'),
basicRadialbarOptions
);
basicRadialbarChart.render();
}
// Multiple RadialBar Chart
if (document.getElementById('multiple-radialbar-chart')) {
const multipleRadialbarOptions = {
series: [44, 55, 67, 83],
chart: {
height: 350,
type: 'radialBar',
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
dataLabels: {
name: {
fontSize: '22px',
},
value: {
fontSize: '16px',
},
total: {
show: true,
label: 'Total',
formatter: function(w) {
return Math.round(w.globals.seriesTotals.reduce((a, b) => a + b, 0) / w.globals.series.length) + '%';
}
}
}
}
},
labels: ['Apples', 'Oranges', 'Bananas', 'Berries'],
colors: [window.colorMap.primary[500].hex, window.colorMap.info[500].hex, window.colorMap.success[500].hex, window.colorMap.warning[500].hex]
};
const multipleRadialbarChart = new ApexCharts(
document.getElementById('multiple-radialbar-chart'),
multipleRadialbarOptions
);
multipleRadialbarChart.render();
}
// Custom Angle Circle Chart
if (document.getElementById('custom-angle-circle-chart')) {
const customAngleOptions = {
series: [76, 67, 61, 90],
chart: {
height: 350,
type: 'radialBar',
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
offsetY: 0,
startAngle: 0,
endAngle: 270,
hollow: {
margin: 5,
size: '30%',
background: 'transparent',
image: undefined,
},
dataLabels: {
name: {
show: false,
},
value: {
show: false,
}
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.info[500].hex, window.colorMap.success[500].hex, window.colorMap.warning[500].hex],
labels: ['Vimeo', 'Messenger', 'Facebook', 'LinkedIn'],
legend: {
show: true,
floating: true,
fontSize: '16px',
position: 'left',
offsetX: 160,
offsetY: 15,
labels: {
useSeriesColors: true,
},
markers: {
size: 0
},
formatter: function(seriesName, opts) {
return seriesName + ": " + opts.w.globals.series[opts.seriesIndex] + "%";
},
itemMargin: {
vertical: 3
}
},
responsive: [{
breakpoint: 480,
options: {
legend: {
show: false
}
}
}]
};
const customAngleChart = new ApexCharts(
document.getElementById('custom-angle-circle-chart'),
customAngleOptions
);
customAngleChart.render();
}
// Gradient RadialBar Chart
if (document.getElementById('gradient-radialbar-chart')) {
const gradientRadialbarOptions = {
series: [75],
chart: {
height: 350,
type: 'radialBar',
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
startAngle: -135,
endAngle: 225,
hollow: {
margin: 0,
size: '70%',
background: '#fff',
image: undefined,
imageOffsetX: 0,
imageOffsetY: 0,
position: 'front',
dropShadow: {
enabled: true,
top: 3,
left: 0,
blur: 4,
opacity: 0.24
}
},
track: {
background: '#fff',
strokeWidth: '67%',
margin: 0,
dropShadow: {
enabled: true,
top: -3,
left: 0,
blur: 4,
opacity: 0.35
}
},
dataLabels: {
show: true,
name: {
offsetY: -10,
show: true,
color: '#888',
fontSize: '17px'
},
value: {
formatter: function(val) {
return parseInt(val) + '%';
},
color: '#111',
fontSize: '36px',
show: true,
}
}
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'horizontal',
shadeIntensity: 0.5,
gradientToColors: [window.colorMap.primary[500].hex],
inverseColors: true,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100]
}
},
stroke: {
lineCap: 'round'
},
labels: ['Percent'],
};
const gradientRadialbarChart = new ApexCharts(
document.getElementById('gradient-radialbar-chart'),
gradientRadialbarOptions
);
gradientRadialbarChart.render();
}
// RadialBar with Image
if (document.getElementById('radialbar-with-image-chart')) {
const radialbarWithImageOptions = {
series: [67],
chart: {
height: 350,
type: 'radialBar',
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
startAngle: -135,
endAngle: 225,
hollow: {
margin: 0,
size: '70%',
background: '#fff',
image: 'img/logo.png',
imageWidth: 150,
imageHeight: 150,
imageClipped: false,
position: 'front',
dropShadow: {
enabled: true,
top: 3,
left: 0,
blur: 4,
opacity: 0.24
}
},
track: {
background: '#fff',
strokeWidth: '67%',
margin: 0,
dropShadow: {
enabled: true,
top: -3,
left: 0,
blur: 4,
opacity: 0.35
}
},
dataLabels: {
show: true,
name: {
offsetY: -10,
show: true,
color: '#888',
fontSize: '17px'
},
value: {
formatter: function(val) {
return parseInt(val) + '%';
},
color: '#111',
fontSize: '36px',
show: true,
}
}
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'horizontal',
shadeIntensity: 0.5,
gradientToColors: [window.colorMap.primary[600].hex],
inverseColors: true,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100]
}
},
stroke: {
lineCap: 'round'
},
labels: ['Performance'],
};
const radialbarWithImageChart = new ApexCharts(
document.getElementById('radialbar-with-image-chart'),
radialbarWithImageOptions
);
radialbarWithImageChart.render();
}
// Stroked Gauge Chart
if (document.getElementById('stroked-gauge-chart')) {
const strokedGaugeOptions = {
series: [67],
chart: {
height: 350,
type: 'radialBar',
offsetY: -10,
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
startAngle: -135,
endAngle: 135,
dataLabels: {
name: {
fontSize: '16px',
color: undefined,
offsetY: 120
},
value: {
offsetY: 76,
fontSize: '22px',
color: undefined,
formatter: function(val) {
return val + "%";
}
}
}
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
shadeIntensity: 0.15,
inverseColors: false,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 50, 65, 91]
},
},
stroke: {
dashArray: 4
},
colors: [window.colorMap.primary[500].hex],
labels: ['Performance'],
};
const strokedGaugeChart = new ApexCharts(
document.getElementById('stroked-gauge-chart'),
strokedGaugeOptions
);
strokedGaugeChart.render();
}
// Semi Circle Gauge Chart
if (document.getElementById('semi-circle-gauge-chart')) {
const semiCircleGaugeOptions = {
series: [76],
chart: {
type: 'radialBar',
height: 350,
offsetY: -20,
sparkline: {
enabled: true
},
toolbar: {
show: true
}
},
plotOptions: {
radialBar: {
startAngle: -90,
endAngle: 90,
track: {
background: "#e7e7e7",
strokeWidth: '97%',
margin: 5,
dropShadow: {
enabled: true,
top: 2,
left: 0,
color: '#999',
opacity: 1,
blur: 2
}
},
dataLabels: {
name: {
show: false
},
value: {
offsetY: -2,
fontSize: '22px'
}
}
}
},
grid: {
padding: {
top: -10
}
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
shadeIntensity: 0.4,
inverseColors: false,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 50, 53, 91]
},
},
colors: [window.colorMap.primary[500].hex],
labels: ['Average Results'],
};
const semiCircleGaugeChart = new ApexCharts(
document.getElementById('semi-circle-gauge-chart'),
semiCircleGaugeOptions
);
semiCircleGaugeChart.render();
}
});
@@ -0,0 +1,599 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Range Area Chart
if (document.getElementById('basic-range-area-chart')) {
const basicRangeAreaOptions = {
series: [
{
name: 'Temperature Range',
data: [
{ x: 'Jan', y: [5, 15] },
{ x: 'Feb', y: [7, 18] },
{ x: 'Mar', y: [10, 22] },
{ x: 'Apr', y: [15, 25] },
{ x: 'May', y: [18, 30] },
{ x: 'Jun', y: [22, 35] },
{ x: 'Jul', y: [25, 38] },
{ x: 'Aug', y: [23, 36] },
{ x: 'Sep', y: [20, 32] },
{ x: 'Oct', y: [15, 26] },
{ x: 'Nov', y: [10, 20] },
{ x: 'Dec', y: [6, 16] }
]
}
],
chart: {
type: 'rangeArea',
height: 350,
zoom: {
enabled: false
}
},
colors: ['#3F80EA'],
stroke: {
curve: 'straight',
color: 'var(--primary-500)',
width: 1
},
markers: {
hover: {
sizeOffset: 5
}
},
dataLabels: {
enabled: false
},
yaxis: {
labels: {
formatter: function(val) {
return val + '°C';
}
}
},
tooltip: {
shared: false,
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
return '<div class="p-2">' +
'<span>Month: ' + data.x + '</span><br>' +
'<span>Min: ' + data.y[0] + '°C</span><br>' +
'<span>Max: ' + data.y[1] + '°C</span>' +
'</div>';
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const basicRangeAreaChart = new ApexCharts(
document.getElementById('basic-range-area-chart'),
basicRangeAreaOptions
);
basicRangeAreaChart.render();
}
// Range Area with Datetime X-axis
if (document.getElementById('datetime-range-area-chart')) {
const datetimeRangeAreaOptions = {
series: [
{
name: 'Price Range',
data: [
{
x: new Date('2019-01-01').getTime(),
y: [30, 45]
},
{
x: new Date('2019-02-01').getTime(),
y: [35, 50]
},
{
x: new Date('2019-03-01').getTime(),
y: [40, 55]
},
{
x: new Date('2019-04-01').getTime(),
y: [45, 62]
},
{
x: new Date('2019-05-01').getTime(),
y: [50, 70]
},
{
x: new Date('2019-06-01').getTime(),
y: [55, 72]
},
{
x: new Date('2019-07-01').getTime(),
y: [58, 75]
},
{
x: new Date('2019-08-01').getTime(),
y: [60, 80]
},
{
x: new Date('2019-09-01').getTime(),
y: [55, 75]
},
{
x: new Date('2019-10-01').getTime(),
y: [50, 65]
},
{
x: new Date('2019-11-01').getTime(),
y: [45, 60]
},
{
x: new Date('2019-12-01').getTime(),
y: [40, 55]
},
{
x: new Date('2020-01-01').getTime(),
y: [45, 60]
},
{
x: new Date('2020-02-01').getTime(),
y: [50, 65]
},
{
x: new Date('2020-03-01').getTime(),
y: [55, 70]
},
{
x: new Date('2020-04-01').getTime(),
y: [60, 75]
},
{
x: new Date('2020-05-01').getTime(),
y: [65, 80]
},
{
x: new Date('2020-06-01').getTime(),
y: [70, 85]
},
{
x: new Date('2020-07-01').getTime(),
y: [75, 90]
},
{
x: new Date('2020-08-01').getTime(),
y: [80, 95]
},
{
x: new Date('2020-09-01').getTime(),
y: [75, 90]
},
{
x: new Date('2020-10-01').getTime(),
y: [70, 85]
},
{
x: new Date('2020-11-01').getTime(),
y: [65, 80]
},
{
x: new Date('2020-12-01').getTime(),
y: [60, 75]
}
]
}
],
chart: {
height: 350,
type: 'rangeArea',
toolbar: {
show: true
}
},
stroke: {
curve: 'smooth',
width: 2
},
dataLabels: {
enabled: false
},
markers: {
size: 0,
hover: {
size: 6
}
},
colors: [window.colorMap.info[500].hex],
tooltip: {
y: {
formatter: function(val) {
return "$" + val;
}
}
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.6,
opacityTo: 0.2,
stops: [0, 100]
}
},
xaxis: {
type: 'datetime'
},
yaxis: {
title: {
text: 'Price ($)'
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const datetimeRangeAreaChart = new ApexCharts(
document.getElementById('datetime-range-area-chart'),
datetimeRangeAreaOptions
);
datetimeRangeAreaChart.render();
}
// Multiple Range Area Charts
if (document.getElementById('multiple-range-area-chart')) {
const multipleRangeAreaOptions = {
series: [
{
name: 'Temperature New York',
data: [
{
x: 'Jan',
y: [-5, 5]
},
{
x: 'Feb',
y: [-3, 7]
},
{
x: 'Mar',
y: [2, 12]
},
{
x: 'Apr',
y: [8, 18]
},
{
x: 'May',
y: [15, 25]
},
{
x: 'Jun',
y: [20, 30]
},
{
x: 'Jul',
y: [22, 32]
},
{
x: 'Aug',
y: [21, 31]
},
{
x: 'Sep',
y: [16, 26]
},
{
x: 'Oct',
y: [10, 20]
},
{
x: 'Nov',
y: [5, 15]
},
{
x: 'Dec',
y: [-2, 8]
}
]
},
{
name: 'Temperature London',
data: [
{
x: 'Jan',
y: [2, 8]
},
{
x: 'Feb',
y: [3, 9]
},
{
x: 'Mar',
y: [5, 12]
},
{
x: 'Apr',
y: [7, 15]
},
{
x: 'May',
y: [10, 18]
},
{
x: 'Jun',
y: [14, 22]
},
{
x: 'Jul',
y: [16, 24]
},
{
x: 'Aug',
y: [16, 24]
},
{
x: 'Sep',
y: [14, 22]
},
{
x: 'Oct',
y: [10, 18]
},
{
x: 'Nov',
y: [6, 14]
},
{
x: 'Dec',
y: [4, 10]
}
]
}
],
chart: {
height: 350,
type: 'rangeArea',
toolbar: {
show: true
}
},
stroke: {
curve: 'straight',
width: [2, 2]
},
dataLabels: {
enabled: false
},
markers: {
hover: {
sizeOffset: 5
}
},
colors: [window.colorMap.success[500].hex, window.colorMap.warning[500].hex],
fill: {
opacity: [0.5, 0.5],
type: 'solid'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'category'
},
yaxis: {
title: {
text: 'Temperature (°C)'
},
tooltip: {
enabled: true
}
}
};
const multipleRangeAreaChart = new ApexCharts(
document.getElementById('multiple-range-area-chart'),
multipleRangeAreaOptions
);
multipleRangeAreaChart.render();
}
// Combo Range Area Chart
if (document.getElementById('combo-range-area-chart')) {
const comboRangeAreaOptions = {
series: [
{
name: 'Temperature Range',
type: 'rangeArea',
data: [
{ x: 'Jan', y: [5, 15] },
{ x: 'Feb', y: [7, 18] },
{ x: 'Mar', y: [10, 22] },
{ x: 'Apr', y: [15, 25] },
{ x: 'May', y: [18, 30] },
{ x: 'Jun', y: [22, 35] },
{ x: 'Jul', y: [25, 38] },
{ x: 'Aug', y: [23, 36] },
{ x: 'Sep', y: [20, 32] },
{ x: 'Oct', y: [15, 26] },
{ x: 'Nov', y: [10, 20] },
{ x: 'Dec', y: [6, 16] }
]
},
{
name: 'Average Temperature',
type: 'line',
data: [
{ x: 'Jan', y: 10 },
{ x: 'Feb', y: 12.5 },
{ x: 'Mar', y: 16 },
{ x: 'Apr', y: 20 },
{ x: 'May', y: 24 },
{ x: 'Jun', y: 28.5 },
{ x: 'Jul', y: 31.5 },
{ x: 'Aug', y: 29.5 },
{ x: 'Sep', y: 26 },
{ x: 'Oct', y: 20.5 },
{ x: 'Nov', y: 15 },
{ x: 'Dec', y: 11 }
]
},
{
name: 'Precipitation',
type: 'column',
data: [
{ x: 'Jan', y: 76 },
{ x: 'Feb', y: 85 },
{ x: 'Mar', y: 101 },
{ x: 'Apr', y: 98 },
{ x: 'May', y: 87 },
{ x: 'Jun', y: 105 },
{ x: 'Jul', y: 91 },
{ x: 'Aug', y: 114 },
{ x: 'Sep', y: 94 },
{ x: 'Oct', y: 86 },
{ x: 'Nov', y: 115 },
{ x: 'Dec', y: 91 }
]
}
],
chart: {
height: 350,
type: 'rangeArea',
stacked: false
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex, window.colorMap.primary[500].hex],
stroke: {
curve: 'smooth',
width: [0, 3, 0]
},
fill: {
opacity: [0.2, 1, 1]
},
markers: {
size: [0, 4, 0],
strokeWidth: 2,
hover: {
size: 6
}
},
dataLabels: {
enabled: false
},
plotOptions: {
bar: {
columnWidth: '40%',
borderRadius: 2
}
},
tooltip: {
shared: true,
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[dataPointIndex];
const rangeData = w.globals.initialSeries[0].data[dataPointIndex];
const avgTemp = w.globals.initialSeries[1].data[dataPointIndex].y;
const precip = w.globals.initialSeries[2].data[dataPointIndex].y;
return '<div class="p-2">' +
'<span>Month: ' + month + '</span><br>' +
'<span><span style="color:#3F80EA">■</span> Min-Max Temp: ' + rangeData.y[0] + '-' + rangeData.y[1] + '°C</span><br>' +
'<span><span style="color:#E63946">■</span> Avg Temp: ' + avgTemp + '°C</span><br>' +
'<span><span style="color:#6D9EEB">■</span> Precipitation: ' + precip + ' mm</span>' +
'</div>';
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'category',
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
},
yaxis: [
{
seriesName: 'Temperature Range',
title: {
text: 'Temperature (°C)',
style: {
color: window.colorMap.danger[500].hex,
}
},
labels: {
formatter: function(val) {
return val + '°C';
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
{
seriesName: 'Average Temperature',
show: false
},
{
opposite: true,
seriesName: 'Precipitation',
title: {
text: 'Precipitation (mm)',
style: {
color: window.colorMap.primary[500].hex,
}
},
labels: {
formatter: function(val) {
return val + ' mm';
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}
]
};
const comboRangeAreaChart = new ApexCharts(
document.getElementById('combo-range-area-chart'),
comboRangeAreaOptions
);
comboRangeAreaChart.render();
}
});
@@ -0,0 +1,479 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Scatter Chart
if (document.getElementById('basic-scatter-chart')) {
const basicScatterOptions = {
series: [{
name: "Sample A",
data: [
[16.4, 5.4], [21.7, 2.1], [25.4, 3.0], [19.0, 2.5], [10.9, 1.4],
[13.6, 3.2], [10.9, 7.1], [10.9, 0], [10.9, 8.7], [16.4, 0],
[16.4, 1.8], [13.6, 0.3], [13.6, 0], [29.9, 0], [27.1, 2.3],
[16.4, 0], [13.6, 3.7], [10.9, 5.2], [16.4, 6.5], [10.9, 0],
[24.5, 7.1], [10.9, 0], [8.1, 4.7], [19.0, 0], [21.7, 1.8],
[27.1, 0], [24.5, 0], [27.1, 0], [29.9, 1.5], [27.1, 0.8],
[22.1, 2.0]
]
},{
name: "Sample B",
data: [
[36.4, 13.4], [1.7, 11.1], [5.4, 8.0], [9.0, 17.5], [1.9, 4.4],
[3.6, 12.2], [1.9, 14.1], [1.9, 9.0], [1.9, 13.7], [1.4, 7.0],
[6.4, 8.8], [3.6, 4.3], [1.6, 10.0], [9.9, 2.0], [7.1, 15.3],
[1.4, 0], [3.6, 13.7], [1.9, 15.2], [6.4, 16.5], [0.9, 10.0],
[4.5, 17.1], [10.9, 10.0], [0.1, 14.7], [9.0, 10.0], [12.7, 11.8],
[2.1, 10.0], [2.5, 10.0], [27.1, 10.0], [2.9, 11.5], [7.1, 10.8],
[2.1, 12.0]
]
}],
chart: {
height: 350,
type: 'scatter',
zoom: {
enabled: true,
type: 'xy'
},
toolbar: {
show: true
}
},
xaxis: {
tickAmount: 10
},
yaxis: {
tickAmount: 7
},
title: {
text: 'Basic Scatter Chart',
align: 'left'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
row: {
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.05), 'transparent'],
opacity: 0.5
},
xaxis: {
lines: {
show: false
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
markers: {
size: 6
}
};
const basicScatterChart = new ApexCharts(
document.getElementById('basic-scatter-chart'),
basicScatterOptions
);
basicScatterChart.render();
}
// Scatter Chart with Categories
if (document.getElementById('category-scatter-chart')) {
const categoryScatterOptions = {
series: [{
name: "TEAM A",
data: [
[1, 5.4], [2, 7.1], [3, 9.0], [4, 9.5], [5, 8.9],
[6, 12.2], [7, 14.3], [8, 16.5], [9, 18.0], [10, 15.8]
]
},{
name: "TEAM B",
data: [
[1, 3.2], [2, 4.5], [3, 5.8], [4, 6.3], [5, 7.0],
[6, 7.5], [7, 8.0], [8, 8.5], [9, 9.1], [10, 9.8]
]
},{
name: "TEAM C",
data: [
[1, 11.1], [2, 12.5], [3, 11.8], [4, 10.5], [5, 12.0],
[6, 13.5], [7, 11.7], [8, 10.8], [9, 13.2], [10, 14.5]
]
}],
chart: {
height: 350,
type: 'scatter',
zoom: {
enabled: true,
type: 'xy'
},
toolbar: {
show: true
}
},
xaxis: {
type: 'category',
tickAmount: 10,
labels: {
formatter: function(val) {
return parseInt(val) + "";
}
}
},
yaxis: {
tickAmount: 7,
title: {
text: 'Performance Score'
}
},
title: {
text: 'Scatter Chart with Categories',
align: 'left'
},
legend: {
position: 'top'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex, window.colorMap.warning[500].hex],
tooltip: {
y: {
formatter: function(val) {
return val + " points";
}
},
x: {
formatter: function(val) {
return "Round " + val;
}
}
},
markers: {
size: 7
}
};
const categoryScatterChart = new ApexCharts(
document.getElementById('category-scatter-chart'),
categoryScatterOptions
);
categoryScatterChart.render();
}
// Scatter Chart with Images
if (document.getElementById('image-scatter-chart')) {
const imageScatterOptions = {
series: [{
name: 'Messenger',
data: [
[16.4, 5.4],
[21.7, 4.1],
[25.4, 3.0],
[19.0, 2.5],
[10.9, 1.4]
]
}, {
name: 'LinkedIn',
data: [
[6.4, 5.4],
[11.7, 4.1],
[15.4, 3.0],
[9.0, 2.5],
[10.9, 1.4]
]
}, {
name: 'Facebook',
data: [
[36.4, 13.4],
[1.7, 11.1],
[5.4, 8.0],
[9.0, 17.0],
[1.9, 4.4]
]
}, {
name: 'Instagram',
data: [
[26.4, 16.4],
[28.7, 18.1],
[29.4, 20.0],
[25.0, 22.5],
[20.9, 19.4]
]
}],
chart: {
height: 350,
type: 'scatter',
animations: {
enabled: false,
},
zoom: {
enabled: true,
type: 'xy'
},
toolbar: {
show: true
}
},
xaxis: {
tickAmount: 10,
min: 0,
max: 40
},
yaxis: {
tickAmount: 7,
min: 0,
max: 25,
title: {
text: 'Engagement Rate (%)'
}
},
title: {
text: 'Social Media Engagement (with custom markers)',
align: 'left'
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex, window.colorMap.warning[500].hex, window.colorMap.success[500].hex],
tooltip: {
y: {
formatter: function(val) {
return val + "%";
}
},
x: {
formatter: function(val) {
return val + "M users";
}
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
markers: {
size: 20,
shape: "circle",
radius: 2
},
legend: {
position: 'top'
}
};
const imageScatterChart = new ApexCharts(
document.getElementById('image-scatter-chart'),
imageScatterOptions
);
imageScatterChart.render();
}
// Datetime Scatter Chart
if (document.getElementById('datetime-scatter-chart')) {
const generateDayWiseTimeSeries = function(startDate, count, range) {
let i = 0;
const series = [];
while (i < count) {
const x = new Date(startDate.getTime() + i * 86400000); // add 1 day
const y = Math.floor(Math.random() * (range.max - range.min + 1)) + range.min;
series.push([x, y]);
i++;
}
return series;
};
const datetimeScatterOptions = {
series: [{
name: 'Sales',
data: generateDayWiseTimeSeries(new Date('01 Jan 2025'), 30, { min: 10, max: 90 })
}, {
name: 'Web Traffic',
data: generateDayWiseTimeSeries(new Date('01 Jan 2025'), 30, { min: 50, max: 150 })
}],
chart: {
height: 350,
type: 'scatter',
zoom: {
enabled: true,
type: 'xy'
},
toolbar: {
show: true
}
},
xaxis: {
type: 'datetime',
labels: {
datetimeUTC: false
}
},
yaxis: {
title: {
text: 'Value'
}
},
title: {
text: 'Scatter Chart with Datetime',
align: 'left'
},
grid: {
row: {
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.05), 'transparent'],
opacity: 0.5
},
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
tooltip: {
x: {
format: 'dd MMM yyyy'
}
},
markers: {
size: 6
}
};
const datetimeScatterChart = new ApexCharts(
document.getElementById('datetime-scatter-chart'),
datetimeScatterOptions
);
datetimeScatterChart.render();
}
// Images Scatter Chart
if (document.getElementById('images-scatter-chart')) {
function generateData(count, yrange) {
let i = 0;
const series = [];
while (i < count) {
const x = (Math.random() * 100).toFixed(0);
const y = (Math.random() * yrange).toFixed(0);
series.push({
x: x,
y: y
});
i++;
}
return series;
}
const imagesScatterOptions = {
series: [{
name: 'Brave',
data: generateData(10, 80)
}, {
name: 'Chrome',
data: generateData(10, 60)
}, {
name: 'Firefox',
data: generateData(10, 40)
}, {
name: 'Safari',
data: generateData(10, 100)
}],
chart: {
height: 350,
type: 'scatter',
animations: {
enabled: false,
},
zoom: {
enabled: false,
},
toolbar: {
show: false
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex, window.colorMap.warning[500].hex, window.colorMap.success[500].hex],
xaxis: {
tickAmount: 20,
min: 0,
max: 10
},
yaxis: {
tickAmount: 5
},
markers: {
size: 20,
shape: 'circle',
strokeWidth: 0,
hover: {
size: 25,
}
},
fill: {
type: 'image',
opacity: 1,
borderRadius: 0,
image: {
src: [
'img/browsers/brave.png',
'img/browsers/chrome.png',
'img/browsers/firefox.png',
'img/browsers/safari.png'
],
width: 30,
height: 30,
}
},
title: {
text: 'Social Media Usage Distribution',
align: 'left'
},
legend: {
position: 'bottom'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const platforms = ['Brave', 'Chrome', 'Firefox', 'Safari'];
const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
return '<div class="p-2">' +
'<div style="text-align: center; margin-bottom: 5px;"><strong>' + platforms[seriesIndex] + '</strong></div>' +
'<span>Usage score: ' + data.y + '</span><br>' +
'<span>Satisfaction: ' + data.x + '</span>' +
'</div>';
}
}
};
const imagesScatterChart = new ApexCharts(
document.getElementById('images-scatter-chart'),
imagesScatterOptions
);
imagesScatterChart.render();
}
});
@@ -0,0 +1,261 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Slope Chart
if (document.getElementById('basic-slope-chart')) {
const basicSlopeOptions = {
series: [
{
name: 'Sales',
data: [
{
x: '2019',
y: 40
},
{
x: '2023',
y: 80
}
]
},
{
name: 'Revenue',
data: [
{
x: '2019',
y: 25
},
{
x: '2023',
y: 70
}
]
},
{
name: 'Customers',
data: [
{
x: '2019',
y: 15
},
{
x: '2023',
y: 65
}
]
}
],
chart: {
type: 'line',
height: 350,
zoom: {
enabled: false
},
toolbar: {
show: true
}
},
dataLabels: {
enabled: true,
offsetY: -5,
style: {
fontSize: '12px',
fontWeight: 'normal',
}
},
title: {
text: 'Basic Slope Chart',
align: 'left'
},
legend: {
position: 'top'
},
colors: [window.colorMap.primary[100].hex, window.colorMap.primary[300].hex, window.colorMap.primary[600].hex],
stroke: {
width: 3
},
grid: {
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: false
}
}
},
markers: {
size: 5
},
xaxis: {
type: 'category',
tickAmount: 2,
tickPlacement: 'on',
axisTicks: {
show: false
},
axisBorder: {
show: false
}
},
yaxis: {
min: 0,
max: 100,
tickAmount: 5,
}
};
const basicSlopeChart = new ApexCharts(
document.getElementById('basic-slope-chart'),
basicSlopeOptions
);
basicSlopeChart.render();
}
// Multi-group Slope Chart
if (document.getElementById('multi-group-slope-chart')) {
const multiGroupSlopeOptions = {
series: [
{
name: 'USA',
data: [
{
x: '2010',
y: 71
},
{
x: '2020',
y: 76
}
]
},
{
name: 'UK',
data: [
{
x: '2010',
y: 55
},
{
x: '2020',
y: 81
}
]
},
{
name: 'Germany',
data: [
{
x: '2010',
y: 50
},
{
x: '2020',
y: 65
}
]
},
{
name: 'Japan',
data: [
{
x: '2010',
y: 40
},
{
x: '2020',
y: 85
}
]
},
{
name: 'India',
data: [
{
x: '2010',
y: 45
},
{
x: '2020',
y: 70
}
]
}
],
chart: {
type: 'line',
height: 350,
zoom: {
enabled: false
},
toolbar: {
show: true
}
},
dataLabels: {
enabled: true,
offsetX: 0,
offsetY: 0,
style: {
fontSize: '12px',
fontWeight: 'normal',
}
},
title: {
text: 'Multi-group Slope Chart',
align: 'left'
},
tooltip: {
theme: 'dark'
},
legend: {
position: 'top'
},
colors: [window.colorMap.primary[500].hex, window.colorMap.info[500].hex, window.colorMap.success[500].hex, window.colorMap.warning[500].hex, window.colorMap.danger[500].hex],
stroke: {
width: 3
},
grid: {
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: false
}
}
},
markers: {
size: 6
},
xaxis: {
type: 'category',
tickAmount: 2,
axisTicks: {
show: false
},
axisBorder: {
show: false
}
},
yaxis: {
min: 0,
max: 100,
tickAmount: 5,
}
};
const multiGroupSlopeChart = new ApexCharts(
document.getElementById('multi-group-slope-chart'),
multiGroupSlopeOptions
);
multiGroupSlopeChart.render();
}
});
@@ -0,0 +1,425 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Sparkline Chart
if (document.getElementById('basic-sparkline-chart')) {
// Generate random data for sparklines
function generateSparklineData(count, min, max) {
const data = [];
for (let i = 0; i < count; i++) {
data.push(Math.floor(Math.random() * (max - min + 1)) + min);
}
return data;
}
// Create sparkline layout
const sparklineContainer = document.getElementById('basic-sparkline-chart');
sparklineContainer.innerHTML = `
<div class="row m-0">
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Sales</h5>
<span class="text-muted">Monthly</span>
</div>
<h3 class="text-success mb-0">+24%</h3>
</div>
<div id="spark1"></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Revenue</h5>
<span class="text-muted">Quarterly</span>
</div>
<h3 class="text-success mb-0">+17%</h3>
</div>
<div id="spark2"></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Customers</h5>
<span class="text-muted">Daily</span>
</div>
<h3 class="text-danger mb-0">-3%</h3>
</div>
<div id="spark3"></div>
</div>
</div>
</div>
</div>
<div class="row m-0">
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Orders</h5>
<span class="text-muted">Weekly</span>
</div>
<h3 class="text-success mb-0">+12%</h3>
</div>
<div id="spark4"></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Engagement</h5>
<span class="text-muted">Monthly</span>
</div>
<h3 class="text-success mb-0">+8%</h3>
</div>
<div id="spark5"></div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0">Conversion</h5>
<span class="text-muted">Monthly</span>
</div>
<h3 class="text-success mb-0">+5%</h3>
</div>
<div id="spark6"></div>
</div>
</div>
</div>
</div>
`;
// Sparkline 1 - Line
const spark1Options = {
series: [{
data: generateSparklineData(30, 30, 90)
}],
chart: {
type: 'line',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
stroke: {
width: 2,
curve: 'smooth'
},
colors: [window.colorMap.primary[500].hex],
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Sales:';
}
}
},
marker: {
show: false
}
}
};
const spark1Chart = new ApexCharts(document.querySelector("#spark1"), spark1Options);
spark1Chart.render();
// Sparkline 2 - Column
const spark2Options = {
series: [{
data: generateSparklineData(20, 10, 60)
}],
chart: {
type: 'bar',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
colors: [window.colorMap.primary[400].hex],
plotOptions: {
bar: {
columnWidth: '60%'
}
},
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Revenue:';
}
}
},
marker: {
show: false
}
}
};
const spark2Chart = new ApexCharts(document.querySelector("#spark2"), spark2Options);
spark2Chart.render();
// Sparkline 3 - Area
const spark3Options = {
series: [{
data: generateSparklineData(30, 20, 50)
}],
chart: {
type: 'area',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
stroke: {
curve: 'straight',
width: 2
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
gradientToColors: [window.colorMap.bootstrapVars.bodyBg.hex],
inverseColors: false,
opacityFrom: 0.7,
opacityTo: 0.2,
stops: [0, 100]
}
},
colors: [window.colorMap.danger[500].hex],
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Customers:';
}
}
},
marker: {
show: false
}
}
};
const spark3Chart = new ApexCharts(document.querySelector("#spark3"), spark3Options);
spark3Chart.render();
// Sparkline 4 - Line with markers
const spark4Options = {
series: [{
data: generateSparklineData(15, 40, 100)
}],
chart: {
type: 'line',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
stroke: {
width: 2,
curve: 'straight'
},
colors: [window.colorMap.success[500].hex],
markers: {
size: 4,
colors: [window.colorMap.success[500].hex],
strokeColors: '#ffffff',
strokeWidth: 2,
hover: {
size: 6
}
},
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Orders:';
}
}
}
}
};
const spark4Chart = new ApexCharts(document.querySelector("#spark4"), spark4Options);
spark4Chart.render();
// Sparkline 5 - Area with negative values
const spark5Data = generateSparklineData(15, -10, 40).map(val => val - 15);
const spark5Options = {
series: [{
data: spark5Data
}],
chart: {
type: 'area',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
stroke: {
curve: 'smooth',
width: 2
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
gradientToColors: [window.colorMap.bootstrapVars.bodyBg.hex],
inverseColors: false,
opacityFrom: 0.7,
opacityTo: 0.2,
stops: [0, 100]
}
},
colors: [window.colorMap.primary[500].hex],
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Engagement:';
}
}
},
marker: {
show: false
}
}
};
const spark5Chart = new ApexCharts(document.querySelector("#spark5"), spark5Options);
spark5Chart.render();
// Sparkline 6 - Bar with colors
const spark6Data = generateSparklineData(24, 10, 50);
const spark6Colors = spark6Data.map(value => {
if (value >= 40) return window.colorMap.primary[500].hex;
if (value >= 30) return window.colorMap.primary[100].hex;
if (value >= 20) return window.colorMap.danger[500].hex;
return window.colorMap.warning[500].hex;
});
const spark6Options = {
series: [{
data: spark6Data
}],
chart: {
type: 'bar',
height: 80,
sparkline: {
enabled: true
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800,
}
},
colors: spark6Colors,
plotOptions: {
bar: {
columnWidth: '80%',
distributed: true
}
},
tooltip: {
fixed: {
enabled: false
},
x: {
show: false
},
y: {
title: {
formatter: function() {
return 'Conversion:';
}
}
},
marker: {
show: false
}
}
};
const spark6Chart = new ApexCharts(document.querySelector("#spark6"), spark6Options);
spark6Chart.render();
}
});
@@ -0,0 +1,641 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Timeline Chart
if (document.getElementById('basic-timeline-chart')) {
const basicTimelineOptions = {
series: [
{
data: [
{
x: 'Code',
y: [
new Date('2019-03-02').getTime(),
new Date('2019-03-04').getTime()
]
},
{
x: 'Test',
y: [
new Date('2019-03-04').getTime(),
new Date('2019-03-08').getTime()
]
},
{
x: 'Validation',
y: [
new Date('2019-03-08').getTime(),
new Date('2019-03-12').getTime()
]
},
{
x: 'Deployment',
y: [
new Date('2019-03-12').getTime(),
new Date('2019-03-18').getTime()
]
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true
}
},
xaxis: {
type: 'datetime'
},
colors: [window.colorMap.primary[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const basicTimelineChart = new ApexCharts(
document.getElementById('basic-timeline-chart'),
basicTimelineOptions
);
basicTimelineChart.render();
}
// Custom Colors Timeline Chart
if (document.getElementById('custom-colors-timeline-chart')) {
const customColorsTimelineOptions = {
series: [
{
data: [
{
x: 'Research',
y: [
new Date('2019-03-01').getTime(),
new Date('2019-03-05').getTime()
],
fillColor: window.colorMap.primary[500].hex
},
{
x: 'Design',
y: [
new Date('2019-03-05').getTime(),
new Date('2019-03-09').getTime()
],
fillColor: window.colorMap.success[500].hex
},
{
x: 'Development',
y: [
new Date('2019-03-09').getTime(),
new Date('2019-03-15').getTime()
],
fillColor: window.colorMap.warning[600].hex
},
{
x: 'Testing',
y: [
new Date('2019-03-15').getTime(),
new Date('2019-03-20').getTime()
],
fillColor: window.colorMap.danger[600].hex
},
{
x: 'Deployment',
y: [
new Date('2019-03-20').getTime(),
new Date('2019-03-25').getTime()
],
fillColor: window.colorMap.info[500].hex
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true,
distributed: true,
dataLabels: {
hideOverflowingLabels: false
}
}
},
dataLabels: {
enabled: true,
formatter: function(val, opts) {
const label = opts.w.globals.labels[opts.dataPointIndex];
const a = new Date(val[0]);
const b = new Date(val[1]);
const diff = Math.floor((b - a) / (1000 * 60 * 60 * 24));
return label + ': ' + diff + (diff > 1 ? ' days' : ' day');
},
style: {
colors: ['#f3f4f5', '#fff']
}
},
xaxis: {
type: 'datetime'
},
tooltip: {
theme: 'dark'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const customColorsTimelineChart = new ApexCharts(
document.getElementById('custom-colors-timeline-chart'),
customColorsTimelineOptions
);
customColorsTimelineChart.render();
}
// Multi-series Timeline Chart
if (document.getElementById('multi-series-timeline-chart')) {
const multiSeriesTimelineOptions = {
series: [
{
name: 'Development Team',
data: [
{
x: 'Design',
y: [
new Date('2019-03-01').getTime(),
new Date('2019-03-05').getTime()
]
},
{
x: 'Code',
y: [
new Date('2019-03-06').getTime(),
new Date('2019-03-15').getTime()
]
},
{
x: 'Test',
y: [
new Date('2019-03-16').getTime(),
new Date('2019-03-22').getTime()
]
}
]
},
{
name: 'Testing Team',
data: [
{
x: 'Review',
y: [
new Date('2019-03-05').getTime(),
new Date('2019-03-08').getTime()
]
},
{
x: 'QA Testing',
y: [
new Date('2019-03-14').getTime(),
new Date('2019-03-25').getTime()
]
},
{
x: 'User Testing',
y: [
new Date('2019-03-20').getTime(),
new Date('2019-03-28').getTime()
]
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true
}
},
dataLabels: {
enabled: true
},
legend: {
position: 'top'
},
xaxis: {
type: 'datetime'
},
tooltip: {
theme: 'dark'
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const multiSeriesTimelineChart = new ApexCharts(
document.getElementById('multi-series-timeline-chart'),
multiSeriesTimelineOptions
);
multiSeriesTimelineChart.render();
}
// Advanced Timeline Chart
if (document.getElementById('advanced-timeline-chart')) {
const advancedTimelineOptions = {
series: [
{
name: 'Bob',
data: [
{
x: 'Design',
y: [
new Date('2019-03-05').getTime(),
new Date('2019-03-08').getTime()
]
},
{
x: 'Code',
y: [
new Date('2019-03-08').getTime(),
new Date('2019-03-11').getTime()
]
},
{
x: 'Test',
y: [
new Date('2019-03-11').getTime(),
new Date('2019-03-16').getTime()
]
}
]
},
{
name: 'Alice',
data: [
{
x: 'Design',
y: [
new Date('2019-03-02').getTime(),
new Date('2019-03-05').getTime()
]
},
{
x: 'Code',
y: [
new Date('2019-03-06').getTime(),
new Date('2019-03-09').getTime()
]
},
{
x: 'Test',
y: [
new Date('2019-03-10').getTime(),
new Date('2019-03-19').getTime()
]
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true
}
},
xaxis: {
type: 'datetime'
},
legend: {
position: 'top'
},
tooltip: {
theme: 'dark'
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const advancedTimelineChart = new ApexCharts(
document.getElementById('advanced-timeline-chart'),
advancedTimelineOptions
);
advancedTimelineChart.render();
}
// Multiple Series - Group Rows Timeline Chart
if (document.getElementById('multiple-series-group-timeline-chart')) {
const multipleSeriesGroupTimelineOptions = {
series: [
{
name: 'Frontend',
data: [
{
x: 'Design',
y: [
new Date('2019-03-02').getTime(),
new Date('2019-03-08').getTime()
]
},
{
x: 'Development',
y: [
new Date('2019-03-08').getTime(),
new Date('2019-03-20').getTime()
]
},
{
x: 'Testing',
y: [
new Date('2019-03-20').getTime(),
new Date('2019-03-28').getTime()
]
}
]
},
{
name: 'Backend',
data: [
{
x: 'API Design',
y: [
new Date('2019-03-01').getTime(),
new Date('2019-03-10').getTime()
]
},
{
x: 'Implementation',
y: [
new Date('2019-03-10').getTime(),
new Date('2019-03-22').getTime()
]
},
{
x: 'Integration Testing',
y: [
new Date('2019-03-22').getTime(),
new Date('2019-04-05').getTime()
]
}
]
},
{
name: 'Database',
data: [
{
x: 'Schema Design',
y: [
new Date('2019-03-05').getTime(),
new Date('2019-03-12').getTime()
]
},
{
x: 'Data Migration',
y: [
new Date('2019-03-15').getTime(),
new Date('2019-03-25').getTime()
]
},
{
x: 'Performance Testing',
y: [
new Date('2019-03-25').getTime(),
new Date('2019-04-10').getTime()
]
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true,
barHeight: '70%',
rangeBarGroupRows: true
}
},
xaxis: {
type: 'datetime'
},
legend: {
position: 'top'
},
tooltip: {
theme: 'dark',
custom: function({series, seriesIndex, dataPointIndex, w}) {
const task = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
const from = new Date(task.y[0]);
const to = new Date(task.y[1]);
const formatDate = (date) => {
return date.getDate() + ' ' + date.toLocaleString('default', { month: 'short' }) + ' ' + date.getFullYear();
};
return (
'<div class="p-2">' +
'<div><b>' + w.globals.initialSeries[seriesIndex].name + ': ' + task.x + '</b></div>' +
'<div>Start: ' + formatDate(from) + '</div>' +
'<div>End: ' + formatDate(to) + '</div>' +
'</div>'
);
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[100].hex],
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const multipleSeriesGroupTimelineChart = new ApexCharts(
document.getElementById('multiple-series-group-timeline-chart'),
multipleSeriesGroupTimelineOptions
);
multipleSeriesGroupTimelineChart.render();
}
// Dumbbell Chart (Horizontal)
if (document.getElementById('dumbbell-timeline-chart')) {
const dumbbellTimelineOptions = {
series: [
{
data: [
{
x: 'Project A',
y: [
new Date('2019-01-01').getTime(),
new Date('2019-03-15').getTime()
]
},
{
x: 'Project B',
y: [
new Date('2019-02-15').getTime(),
new Date('2019-06-01').getTime()
]
},
{
x: 'Project C',
y: [
new Date('2019-04-01').getTime(),
new Date('2019-08-15').getTime()
]
},
{
x: 'Project D',
y: [
new Date('2019-05-15').getTime(),
new Date('2019-09-01').getTime()
]
},
{
x: 'Project E',
y: [
new Date('2019-07-01').getTime(),
new Date('2019-12-31').getTime()
]
}
]
}
],
chart: {
height: 350,
type: 'rangeBar',
toolbar: {
show: true
}
},
plotOptions: {
bar: {
horizontal: true,
isDumbbell: true,
dumbbellColors: [[window.colorMap.primary[500].hex, window.colorMap.danger[500].hex]]
}
},
xaxis: {
type: 'datetime'
},
yaxis: {
labels: {
formatter: function (val) {
return val
}
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
theme: 'dark',
custom: function({series, seriesIndex, dataPointIndex, w}) {
const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
const startDate = new Date(data.y[0]);
const endDate = new Date(data.y[1]);
const formatDate = (date) => {
return date.toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
};
return `
<div class="p-2">
<div><b>${data.x}</b></div>
<div>Start: ${formatDate(startDate)}</div>
<div>End: ${formatDate(endDate)}</div>
<div>Duration: ${Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24))} days</div>
</div>
`;
}
}
};
const dumbbellTimelineChart = new ApexCharts(
document.getElementById('dumbbell-timeline-chart'),
dumbbellTimelineOptions
);
dumbbellTimelineChart.render();
}
});
@@ -0,0 +1,310 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Treemap Chart
if (document.getElementById('basic-treemap-chart')) {
const basicTreemapOptions = {
series: [{
data: [
{
x: 'New Delhi',
y: 218
}, {
x: 'Kolkata',
y: 149
}, {
x: 'Mumbai',
y: 184
}, {
x: 'Ahmedabad',
y: 55
}, {
x: 'Bangalore',
y: 84
}, {
x: 'Pune',
y: 31
}, {
x: 'Chennai',
y: 70
}, {
x: 'Jaipur',
y: 30
}, {
x: 'Surat',
y: 44
}, {
x: 'Hyderabad',
y: 68
}, {
x: 'Lucknow',
y: 28
}, {
x: 'Indore',
y: 19
}, {
x: 'Kanpur',
y: 29
}
]
}],
chart: {
height: 350,
type: 'treemap',
toolbar: {
show: false
},
zoom: {
enabled: false,
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[400].hex, window.colorMap.primary[100].hex],
tooltip: {
theme: 'dark'
}
};
const basicTreemapChart = new ApexCharts(
document.getElementById('basic-treemap-chart'),
basicTreemapOptions
);
basicTreemapChart.render();
}
// Multiple Series Treemap Chart
if (document.getElementById('multiple-series-treemap-chart')) {
const multiSeriesTreemapOptions = {
series: [{
name: 'Desktops',
data: [
{
x: 'ABC',
y: 10
}, {
x: 'DEF',
y: 60
}, {
x: 'XYZ',
y: 41
}
]
}, {
name: 'Mobile',
data: [
{
x: 'ABCD',
y: 10
}, {
x: 'DEFG',
y: 20
}, {
x: 'WXYZ',
y: 51
}, {
x: 'PQR',
y: 30
}, {
x: 'MNO',
y: 20
}, {
x: 'CDE',
y: 30
}
]
}],
chart: {
height: 350,
type: 'treemap',
toolbar: {
show: false
},
zoom: {
enabled: false,
}
},
colors: [window.colorMap.primary[500].hex, window.colorMap.primary[100].hex],
plotOptions: {
treemap: {
distributed: false,
enableShades: false
}
}
};
const multiSeriesTreemapChart = new ApexCharts(
document.getElementById('multiple-series-treemap-chart'),
multiSeriesTreemapOptions
);
multiSeriesTreemapChart.render();
}
// Color Range Treemap Chart
if (document.getElementById('color-range-treemap-chart')) {
const colorRangeOptions = {
series: [{
data: [
{
x: 'INTC',
y: 1.2
}, {
x: 'GS',
y: 0.4
}, {
x: 'CVX',
y: -1.4
}, {
x: 'GE',
y: 2.7
}, {
x: 'CAT',
y: -0.3
}, {
x: 'RTX',
y: 5.1
}, {
x: 'CSCO',
y: -2.3
}, {
x: 'JNJ',
y: 2.1
}, {
x: 'PG',
y: 0.3
}, {
x: 'TRV',
y: 0.12
}, {
x: 'MMM',
y: -2.31
}, {
x: 'NKE',
y: 3.98
}, {
x: 'IYT',
y: 1.67
}
]
}],
chart: {
height: 350,
type: 'treemap',
toolbar: {
show: false
},
zoom: {
enabled: false,
}
},
tooltip: {
theme: 'dark'
},
plotOptions: {
treemap: {
distributed: true,
enableShades: false,
colorScale: {
ranges: [{
from: -6,
to: 0,
color: window.colorMap.danger[500].hex
}, {
from: 0.001,
to: 6,
color: window.colorMap.primary[500].hex
}]
}
}
}
};
const colorRangeTreemapChart = new ApexCharts(
document.getElementById('color-range-treemap-chart'),
colorRangeOptions
);
colorRangeTreemapChart.render();
}
// Distributed Treemap Chart
if (document.getElementById('distributed-treemap-chart')) {
const distributedTreemapOptions = {
series: [{
data: [
{
x: 'New Delhi',
y: 218
}, {
x: 'Kolkata',
y: 149
}, {
x: 'Mumbai',
y: 184
}, {
x: 'Ahmedabad',
y: 55
}, {
x: 'Bangalore',
y: 84
}, {
x: 'Pune',
y: 31
}, {
x: 'Chennai',
y: 70
}, {
x: 'Jaipur',
y: 30
}, {
x: 'Surat',
y: 44
}, {
x: 'Hyderabad',
y: 68
}, {
x: 'Lucknow',
y: 28
}, {
x: 'Indore',
y: 19
}, {
x: 'Kanpur',
y: 29
}
]
}],
chart: {
height: 350,
type: 'treemap',
toolbar: {
show: false
},
zoom: {
enabled: false,
}
},
colors: [
window.colorMap.primary[800].hex, window.colorMap.primary[500].hex, window.colorMap.success[500].hex, window.colorMap.success[700].hex,
window.colorMap.danger[500].hex, window.colorMap.warning[500].hex, window.colorMap.info[500].hex, window.colorMap.primary[100].hex
],
tooltip: {
theme: 'dark'
},
plotOptions: {
treemap: {
distributed: true,
enableShades: false
}
}
};
const distributedTreemapChart = new ApexCharts(
document.getElementById('distributed-treemap-chart'),
distributedTreemapOptions
);
distributedTreemapChart.render();
}
});
@@ -0,0 +1,11 @@
VANTA.HALO({
el: "#net",
mouseControls: false,
touchControls: false,
gyroControls: false,
color: 0xfd3995,
size: 1.6,
scale: 0.75,
xOffset: 0.22,
scaleMobile: 0.50,
});
@@ -0,0 +1,14 @@
const switchToTokenButton = document.querySelector('#switchToToken');
const switchToRegularButton = document.querySelector('#switchToRegular');
const regularLogin = document.querySelector('#regular-login');
const tokenLogin = document.querySelector('#token-login');
switchToTokenButton.addEventListener('click', function () {
regularLogin.classList.add('d-none');
tokenLogin.classList.remove('d-none');
});
switchToRegularButton.addEventListener('click', function () {
tokenLogin.classList.add('d-none');
regularLogin.classList.remove('d-none');
});
+5
View File
@@ -0,0 +1,5 @@
//
console.log('Blank page initialized');
console.log('blank.js loaded');
@@ -0,0 +1,33 @@
document.addEventListener("DOMContentLoaded", function () {
// Set the indeterminate state for the checkbox
document.getElementById("defaultIndeterminate").indeterminate = true;
// Get buttons
const checkboxToggleBtn = document.getElementById("js-checkbox-toggle");
const radioToggleBtn = document.getElementById("js-radio-toggle");
// Add click event listeners
if (checkboxToggleBtn) {
checkboxToggleBtn.addEventListener("click", toggleCheckbox);
}
if (radioToggleBtn) {
radioToggleBtn.addEventListener("click", toggleRadio);
}
// Function to toggle checkbox styles
function toggleCheckbox() {
let checkboxes = document.querySelectorAll(".demo-checkbox .form-check-input");
toggleText(this, "Change to CIRCLE", "Change back to default");
checkboxes.forEach(checkbox => checkbox.classList.toggle("rounded-circle"));
}
// Function to toggle radio button styles
function toggleRadio() {
let radios = document.querySelectorAll(".demo-radio .form-check-input");
toggleText(this, "Change to ROUNDED", "Change back to default");
radios.forEach(radio => radio.classList.toggle("rounded"));
}
// Function to toggle button text
function toggleText(element, text1, text2) {
element.textContent = element.textContent.trim() === text1 ? text2 : text1;
}
});
@@ -0,0 +1,4 @@
document.addEventListener('DOMContentLoaded', function () {
hljs.highlightAll();
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,339 @@
/**
* Easy Pie Chart Implementation
* Vanilla JavaScript implementation for easy pie charts
*/
// Global registry to keep track of all chart instances
window.easyPieChartRegistry = {
instances: [],
elements: []
};
// Global color state
window.easyPieChartColors = {
currentColors: {},
previousColors: {}
};
// Extract colors from the theme
function extractEasyPieChartColors() {
// Store previous colors
window.easyPieChartColors.previousColors = {...window.easyPieChartColors.currentColors};
// Extract new colors
window.easyPieChartColors.currentColors = {};
if (window.colorMap) {
// Define color categories to extract
const colorCategories = ['primary', 'success', 'warning', 'danger', 'info'];
// Get all available color shades for each category
colorCategories.forEach(category => {
if (window.colorMap[category]) {
// For each category, find all available shades
Object.keys(window.colorMap[category]).forEach(shade => {
if (window.colorMap[category][shade]?.hex) {
// Store each shade with a category-shade key
const colorKey = `${category}-${shade}`;
window.easyPieChartColors.currentColors[colorKey] = window.colorMap[category][shade].hex;
}
});
}
});
// Also add bodyColor as it's commonly used
if (window.colorMap.bootstrapVars?.bodyColor?.hex) {
window.easyPieChartColors.currentColors['bodyColor'] = window.colorMap.bootstrapVars.bodyColor.hex;
window.easyPieChartColors.currentColors['bodyColor-alpha'] = window.colorMap.bootstrapVars.bodyColor.rgba(0.07);
}
}
return window.easyPieChartColors.currentColors;
}
// Helper to update a color value based on the before/after color mapping
function updateEasyPieChartColorValue(color) {
if (!color || typeof color !== 'string') return color;
// First, direct match from previous colors to current colors
if (window.easyPieChartColors.previousColors) {
for (const [key, oldColor] of Object.entries(window.easyPieChartColors.previousColors)) {
if (color === oldColor) {
return window.easyPieChartColors.currentColors[key] || color;
}
}
}
// If no direct match, check if it's a color from the theme
if (window.colorMap) {
const colorCategories = ['primary', 'success', 'warning', 'danger', 'info'];
// Try to match with any shade in each category
for (const category of colorCategories) {
if (window.colorMap[category]) {
for (const shade in window.colorMap[category]) {
if (window.colorMap[category][shade]?.hex === color) {
// We found a match - use the updated color for this category/shade
return window.colorMap[category][shade].hex;
}
}
}
}
}
// Handle rgba colors
if (color.includes('rgba') && window.colorMap?.bootstrapVars?.bodyColor) {
const opacityMatch = color.match(/rgba\([^)]+,\s*([\d.]+)\)/);
if (opacityMatch && opacityMatch[1]) {
const opacity = parseFloat(opacityMatch[1]);
// Common opacities to check against
const standardOpacities = [0.07, 0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9];
const closestOpacity = standardOpacities.reduce((prev, curr) =>
Math.abs(curr - opacity) < Math.abs(prev - opacity) ? curr : prev
);
// If this is a very close match to a standard opacity, use exact value
if (Math.abs(closestOpacity - opacity) < 0.02) {
return window.colorMap.bootstrapVars.bodyColor.rgba(closestOpacity);
}
// Otherwise use the exact opacity provided
return window.colorMap.bootstrapVars.bodyColor.rgba(opacity);
}
}
// Handle CSS variables
if (color.startsWith('var(')) {
// Try to extract the variable value
const varName = color.replace('var(', '').replace(')', '').trim();
const computedStyle = getComputedStyle(document.documentElement);
const varValue = computedStyle.getPropertyValue(varName);
if (varValue) {
return varValue.trim();
}
}
return color;
}
// Global function to update all Easy Pie Chart colors
window.updateEasyPieCharts = function() {
console.log(`EasyPieChart: Updating colors for ${window.easyPieChartRegistry.instances.length} charts`);
// Update color maps
extractEasyPieChartColors();
if (window.easyPieChartRegistry.instances.length === 0) {
console.warn('EasyPieChart: No charts found to update');
return;
}
// Update each chart with new colors
window.easyPieChartRegistry.instances.forEach((chart, index) => {
try {
if (!chart) {
console.warn(`EasyPieChart: Chart #${index + 1} cannot be updated`);
return;
}
const element = window.easyPieChartRegistry.elements[index];
if (!element) return;
// Get computed style
const computedStyle = window.getComputedStyle(element);
// Update barColor
const currentBarColor = chart.options.barColor;
const newBarColor = computedStyle.color || currentBarColor;
const updatedBarColor = updateEasyPieChartColorValue(newBarColor);
// Update trackColor
let updatedTrackColor;
try {
if (window.colorMap && window.colorMap.bootstrapVars && window.colorMap.bootstrapVars.bodyColorRgb) {
updatedTrackColor = window.colorMap.bootstrapVars.bodyColorRgb.rgba(0.07);
} else {
updatedTrackColor = 'rgba(0,0,0,0.04)';
}
} catch (e) {
updatedTrackColor = 'rgba(0,0,0,0.04)';
}
// Update scaleColor
const currentScaleColor = chart.options.scaleColor;
const newScaleColor = element.dataset.scalecolor || computedStyle.color || currentScaleColor;
const updatedScaleColor = updateEasyPieChartColorValue(newScaleColor);
// Apply the new colors using Canvas API's getImageData for efficiency
const canvas = chart.renderer.getCanvas();
const ctx = chart.renderer.getCtx();
// If canvas context is available, use pixel manipulation
if (ctx && ctx.getImageData && ctx.putImageData) {
// Store current canvas state
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Parse colors to RGB
const barRGB = parseColorToRGB(updatedBarColor);
const oldBarRGB = parseColorToRGB(currentBarColor);
const trackRGB = parseColorToRGB(updatedTrackColor);
// Update pixels - only change bar color (non-transparent, non-track pixels)
for (let i = 0; i < data.length; i += 4) {
// Only process non-transparent pixels
if (data[i + 3] > 0) {
// Check if pixel is close to the old bar color and not track color
const isBarPixel =
(Math.abs(data[i] - oldBarRGB.r) < 15 &&
Math.abs(data[i + 1] - oldBarRGB.g) < 15 &&
Math.abs(data[i + 2] - oldBarRGB.b) < 15) ||
(Math.abs(data[i] - trackRGB.r) > 10 ||
Math.abs(data[i + 1] - trackRGB.g) > 10 ||
Math.abs(data[i + 2] - trackRGB.b) > 10);
if (isBarPixel) {
// Update to new bar color
data[i] = barRGB.r;
data[i + 1] = barRGB.g;
data[i + 2] = barRGB.b;
// Keep original alpha
}
}
}
// Put the modified image data back
ctx.putImageData(imageData, 0, 0);
// Update the chart's options
chart.options.barColor = updatedBarColor;
chart.options.trackColor = updatedTrackColor;
chart.options.scaleColor = updatedScaleColor;
console.log(`EasyPieChart: Updated colors for chart #${index + 1} using pixel manipulation`);
} else {
// Fallback to standard update method
chart.options.barColor = updatedBarColor;
chart.options.trackColor = updatedTrackColor;
chart.options.scaleColor = updatedScaleColor;
// Redraw the chart with new colors
chart.update(chart.currentValue);
console.log(`EasyPieChart: Updated colors for chart #${index + 1} using redraw`);
}
} catch (e) {
console.error(`EasyPieChart: Error updating chart #${index + 1}:`, e);
}
});
console.log('EasyPieChart: Chart color update complete');
};
// Helper function to parse color string to RGB
function parseColorToRGB(color) {
// For hex colors
if (color.startsWith('#')) {
const hex = color.substring(1);
const bigint = parseInt(hex, 16);
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255
};
}
// For rgb/rgba colors
else if (color.startsWith('rgb')) {
const matches = color.match(/(\d+),\s*(\d+),\s*(\d+)/);
if (matches) {
return {
r: parseInt(matches[1]),
g: parseInt(matches[2]),
b: parseInt(matches[3])
};
}
}
// Default fallback
return { r: 0, g: 0, b: 0 };
}
// Initialize colors on load
extractEasyPieChartColors();
document.addEventListener('DOMContentLoaded', function () {
/*
Browsers optimize canvas rendering for drawing, not for reading pixel data.
When you use getImageData() repeatedly (as some chart libraries do for animations or gradients),
it can cause readback performance hits.
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
*/
(function patchEasyPieCanvasContext() {
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, options) {
if (type === '2d') {
options = { ...options, willReadFrequently: true };
}
return originalGetContext.call(this, type, options);
};
})();
/* Easy pie chart initialization
Pure JavaScript implementation with no jQuery dependencies
*/
document.querySelectorAll('.js-easy-pie-chart').forEach(function(element) {
// Get element properties using vanilla JS
const computedStyle = window.getComputedStyle(element);
const barcolor = computedStyle.color || 'var(--primary-700)';
// Check if window.colorMap exists, if not use a fallback
let trackcolor;
try {
if (window.colorMap && window.colorMap.bootstrapVars && window.colorMap.bootstrapVars.bodyColorRgb) {
trackcolor = window.colorMap.bootstrapVars.bodyColorRgb.rgba(0.07);
} else {
trackcolor = 'rgba(0,0,0,0.04)';
}
} catch (e) {
trackcolor = 'rgba(0,0,0,0.04)';
}
// Read dataset attributes with fallbacks
const size = parseInt(element.dataset.piesize) || 50;
const scalecolor = element.dataset.scalecolor || computedStyle.color;
const scalelength = parseInt(element.dataset.scalelength) || 0;
const linewidth = parseInt(element.dataset.linewidth) || parseInt(size / 8.5);
const linecap = element.dataset.linecap || 'butt'; // butt, round and square.
// Create EasyPieChart instance
const chart = new EasyPieChart(element, {
size: size,
barColor: barcolor,
trackColor: trackcolor,
scaleColor: scalecolor,
scaleLength: scalelength,
lineCap: linecap,
lineWidth: linewidth,
animate: {
duration: 1500,
enabled: true
},
easing: 'easeOutQuad', // Use our built-in easing function
onStep: function(from, to, percent) {
// Find the percentage element and update its text
const percentElement = element.querySelector('.js-percent');
if (percentElement) {
percentElement.textContent = Math.round(percent);
}
}
});
// Register the chart instance
window.easyPieChartRegistry.instances.push(chart);
window.easyPieChartRegistry.elements.push(element);
});
});
@@ -0,0 +1,726 @@
/*!
* FullCalendar Implementation for SmartAdmin WebApp
* ©2025 SmartAdmin WebApp
*/
// Sample events data
const eventData = [
{ id: '1', title: 'Team Meeting', start: new Date(new Date().setHours(10, 0)), end: new Date(new Date().setHours(11, 30)), backgroundColor: 'var(--primary-500)', borderColor: 'var(--primary-600)', description: 'Weekly team status meeting', location: 'Conference Room A' },
{ id: '2', title: 'Client Call', start: new Date(new Date().setDate(new Date().getDate() + 1)), allDay: true, backgroundColor: 'var(--success-500)', borderColor: 'var(--success-600)', description: 'Quarterly review with major client', location: 'Zoom Meeting' },
{ id: '3', title: 'Product Launch', start: new Date(new Date().setDate(new Date().getDate() + 3)), end: new Date(new Date().setDate(new Date().getDate() + 3)).setHours(15, 0), backgroundColor: 'var(--danger-500)', borderColor: 'var(--danger-600)', description: 'New product launch event', location: 'Main Auditorium' },
{ id: '4', title: 'Deadline: Q3 Report', start: new Date(new Date().setDate(new Date().getDate() + 5)), allDay: true, backgroundColor: 'var(--warning-500)', borderColor: 'var(--warning-600)', description: 'Submit quarterly financial reports', location: 'Finance Department' },
{ id: '5', title: 'Training Session', start: new Date(new Date().setDate(new Date().getDate() - 2)), end: new Date(new Date().setDate(new Date().getDate() - 2)).setHours(16, 0), backgroundColor: 'var(--info-500)', borderColor: 'var(--info-600)', description: 'New software training for all employees', location: 'Training Room B' },
{ id: '6', title: 'Board Meeting', start: new Date(new Date().setDate(new Date().getDate() + 7)), allDay: true, backgroundColor: 'var(--danger-500)', borderColor: 'var(--danger-700)', description: 'Annual board meeting with stakeholders', location: 'Executive Boardroom' },
{ id: '7', title: 'Website Maintenance', start: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(23, 0), end: new Date(new Date().setDate(new Date().getDate())).setHours(5, 0), backgroundColor: 'var(--info-500)', borderColor: 'var(--info-600)', description: 'Scheduled website maintenance window', location: 'IT Department' },
{ id: '8', title: 'Team Building', start: new Date(new Date().setDate(new Date().getDate() + 12)), end: new Date(new Date().setDate(new Date().getDate() + 12)).setHours(16, 0), backgroundColor: 'var(--primary-300)', borderColor: 'var(--primary-400)', description: 'Annual team building activities', location: 'City Park' },
{ id: '9', title: 'Client Dinner', start: new Date(new Date().setDate(new Date().getDate() + 4)).setHours(19, 0), end: new Date(new Date().setDate(new Date().getDate() + 4)).setHours(21, 0), backgroundColor: 'var(--success-300)', borderColor: 'var(--success-400)', description: 'Dinner with potential investors', location: 'Downtown Restaurant' },
{ id: '10', title: 'Vacation', start: new Date(new Date().setDate(new Date().getDate() + 14)), end: new Date(new Date().setDate(new Date().getDate() + 21)), backgroundColor: 'var(--bs-teal)', borderColor: 'var(--bs-teal)', description: 'Annual vacation time', location: 'Beach Resort' },
{ id: '11', title: 'Marketing Campaign', start: new Date(new Date().setDate(new Date().getDate() + 2)), end: new Date(new Date().setDate(new Date().getDate() + 2)).setHours(17, 0), backgroundColor: 'var(--bs-purple)', borderColor: 'var(--bs-purple)', description: 'Launch new product marketing campaign', location: 'Marketing Department', category: 'marketing' },
{ id: '12', title: 'Sales Meeting', start: new Date(new Date().setDate(new Date().getDate() + 3)).setHours(9, 0), end: new Date(new Date().setDate(new Date().getDate() + 3)).setHours(10, 30), backgroundColor: 'var(--success-600)', borderColor: 'var(--success-700)', description: 'Monthly sales team catchup', location: 'Conference Room B', category: 'sales' },
{ id: '13', title: 'Development Sprint Review', start: new Date(new Date().setDate(new Date().getDate() + 6)).setHours(14, 0), end: new Date(new Date().setDate(new Date().getDate() + 6)).setHours(16, 0), backgroundColor: 'var(--bs-indigo)', borderColor: 'var(--bs-indigo)', description: 'End of sprint review with stakeholders', location: 'Dev Team Area', category: 'development' },
{ id: '14', title: 'Tech Conference', start: new Date(new Date().setDate(new Date().getDate() + 8)), end: new Date(new Date().setDate(new Date().getDate() + 10)), backgroundColor: 'var(--bs-indigo)', borderColor: 'var(--bs-indigo)', description: 'Annual tech conference for industry professionals', location: 'Convention Center', category: 'conference' },
{ id: '15', title: 'Dentist Appointment', start: new Date(new Date().setDate(new Date().getDate() + 2)).setHours(14, 0), end: new Date(new Date().setDate(new Date().getDate() + 2)).setHours(15, 0), backgroundColor: 'var(--warning-500)', borderColor: 'var(--warning-600)', description: 'Routine dental checkup', location: 'Downtown Clinic', category: 'personal' },
{ id: '16', title: 'Project Milestone: Beta Release', start: new Date(new Date().setDate(new Date().getDate() + 15)), allDay: true, backgroundColor: 'var(--success-500)', borderColor: 'var(--success-600)', description: 'Beta release of the new app', location: 'Development Team', category: 'development' },
{ id: '17', title: 'Company Announcement', start: new Date(new Date().setDate(new Date().getDate() + 1)).setHours(11, 0), end: new Date(new Date().setDate(new Date().getDate() + 1)).setHours(11, 30), backgroundColor: 'var(--primary-500)', borderColor: 'var(--primary-600)', description: 'Announcement of new company policies', location: 'Main Hall', category: 'announcement' },
{ id: '18', title: 'Weekly Team Sync', start: new Date(new Date().setDate(new Date().getDate() + 4)).setHours(9, 0), end: new Date(new Date().setDate(new Date().getDate() + 4)).setHours(9, 30), backgroundColor: 'var(--primary-400)', borderColor: 'var(--primary-500)', description: 'Recurring weekly sync for project updates', location: 'Meeting Room C', category: 'team', extendedProps: { recurrence: 'weekly' } }
];
// Define fixed event categories
const eventCategories = [
{ id: 'general', name: 'General', color: 'var(--primary-500)' },
{ id: 'meeting', name: 'Meetings', color: 'var(--success-500)' },
{ id: 'task', name: 'Tasks', color: 'var(--warning-500)' },
{ id: 'deadline', name: 'Deadlines', color: 'var(--danger-500)' },
{ id: 'marketing', name: 'Marketing', color: 'var(--bs-purple)' },
{ id: 'sales', name: 'Sales', color: 'var(--success-600)' },
{ id: 'development', name: 'Development', color: 'var(--bs-indigo)' }
];
// Initialize calendar after all scripts are loaded
document.addEventListener('DOMContentLoaded', function () {
// Initialize event modals
const eventModal = new bootstrap.Modal(document.getElementById('eventModal'));
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
const categoryModal = new bootstrap.Modal(document.getElementById('categoryModal'));
// Form element references
const eventForm = document.getElementById('eventForm');
const eventTitle = document.getElementById('eventTitle');
const eventStart = document.getElementById('eventStart');
const eventEnd = document.getElementById('eventEnd');
const eventAllDay = document.getElementById('eventAllDay');
const eventColor = document.getElementById('eventColor');
const eventDescription = document.getElementById('eventDescription');
const eventLocation = document.getElementById('eventLocation');
const eventCategory = document.getElementById('eventCategory');
const eventId = document.getElementById('eventId');
const deleteId = document.getElementById('deleteEventId');
// Populate category select dropdown
populateCategoryDropdown();
// Pre-process events to ensure all have a category
eventData.forEach(event => {
// Initialize extendedProps if it doesn't exist
if (!event.extendedProps) {
event.extendedProps = {};
}
// Set category in extendedProps if missing
if (!event.extendedProps.category) {
event.extendedProps.category = event.category || 'general';
}
});
// Initialize the calendar
const calendarEl = document.getElementById('calendar');
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
themeSystem: 'bootstrap',
headerToolbar: {
right: 'today prev,next',
left: 'title'
},
events: [],
// Apply custom styling after render
datesSet: function () {
setTimeout(() => {
// Replace fc-button-group with btn-group
document.querySelectorAll('.fc-button-group').forEach((el) => {
el.classList.remove('fc-button-group');
el.classList.add('btn-group');
});
// Optional: Replace fc-button styles with Bootstrap
document.querySelectorAll('.fc-button').forEach((btn) => {
btn.classList.remove('fc-button');
btn.classList.add('btn', 'btn-outline-default', 'btn-sm');
});
document.querySelectorAll('.fc-button-primary').forEach((btn) => {
btn.classList.remove('fc-button-primary');
});
// Highlight active button
const activeBtn = document.querySelector('.fc-button-active');
if (activeBtn) {
activeBtn.classList.remove('fc-button-active');
activeBtn.classList.add('active');
}
const groupBtn = document.querySelector('.fc-button-group');
if (groupBtn) {
groupBtn.className = 'btn-group';
}
const prevBtn = document.querySelector('.fc-prev-button');
if (prevBtn) {
prevBtn.className = 'btn btn-outline-default btn-sm';
}
const nextBtn = document.querySelector('.fc-next-button');
if (nextBtn) {
nextBtn.className = 'btn btn-outline-default btn-sm';
}
const todayBtn = document.querySelector('.fc-today-button');
if (todayBtn) {
todayBtn.className = 'btn btn-outline-default btn-sm';
}
}, 0);
},
footerToolbar: {
left: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
},
buttonText: {
today: 'Today',
month: 'Month',
week: 'Week',
day: 'Day',
list: 'List'
},
dayMaxEvents: 2, // Show only 2 events per day, then show "more" link
height: undefined, // Let the calendar calculate its own height
minHeight: '750px', // Set minimum height for desktop
editable: true,
droppable: true, // allow draggable events
selectable: true, // allow selection for new events
businessHours: {
daysOfWeek: [1, 2, 3, 4, 5], // Monday - Friday
startTime: '9:00',
endTime: '17:00'
},
eventSources: [
{ events: eventData }
],
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short'
},
// Handle date selection for new events
select: function (info) {
// Reset form
eventForm.reset();
eventId.value = '';
// Set initial form values from selection
const startDate = info.start;
const endDate = info.end;
eventStart.value = formatDateForInput(startDate);
eventEnd.value = formatDateForInput(endDate);
eventAllDay.checked = info.allDay;
eventColor.value = 'var(--primary-500)';
// Show the modal
document.getElementById('eventModalTitle').textContent = 'Add New Event';
document.getElementById('deleteEvent').style.display = 'none';
eventModal.show();
},
// Handle event click for editing
eventClick: function (info) {
const event = info.event;
console.log("Clicked event:", event); // Debug
// Populate form with event data
eventId.value = event.id;
eventTitle.value = event.title;
eventStart.value = formatDateForInput(event.start);
eventEnd.value = event.end ? formatDateForInput(event.end) : '';
eventAllDay.checked = event.allDay;
eventColor.value = event.backgroundColor || 'var(--primary-500)';
// Make sure extendedProps is defined before accessing properties
if (event.extendedProps) {
eventDescription.value = event.extendedProps.description || '';
eventLocation.value = event.extendedProps.location || '';
eventCategory.value = event.extendedProps.category || '';
console.log("Event category:", event.extendedProps.category); // Debug
} else {
eventDescription.value = '';
eventLocation.value = '';
eventCategory.value = '';
}
// Show the modal
document.getElementById('eventModalTitle').textContent = 'Edit Event';
document.getElementById('deleteEvent').style.display = 'block';
eventModal.show();
},
// Allow event dragging and resizing
eventDrop: function (info) {
showToast('Event moved: ' + info.event.title);
},
eventResize: function (info) {
showToast('Event duration changed: ' + info.event.title);
},
// Handle external draggables
drop: function (info) {
// Remove the dragged element from the list if the checkbox is checked
if (document.getElementById('removeAfterDrop').checked) {
info.draggedEl.parentNode.removeChild(info.draggedEl);
}
showToast('New event added via drag & drop');
},
// Add hover tooltips to events
eventDidMount: function (info) {
const event = info.event;
// Create tooltip with rich content
if (event.extendedProps.description || event.extendedProps.location) {
const tooltipContent = document.createElement('div');
tooltipContent.classList.add('fc-event-tooltip');
const titleEl = document.createElement('div');
titleEl.classList.add('tooltip-title');
titleEl.textContent = event.title;
tooltipContent.appendChild(titleEl);
if (event.extendedProps.description) {
const descEl = document.createElement('div');
descEl.classList.add('tooltip-desc');
descEl.textContent = event.extendedProps.description;
tooltipContent.appendChild(descEl);
}
if (event.extendedProps.location) {
const locEl = document.createElement('div');
locEl.classList.add('tooltip-location');
locEl.innerHTML = '<i class="fa fa-map-marker-alt me-1"></i> ' + event.extendedProps.location;
tooltipContent.appendChild(locEl);
}
// Show time for non-all-day events
if (!event.allDay && event.start) {
const timeEl = document.createElement('div');
timeEl.classList.add('tooltip-time');
timeEl.innerHTML = '<i class="fa fa-clock me-1"></i> ' +
event.start.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (event.end) {
timeEl.innerHTML += ' - ' + event.end.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
tooltipContent.appendChild(timeEl);
}
// Initialize Bootstrap tooltip
new bootstrap.Tooltip(info.el, {
title: tooltipContent.outerHTML,
placement: 'top',
trigger: 'hover',
html: true,
container: 'body'
});
}
}
});
// First initialize the calendar, then setup filters
calendar.render();
// Set up filter controls after calendar initialization
setupFilterControls();
// Initialize draggable events
const draggableEl = document.getElementById('external-events');
new FullCalendar.Draggable(draggableEl, {
itemSelector: '.external-event',
eventData: function (eventEl) {
return {
title: eventEl.innerText.trim(),
backgroundColor: window.getComputedStyle(eventEl).backgroundColor,
borderColor: window.getComputedStyle(eventEl).borderColor,
description: eventEl.dataset.description || '',
location: eventEl.dataset.location || '',
category: eventEl.dataset.category || ''
};
}
});
// Handle form submission
eventForm.addEventListener('submit', function (e) {
e.preventDefault();
// Prepare event data
const eventData = {
title: eventTitle.value,
start: new Date(eventStart.value),
end: eventEnd.value ? new Date(eventEnd.value) : null,
allDay: eventAllDay.checked,
backgroundColor: eventColor.value,
borderColor: eventColor.value,
extendedProps: {
description: eventDescription.value,
location: eventLocation.value,
category: eventCategory.value || 'general'
}
};
// If a category is selected, use its color
if (eventCategory.value) {
const category = eventCategories.find(cat => cat.id === eventCategory.value);
if (category) {
eventData.backgroundColor = category.color;
eventData.borderColor = category.color;
}
}
console.log("Form submitted with data:", eventData); // Debug
console.log("Event ID:", eventId.value); // Debug
if (eventId.value) {
// Update existing event
const existingEvent = calendar.getEventById(eventId.value);
console.log("Existing event:", existingEvent); // Debug
if (existingEvent) {
// Remove the old event and add a new one to ensure all properties are updated
existingEvent.remove();
// Add the updated event with the same ID
eventData.id = eventId.value;
calendar.addEvent(eventData);
showToast('Event updated successfully');
} else {
console.error("Could not find event with ID:", eventId.value);
showToast('Error updating event: Event not found');
}
} else {
// Create new event
eventData.id = 'event-' + new Date().getTime();
calendar.addEvent(eventData);
showToast('Event added successfully');
}
// Close the modal
eventModal.hide();
// Refresh filters to ensure visibility
filterEvents();
});
// Handle delete button click
const deleteEventBtn = document.getElementById('deleteEvent');
if (deleteEventBtn) {
deleteEventBtn.addEventListener('click', function () {
deleteId.value = eventId.value;
eventModal.hide();
deleteModal.show();
});
}
// Handle confirm delete
const confirmDeleteBtn = document.getElementById('confirmDelete');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function () {
const event = calendar.getEventById(deleteId.value);
if (event) {
event.remove();
showToast('Event deleted successfully');
}
deleteModal.hide();
});
}
// Handle category management modal
const manageCategoriesBtn = document.getElementById('manageCategoriesBtn');
if (manageCategoriesBtn) {
manageCategoriesBtn.addEventListener('click', function () {
populateCategoryList();
categoryModal.show();
});
}
// Handle category form submission
const categoryForm = document.getElementById('categoryForm');
if (categoryForm) {
categoryForm.addEventListener('submit', function (e) {
e.preventDefault();
const categoryName = document.getElementById('categoryName').value;
const categoryColor = document.getElementById('categoryColor').value;
// Simple ID generation
const categoryId = 'cat-' + new Date().getTime();
// Add to categories
eventCategories.push({
id: categoryId,
name: categoryName,
color: categoryColor
});
// Update dropdowns
populateCategoryDropdown();
populateCategoryList();
showToast('Category added successfully');
document.getElementById('categoryName').value = '';
});
}
// Toggle all category filters
const toggleAllFiltersBtn = document.getElementById('toggleAllFilters');
if (toggleAllFiltersBtn) {
toggleAllFiltersBtn.addEventListener('click', function () {
const checkboxes = document.querySelectorAll('.category-filter');
// Check if all are currently checked
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
// Toggle all checkboxes to the opposite state
checkboxes.forEach(checkbox => {
checkbox.checked = !allChecked;
});
// Update icon based on new state
const icon = this.querySelector('i');
if (icon) {
icon.className = !allChecked ? 'fa fa-check-square' : 'fa fa-square';
}
// Trigger filtering
filterEvents();
showToast(!allChecked ? 'All categories enabled' : 'All categories disabled');
});
}
// Filter events when checkboxes change
const categoryFilters = document.querySelectorAll('.category-filter');
if (categoryFilters && categoryFilters.length > 0) {
categoryFilters.forEach(checkbox => {
checkbox.addEventListener('change', filterEvents);
});
}
// Helper functions
// Format dates for input elements
function formatDateForInput(date) {
if (!date) return '';
const localDate = new Date(date);
// Format: YYYY-MM-DDTHH:MM
const year = localDate.getFullYear();
const month = String(localDate.getMonth() + 1).padStart(2, '0');
const day = String(localDate.getDate()).padStart(2, '0');
const hours = String(localDate.getHours()).padStart(2, '0');
const minutes = String(localDate.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
// Show toast notifications
function showToast(message) {
const toastContainer = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast align-items-center show text-white bg-primary border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toast);
// Auto-remove after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Populate category dropdown in event form
function populateCategoryDropdown() {
const select = document.getElementById('eventCategory');
select.innerHTML = '<option value="">Select Category</option>';
eventCategories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.name;
option.style.color = category.color;
select.appendChild(option);
});
}
// Populate category list in management modal
function populateCategoryList() {
const list = document.getElementById('categoryList');
list.innerHTML = '';
eventCategories.forEach((category, index) => {
// Don't allow deletion of default categories (first 7)
const isDefaultCategory = index < 7;
const item = document.createElement('li');
item.className = 'list-group-item d-flex align-items-center justify-content-between';
item.innerHTML = `
<div class="d-flex align-items-center">
<span class="badge d-block rounded-circle p-1" style="background-color: ${category.color};"></span>
<span class="ms-2">${category.name}</span>
</div>
${!isDefaultCategory ?
`<button type="button" class="btn btn-xs btn-outline-danger delete-category" data-id="${category.id}">
Delete
</button>` :
''}
`;
list.appendChild(item);
});
// Add event listeners to delete buttons
document.querySelectorAll('.delete-category').forEach(button => {
button.addEventListener('click', function () {
const categoryId = this.getAttribute('data-id');
deleteCategory(categoryId);
});
});
}
// Delete a category
function deleteCategory(categoryId) {
// Find the index of the category
const index = eventCategories.findIndex(cat => cat.id === categoryId);
if (index === -1 || index < 7) return; // Don't delete default categories
// Get the category name for the confirmation message
const categoryName = eventCategories[index].name;
// Remove the category
eventCategories.splice(index, 1);
// Update the UI
populateCategoryDropdown();
populateCategoryList();
setupFilterControls();
// Update events that used this category
calendar.getEvents().forEach(event => {
if (event.extendedProps.category === categoryId) {
event.setExtendedProp('category', 'general');
event.setProp('backgroundColor', eventCategories[0].color);
event.setProp('borderColor', eventCategories[0].color);
}
});
showToast(`Category "${categoryName}" deleted`);
}
// Set up filter controls
function setupFilterControls() {
const container = document.getElementById('category-filters');
if (!container) return;
container.innerHTML = '';
eventCategories.forEach(category => {
const div = document.createElement('div');
div.className = 'form-check';
div.innerHTML = `
<input class="form-check-input category-filter" type="checkbox" value="${category.id}" id="filter-${category.id}">
<label class="form-check-label d-flex align-items-center" for="filter-${category.id}">
<span class="badge rounded-circle p-1 me-2" style="background-color: ${category.color}; width: 0.25rem; height: 0.25rem;">&nbsp;</span>
${category.name}
</label>
`;
container.appendChild(div);
});
// Add event listeners to checkboxes after they're created
document.querySelectorAll('.category-filter').forEach(checkbox => {
checkbox.addEventListener('change', filterEvents);
});
}
// Filter events based on category checkboxes
function filterEvents() {
if (!calendar) return; // Prevent execution if calendar is not initialized
const selectedCategories = [];
document.querySelectorAll('.category-filter:checked').forEach(checkbox => {
selectedCategories.push(checkbox.value);
});
console.log("Selected categories:", selectedCategories); // For debugging
// If general is selected, make sure any uncategorized events show up
const includeGeneral = selectedCategories.includes('general');
// Handle all events, including those without a category
calendar.getEvents().forEach(event => {
// Get the event's category, defaulting to 'general' if not set
let eventCategory = event.extendedProps && event.extendedProps.category ?
event.extendedProps.category : 'general';
console.log(`Event: ${event.title}, Category: ${eventCategory}`); // For debugging
// Show all events if no categories are selected OR the event's category is selected
// OR if the event has no category and general is selected
if (selectedCategories.length === 0 ||
selectedCategories.includes(eventCategory) ||
(eventCategory === 'general' && includeGeneral)) {
event.setProp('display', 'auto');
} else {
event.setProp('display', 'none');
}
});
// Try both update methods to ensure rendering
calendar.updateSize();
}
// Export events as JSON
const exportEventsBtn = document.getElementById('exportEvents');
if (exportEventsBtn) {
exportEventsBtn.addEventListener('click', function () {
const events = calendar.getEvents().map(event => {
return {
id: event.id,
title: event.title,
start: event.start,
end: event.end,
allDay: event.allDay,
backgroundColor: event.backgroundColor,
borderColor: event.borderColor,
description: event.extendedProps.description,
location: event.extendedProps.location,
category: event.extendedProps.category
};
});
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(events, null, 2));
const downloadAnchor = document.createElement('a');
downloadAnchor.setAttribute("href", dataStr);
downloadAnchor.setAttribute("download", "calendar-events.json");
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
downloadAnchor.remove();
});
}
// Import events from JSON
const importEventsBtn = document.getElementById('importEvents');
if (importEventsBtn) {
importEventsBtn.addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (e) {
try {
const events = JSON.parse(e.target.result);
// Remove current events
calendar.getEvents().forEach(event => event.remove());
// Add imported events
events.forEach(event => {
calendar.addEvent({
id: event.id || 'imported-' + new Date().getTime() + Math.random().toString(36).substr(2, 5),
title: event.title,
start: new Date(event.start),
end: event.end ? new Date(event.end) : null,
allDay: event.allDay,
backgroundColor: event.backgroundColor,
borderColor: event.borderColor,
extendedProps: {
description: event.description,
location: event.location,
category: event.category
}
});
});
showToast('Events imported successfully');
} catch (err) {
showToast('Error importing events: ' + err.message);
}
};
reader.readAsText(file);
});
}
// Print calendar button
const printCalendarBtn = document.getElementById('printCalendar');
if (printCalendarBtn) {
printCalendarBtn.addEventListener('click', function () {
window.print();
});
}
});
@@ -0,0 +1,350 @@
// NOTE: The scripts is used for all iconography pages and for demo purposes, if you wish you may use any part of the code for your own project
// Global variables (add new ones)
let currentSvgWeight = 'sa-thin'; // Default weight for SVG icons
let isNoFill = false; // Default no-fill state for SVG icons
// Global variables
let allIcons = [];
let searchHistory = new Set();
let searchTimeout = null;
let currentToast = null;
let iconMappings = {};
let currentSearchTerm = '';
let currentIconSet = 'sa';
// Semantic matches
function findSemanticMatches(searchTerm) {
searchTerm = searchTerm.toLowerCase().replace(/\s+/g, '-');
if (iconMappings[searchTerm]) return iconMappings[searchTerm];
const partialMatches = Object.keys(iconMappings).filter(key => {
const normalizedKey = key.toLowerCase().replace(/\s+/g, '-').replace(/^-/, ''); // Strip leading hyphen
return normalizedKey.includes(searchTerm) || searchTerm.includes(normalizedKey);
});
if (partialMatches.length > 0) return partialMatches.flatMap(key => iconMappings[key]);
for (const category in iconMappings) {
const categoryMatches = Object.keys(iconMappings[category]).filter(key => {
const normalizedKey = key.toLowerCase().replace(/\s+/g, '-').replace(/^-/, ''); // Strip leading hyphen
return normalizedKey.includes(searchTerm) || searchTerm.includes(normalizedKey);
});
if (categoryMatches.length > 0) return categoryMatches.flatMap(key => iconMappings[category][key]);
}
return null;
}
// Suggestions with Levenshtein distance
function findSuggestions(searchTerm) {
if (!searchTerm || searchTerm.length < 2) return [];
searchTerm = searchTerm.replace(/\s+/g, '-').replace(/^-/, ''); // Strip leading hyphen
const semanticMatches = findSemanticMatches(searchTerm);
if (semanticMatches) {
return [...new Set(semanticMatches)].filter(icon =>
allIcons.includes(`-${icon}`) || allIcons.includes(icon)
).map(icon => icon.replace(/^-/, '')); // Clean icons in results
}
return allIcons
.map(icon => {
const iconName = icon.startsWith('-') ? icon.substring(1) : icon;
return {
name: iconName,
distance: levenshteinDistance(searchTerm.toLowerCase(), iconName.toLowerCase())
};
})
.filter(item => {
const maxDistance = Math.min(Math.floor(searchTerm.length * 0.4), 3);
return item.distance > 0 && item.distance <= maxDistance;
})
.sort((a, b) => a.distance - b.distance)
.slice(0, 3)
.map(item => item.name);
}
function levenshteinDistance(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
for (let i = 0; i <= b.length; i++) matrix[i][0] = i;
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
matrix[i][j] = b.charAt(i - 1) === a.charAt(j - 1) ?
matrix[i - 1][j - 1] :
Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
return matrix[b.length][a.length];
}
// Filter logic
function initializeFilter() {
const searchInput = document.getElementById('searchIcons');
searchInput.addEventListener('input', function () {
currentSearchTerm = this.value.trim().replace(/^-/, ''); // Strip leading hyphen
filterIcons();
});
}
function filterIcons() {
const searchTerms = currentSearchTerm.toLowerCase().split(/\s+/).filter(term => term.length > 0).map(term => term.replace(/^-/, '')); // Strip leading hyphens
document.querySelectorAll('#iconList li').forEach(item => {
const text = item.textContent.toLowerCase().replace(/^-/, ''); // Strip leading hyphen
const matches = searchTerms.every(term => text.includes(term));
item.classList.toggle('js-filter-hide', !matches);
});
updateVisibleCount();
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateSearchHistory(currentSearchTerm), 1000);
const visibleIcons = document.querySelectorAll('#iconList li:not(.js-filter-hide)').length;
const suggestionsContainer = document.getElementById('suggestions');
if (currentSearchTerm.length >= 2 && visibleIcons < 10) {
const suggestions = findSuggestions(currentSearchTerm);
suggestionsContainer.innerHTML = suggestions.length > 0 ?
`<span class="suggest-title">Did you mean?</span> ${suggestions.map(s =>
`<span class="suggestion px-1" onclick="applySearch('${s}')">${s}</span>`).join(' ')}` :
'';
} else {
suggestionsContainer.innerHTML = '';
}
}
// Load icon set
async function loadIconSet(iconSet = 'sa') {
const iconSets = {
'sa': { icons: 'json/sa-icons.json', mappings: 'json/sa-mappings.json', prefix: 'sa' },
'base': { icons: 'json/sa-base.json', mappings: 'json/sa-mappings.json', prefix: 'sa' },
'svg': { icons: 'json/sa-svg-icons.json', mappings: 'json/sa-svg-mappings.json', prefix: 'svg' },
'fal': { icons: 'json/fa-icons.json', mappings: 'json/fa-mappings.json', prefix: 'fal' },
'fas': { icons: 'json/fa-icons.json', mappings: 'json/fa-mappings.json', prefix: 'fas' },
'far': { icons: 'json/fa-icons.json', mappings: 'json/fa-mappings.json', prefix: 'far' },
'fad': { icons: 'json/fa-duotone.json', mappings: 'json/fa-mappings.json', prefix: 'fad' },
'fab': { icons: 'json/fa-brands.json', mappings: 'json/fa-mappings.json', prefix: 'fab' },
'material': { icons: 'json/material-icons.json', mappings: 'json/material-mappings.json', prefix: 'material' }
};
const selectedSet = iconSets[iconSet] || iconSets['sa'];
// If first load or switching between different icon families, fetch new data
if (!allIcons.length || (currentIconSet === 'sa' || currentIconSet === 'svg') !== (iconSet === 'sa' || iconSet === 'svg')) {
try {
const [iconsResponse, mappingsResponse] = await Promise.all([
fetch(selectedSet.icons),
fetch(selectedSet.mappings)
]);
if (!iconsResponse.ok || !mappingsResponse.ok) throw new Error('Failed to load resources');
allIcons = (await iconsResponse.json()).map(icon => icon.replace(/^-/, '')); // Strip leading hyphen from all icons
iconMappings = await mappingsResponse.json();
currentIconSet = iconSet;
generateIconList(allIcons, selectedSet.prefix);
initializeFilter();
} catch (error) {
console.error('Error loading icon set:', error);
}
} else {
// Update style for same icon family
currentIconSet = iconSet;
document.querySelectorAll('#iconList li').forEach(item => {
const iconName = item.dataset.iconName.replace(/^-/, ''); // Strip leading hyphen
const iconElement = item.querySelector('.icon-container');
iconElement.innerHTML = getIconClass(iconSet, iconName);
});
}
// Re-apply filter if search term exists
if (currentSearchTerm) filterIcons();
updateVisibleCount();
}
// Generate icon list
function generateIconList(icons, iconPrefix) {
const iconList = document.getElementById('iconList');
iconList.innerHTML = icons.map(icon => {
const cleanIconName = icon.replace(/^-/, ''); // Ensure no leading hyphen
const iconClass = getIconClass(iconPrefix, cleanIconName);
const displayName = cleanIconName;
const isSvg = iconPrefix === 'svg';
return `
<li class="col-4 col-sm-3 col-md-3 col-lg-2 col-xl-2 col-xxl-1 d-flex justify-content-center align-items-center mb-g" data-icon-name="${displayName}">
<a href="#" class="js-showcase-icon rounded color-fusion-300 p-0 m-0 d-flex flex-column w-100 shadow-hover-2 ${isSvg ? 'has-svg' : ''}">
<div class="icon-preview rounded-top w-100 position-relative">
<div class="icon-container rounded-top d-flex align-items-center justify-content-center w-100 pt-3 pb-3 pe-2 ps-2 position-absolute">
${iconClass}
</div>
</div>
<div class="rounded-bottom p-1 w-100 d-flex justify-content-center align-items-center text-center mt-auto">
<span class="nav-link-text small text-muted text-truncate">${displayName}</span>
</div>
</a>
</li>
`;
}).join('');
updateVisibleCount();
addIconClickHandlers();
}
// Search history
function updateSearchHistory(term) {
if (term && term.length >= 2) {
searchHistory.add(term.replace(/^-/, '')); // Strip leading hyphen
if (searchHistory.size > 5) searchHistory.delete([...searchHistory][0]);
renderSearchHistory();
}
}
function renderSearchHistory() {
const historyContainer = document.getElementById('searchHistory');
historyContainer.innerHTML = [...searchHistory].map(term =>
`<span class="badge bg-secondary me-1" onclick="applySearch('${term}')">
<span class="text-truncate-xs overflow-hidden">${term}</span>
<i class="sa sa-close ms-1" onclick="event.stopPropagation(); removeFromHistory('${term}')"></i>
</span>`
).join('');
}
function removeFromHistory(term) {
searchHistory.delete(term);
renderSearchHistory();
}
function applySearch(term) {
const searchIcons = document.getElementById('searchIcons');
searchIcons.value = term.replace(/^-/, ''); // Strip leading hyphen
currentSearchTerm = term.replace(/^-/, ''); // Strip leading hyphen
filterIcons();
}
// Icon utilities
function updateVisibleCount() {
const visibleIcons = document.querySelectorAll('#iconList li:not(.js-filter-hide)').length;
document.querySelector('.results-count').textContent =
`Showing ${visibleIcons} of ${allIcons.length} icons`;
}
// Update getIconClass to handle SVG classes dynamically (add this at the end of the existing getIconClass function)
const getIconClass = (prefix, icon) => {
const cleanIcon = icon.replace(/^-/, ''); // Ensure no leading hyphen
switch (prefix) {
case 'svg':
const weightClass = currentSvgWeight; // Use the current weight from dropdown
const fillClass = isNoFill ? ' sa-nofill' : ''; // Add no-fill class if checkbox is checked
return `<svg class="sa-icon ${weightClass}${fillClass}"><use href="icons/sprite.svg#${cleanIcon}"></use></svg>`;
case 'fal':
return `<i class="fal fa-${cleanIcon}"></i>`;
case 'fas':
return `<i class="fas fa-${cleanIcon}"></i>`;
case 'far':
return `<i class="far fa-${cleanIcon}"></i>`;
case 'fad':
return `<i class="fad fa-${cleanIcon}"></i>`;
case 'fab':
return `<i class="fab fa-${cleanIcon}"></i>`;
case 'material':
return `<i class="material-icons">${cleanIcon}</i>`;
case 'sa':
return `<i class="sa sa-${cleanIcon}"></i>`; // SmartAdmin format: sa sa-iconname
case 'base':
return `<i class="sa base-${cleanIcon}"></i>`;
default:
return `<i class="${prefix} ${prefix}-${cleanIcon}"></i>`;
}
};
function addIconClickHandlers() {
document.querySelectorAll('.js-showcase-icon').forEach(iconElement => {
iconElement.addEventListener('click', function (event) {
event.preventDefault();
const iconEl = this.querySelector('svg') || this.querySelector('i');
if (iconEl) {
if (iconEl.tagName.toLowerCase() === 'svg') {
const useEl = iconEl.querySelector('use');
const iconClass = useEl ? useEl.getAttribute('href') : null;
if (iconClass) {
// Include current weight and fill classes in the copied SVG
const weightClass = currentSvgWeight;
const fillClass = isNoFill ? ' sa-nofill' : '';
const svgMarkup = `<svg class="sa-icon ${weightClass}${fillClass}"><use href="${iconClass}"></use></svg>`;
copyToClipboard(svgMarkup);
showToast(svgMarkup);
}
} else {
const iconClass = iconEl.className;
copyToClipboard(iconClass);
showToast(iconClass);
}
}
});
});
}
function copyToClipboard(text) {
let copyText = text;
if (text.includes('sprite.svg#')) {
// Keep the weight and fill classes when copying SVG markup
const weightClass = currentSvgWeight;
const fillClass = isNoFill ? ' sa-nofill' : '';
copyText = `<svg class="sa-icon ${weightClass}${fillClass}"><use href="${text.split('href="')[1].split('"')[0]}"></use></svg>`;
}
navigator.clipboard.writeText(copyText).then(() =>
console.log('Icon class/markup copied:', copyText)
).catch(err => console.error('Failed to copy:', err));
}
function escapeHTML(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, "'");
}
function showToast(iconClass) {
if (currentToast) {
currentToast.hide();
}
const toastElement = document.getElementById('liveToast');
const toastBody = toastElement.querySelector('.toast-body');
const isSvg = iconClass.includes('sprite.svg#');
// For SVGs, we'll only show the icon once in the message
if (isSvg) {
const iconName = iconClass.split('#')[1].split('"')[0]; // Get "iconname"
toastBody.innerHTML = `${iconClass}<span class="fw-bold ms-2"><span class="disabled">'#${iconName}'</span> &#x2714;copied</span>`;
} else {
const iconDisplay = `<i class="${escapeHTML(iconClass)} display-1 p-1 m-0 me-2 text-primary"></i>`;
toastBody.innerHTML = `${iconDisplay}<span class="fw-bold"><span class="disabled">'${escapeHTML(iconClass)}'</span> &#x2714;copied</span>`;
}
currentToast = new bootstrap.Toast(toastElement, {
delay: 2500,
animation: true,
autohide: true
});
toastElement.addEventListener('hidden.bs.toast', () => {
currentToast = null;
}, { once: true });
currentToast.show();
}
function updateSvgClasses() {
document.querySelectorAll('#iconList li').forEach(item => {
const svgElement = item.querySelector('svg');
if (svgElement) {
const weightClass = currentSvgWeight;
const fillClass = isNoFill ? ' sa-nofill' : '';
// Update classes using classList
svgElement.setAttribute('class', `sa-icon ${weightClass}${fillClass}`);
}
});
}
// Initialize when DOM is loaded
// document.addEventListener('DOMContentLoaded', function() {
// loadIconSet('sa')
// .then(() => console.log('Icon set loaded successfully'))
// .catch(error => console.error('Failed to load icon set:', error));
// });
@@ -0,0 +1,68 @@
VANTA.HALO({
el: "#net",
mouseControls: false,
touchControls: false,
gyroControls: false,
color: 0xfd3995,
size: 1.6,
scale: 0.75,
xOffset: 0.22,
scaleMobile: 0.50,
});
// Typewriter effect script (ES5)
var textElement = document.getElementById('typewriter-text'); // Element to display text
var messages = [
"The world's first Admin WebApp built with Artificial Intelligence",
"AIready by design: prewritten prompt instructions included",
"An advanced, jQueryfree Bootstrap 5 Admin Dashboard UI",
"Built for the next generation of enterprise web applications",
"Clean, scalable, and engineered for futureforward growth",
"Continuously updated by a team of expert developers",
"Enterprise-grade performance with beautiful design",
"One dashboard template, unlimited use cases",
"Modern, responsive UI built for serious development"
];
var currentMessageIndex = 0;
var isDeleting = false;
var typingSpeed = 15; // Speed of typing/deleting in milliseconds
var pauseSpeed = 3000; // Pause between cycles in milliseconds
function typeWrite() {
var fullText = messages[currentMessageIndex];
var currentText = textElement.textContent;
var isEndOfMessage = !isDeleting && currentText === fullText;
var isStartOfMessage = isDeleting && currentText === '';
if (isEndOfMessage) {
setTimeout(function () {
isDeleting = true;
typeWrite();
}, pauseSpeed);
return;
}
if (isStartOfMessage) {
isDeleting = false;
currentMessageIndex = (currentMessageIndex + 1) % messages.length;
typeWrite();
return;
}
if (isDeleting) {
currentText = currentText.slice(0, -1);
}
else {
currentText = fullText.slice(0, currentText.length + 1);
}
textElement.textContent = currentText;
setTimeout(typeWrite, typingSpeed);
}
// Start the typewriter effect
if (textElement) {
textElement.textContent = '';
typeWrite();
}
else {
console.error('Typewriter text element not found');
}
@@ -0,0 +1,115 @@
document.addEventListener('DOMContentLoaded', function () {
// Add styles for suggestions
const style = document.createElement('style');
style.textContent = ['.suggestions-container {', ' margin: -0.5rem 0 1rem 0;', ' font-size: 0.9rem;', ' color: #666;', ' padding: 0', '}', '.did-you-mean {', ' padding: 0.25rem 0;', ' font-weight: 500;', ' color: var(--danger-500);', '}', '.did-you-mean a {', ' color: #2196F3;', ' text-decoration: none;', ' font-weight: 500;', '}', '.did-you-mean a:hover {', ' text-decoration: underline;', '}'].join('\n');
document.head.appendChild(style);
// Helper function to safely initialize ListFilter
function initListFilter(listId, inputId, options) {
const list = document.querySelector(listId);
const input = document.querySelector(inputId);
if (!list || !input) {
console.warn('ListFilter initialization failed: ' + (!list ? 'List element' : 'Input element') + ' not found');
return null;
}
try {
// Add suggestions container if it doesn't exist
const suggestionsContainer = document.createElement('div');
suggestionsContainer.id = inputId + 'Suggestions';
suggestionsContainer.className = 'suggestions-container';
// Find the input-group div and insert the suggestions container after it
const inputGroup = input.closest('.input-group');
if (inputGroup && inputGroup.parentNode) {
inputGroup.parentNode.insertBefore(suggestionsContainer, inputGroup.nextSibling);
}
// Add Levenshtein distance function
function levenshteinDistance(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
for (let i = 0; i <= b.length; i++) matrix[i][0] = i;
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
matrix[i][j] = b.charAt(i - 1) === a.charAt(j - 1) ? matrix[i - 1][j - 1] : Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
return matrix[b.length][a.length];
}
// Add find suggestions function
function findSuggestions(searchTerm, items) {
if (!searchTerm || searchTerm.length < 2) return [];
searchTerm = searchTerm.toLowerCase();
return items.map(item => {
const text = item.querySelector('.nav-link-text')?.textContent.toLowerCase() || '';
return {
text: text,
distance: levenshteinDistance(searchTerm, text)
};
}).filter(item => {
const maxDistance = Math.min(Math.floor(searchTerm.length * 0.4), 3);
return item.distance > 0 && item.distance <= maxDistance;
}).sort((a, b) => a.distance - b.distance).slice(0, 3).map(item => item.text);
}
// Modify the onFilter callback to include suggestions
const originalOnFilter = options.onFilter;
options.onFilter = function (filter) {
if (originalOnFilter) originalOnFilter(filter);
const suggestionsContainer = document.getElementById(inputId + 'Suggestions');
if (!suggestionsContainer) {
console.error('Suggestions container not found');
return;
}
const visibleItems = list.querySelectorAll('li:not(.js-filter-hide)');
const suggestions = findSuggestions(filter, Array.from(list.querySelectorAll('li')));
if (filter.length >= 2 && visibleItems.length < 10 && suggestions.length > 0) {
// Get the first suggestion and capitalize it
const suggestion = suggestions[0];
const capitalizedSuggestion = suggestion.charAt(0).toUpperCase() + suggestion.slice(1);
suggestionsContainer.innerHTML = ['<div class="did-you-mean">', ' Did you mean: <a href="#" class="suggestion-link">' + capitalizedSuggestion + '</a>', '</div>'].join('\n');
// Add click event listener to the suggestion link
const suggestionLink = suggestionsContainer.querySelector('.suggestion-link');
if (suggestionLink) {
suggestionLink.addEventListener('click', function (e) {
e.preventDefault();
input.value = suggestion;
input.dispatchEvent(new Event('input',
{
bubbles: true
}));
suggestionsContainer.innerHTML = '';
});
}
}
else {
suggestionsContainer.innerHTML = '';
}
};
return new ListFilter(listId, inputId, options);
}
catch (error) {
console.error('ListFilter initialization error:', error);
return null;
}
}
// File Explorer Example with Nested Structure
initListFilter('#fileExplorer', '#fileFilterInput',
{
messageSelector: '#fileFilterMessage',
debounceWait: 150,
minLength: 1,
onFilter: function (filter) {
console.log('File filter:', filter);
}
});
// Did you mean suggestions example
initListFilter('#suggestionsNav', '#suggestionsFilterInput',
{
messageSelector: '#suggestionsFilterMessage',
debounceWait: 200,
minLength: 1,
onFilter: function (filter) {
console.log('Suggestions filter:', filter);
}
});
});
@@ -0,0 +1,866 @@
// All imports for the Marketing Dashboard
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
import { PeityAPI } from './../thirdparty/peity.es6.js';
document.addEventListener('DOMContentLoaded', function () {
'use strict';
/***************************************************************/
/* Marketing Performance Chart */
/***************************************************************/
const marketingPerformanceChart = new ApexCharts(document.querySelector('#marketing-profits-chart'), {
chart: {
height: 350,
type: 'line',
stacked: false,
toolbar: {
show: false
},
zoom: {
enabled: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800
},
parentHeightOffset: 0,
fontFamily: 'inherit'
},
colors: [
window.colorMap.primary[300].hex,
window.colorMap.warning[300].hex,
window.colorMap.success[300].hex
],
stroke: {
width: [0, 3, 3],
curve: 'smooth'
},
fill: {
type: ['gradient', 'solid', 'solid'],
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.5,
gradientToColors: [window.colorMap.primary[200].hex],
inverseColors: false,
opacityFrom: 1,
opacityTo: 0.3
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
padding: {
left: -5,
right: 0,
top: -20,
bottom: -5
},
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: true
}
}
},
plotOptions: {
bar: {
columnWidth: '50%',
endingShape: 'rounded',
borderRadius: 5,
}
},
dataLabels: {
enabled: false
},
markers: {
size: 4,
strokeWidth: 0,
hover: {
size: 6
}
},
xaxis: {
type: 'category',
categories: ['Jan 2013', 'Apr 2013', 'Jul 2013', 'Oct 2013', 'Jan 2014', 'Apr 2014', 'Jul 2014', 'Oct 2014', 'Jan 2015', 'Apr 2015', 'Jul 2015', 'Oct 2015', 'Jan 2016', 'Apr 2016'],
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.rgba(0.7),
fontSize: '10px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
min: 0,
max: 1200,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.rgba(0.7),
fontSize: '10px'
},
formatter: function (val) {
return val.toFixed(0);
}
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (y) {
if (typeof y !== "undefined") {
return y.toFixed(0);
}
return y;
}
}
},
legend: {
show: false
},
series: [
{
name: 'Target Profit',
type: 'column',
data: [150, 650, 200, 650, 800, 1050, 350, 750, 500, 250, 650, 250, 350, 350]
},
{
name: 'Actual Profit',
type: 'line',
data: [50, 70, 90, 80, 300, 950, 800, 700, 650, 30, 100, 80, 30, 30]
},
{
name: 'User Signups',
type: 'line',
data: [650, 430, 800, 350, 450, 450, 450, 470, 250, 830, 650, 250, 350, 350]
}
]
});
marketingPerformanceChart.render();
/***************************************************************/
/* Returning Target Chart */
/***************************************************************/
const returningTargetChart = new ApexCharts(document.querySelector('#returning-target-chart'), {
chart: {
height: 350,
type: 'area',
stacked: false,
toolbar: {
show: false
},
zoom: {
enabled: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800
},
parentHeightOffset: 0,
fontFamily: 'inherit'
},
colors: [
window.colorMap.success[200].hex, // New Customer (darker blue)
window.colorMap.primary[200].hex, // Returning Customer (lighter teal)
],
stroke: {
width: 2,
curve: 'straight'
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
opacityFrom: 0.8,
opacityTo: 0.1,
stops: [0, 90, 100]
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.07),
strokeDashArray: 0,
position: 'back',
padding: {
left: 5
},
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: false
}
}
},
dataLabels: {
enabled: false
},
markers: {
size: 4,
strokeWidth: 0,
hover: {
size: 6
}
},
xaxis: {
type: 'numeric',
categories: [0, 100, 200, 300, 400, 500, 600, 700],
tickAmount: 8,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.rgba(0.7),
fontSize: '10px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
max: 180,
tickAmount: 10,
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (y) {
if (typeof y !== "undefined") {
return y.toFixed(0);
}
return y;
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'left',
fontSize: '12px',
fontFamily: 'inherit',
offsetY: 10,
offsetX: -35,
itemMargin: {
horizontal: 10,
vertical: 0
},
markers: {
width: 8,
height: 8,
radius: 0,
}
},
series: [
{
name: 'Returning Customer',
data: [140, 120, 95, 80, 60, 95, 70, 50]
},
{
name: 'New Customer',
data: [110, 90, 70, 55, 50, 75, 50, 30]
},
]
});
returningTargetChart.render();
/***************************************************************/
/* Efficiency Metrics Chart */
/***************************************************************/
const efficiencyMetricsChart = new ApexCharts(document.querySelector('#efficiency-metrics-chart'), {
chart: {
height: 259,
type: 'area',
stacked: false,
toolbar: {
show: false
},
zoom: {
enabled: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800
},
parentHeightOffset: 0,
fontFamily: 'inherit'
},
colors: [
window.colorMap.primary[300].hex, // Sessions (blue)
window.colorMap.success[300].hex, // New Sessions (teal)
window.colorMap.warning[300].hex, // Bounce Rate (yellow)
window.colorMap.info[300].hex // Clickthrough (cyan)
],
stroke: {
width: 2,
curve: 'straight'
},
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 0.4,
opacityFrom: 0.8,
opacityTo: 0.1,
stops: [0, 90, 100]
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
padding: {
top: -25,
},
xaxis: {
lines: {
show: true
}
},
yaxis: {
lines: {
show: true
}
}
},
dataLabels: {
enabled: false
},
markers: {
size: 3,
strokeWidth: 0,
hover: {
size: 5
}
},
xaxis: {
type: 'category',
categories: ['2am', '3am', '4am', '5am', '6am', '7am', '8am', '9am', '1pm', '2pm', '3pm', '4pm'],
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.rgba(0.7),
fontSize: '10px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
min: 0,
max: 300000,
tickAmount: 3,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.rgba(0.7),
fontSize: '10px'
},
formatter: function (val) {
if (val >= 1000) {
return (val / 1000) + 'K';
}
return val;
}
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: function (y) {
if (typeof y !== "undefined") {
if (y >= 1000) {
return Math.round(y).toLocaleString();
}
return y.toFixed(0);
}
return y;
}
}
},
legend: {
show: false
},
series: [
{
name: 'Sessions',
data: [20000, 25000, 40000, 50000, 60000, 150000, 190000, 180000, 200000, 100000, 100000, 90000]
},
{
name: 'New Sessions',
data: [15000, 30000, 35000, 60000, 80000, 110000, 230000, 200000, 180000, 230000, 160000, 100000]
},
{
name: 'Bounce Rate',
data: [10000, 15000, 20000, 25000, 30000, 40000, 60000, 80000, 100000, 120000, 140000, 160000]
},
{
name: 'Clickthrough',
data: [5000, 10000, 15000, 20000, 25000, 30000, 40000, 50000, 60000, 70000, 80000, 90000]
}
]
});
efficiencyMetricsChart.render();
/***************************************************************/
/* Peity Charts */
/***************************************************************/
// Global default settings for charts
const defaults = {
// Default background color for donut and pie charts (replaces the gray)
donutBackground: 'var(--bs-border-color)', // Light purple background instead of gray
pieBackground: 'var(--bs-border-color)' // Same for pie charts
};
// Helper to create peity charts (similar to jQuery plugin style)
function createPeity(selector, type, customOptions = {}) {
document.querySelectorAll(selector).forEach(element => {
// Parse data-peity attribute for options
let options = { ...customOptions };
const dataAttr = element.getAttribute('data-peity');
if (dataAttr) {
try {
options = { ...options, ...JSON.parse(dataAttr) };
} catch (e) {
console.warn('Invalid data-peity format for', selector);
}
}
// Apply defaults for donut and pie charts background colors
if (type === 'donut' || type === 'pie') {
// If fill is not defined or is an empty array, set default colors
if (!options.fill || options.fill.length === 0) {
options.fill = type === 'donut' ?
['var(--primary-500)', defaults.donutBackground] :
['var(--primary-500)', defaults.pieBackground];
}
// If fill is defined but doesn't include a background color (for fractions like "1/4")
else if (Array.isArray(options.fill) && options.fill.length === 1) {
options.fill.push(type === 'donut' ? defaults.donutBackground : defaults.pieBackground);
}
// If data contains multiple values (e.g., "10,4,4,6"), don't add background
}
PeityAPI.create(element, type, options);
});
}
try {
// Simple chart initializations with default settings
createPeity('.peity-pie', 'pie');
createPeity('.peity-donut', 'donut');
createPeity('.peity-line', 'line');
createPeity('.peity-bar', 'bar');
// Updating chart with animation
const updatingChart = document.querySelector('.updating-chart');
if (updatingChart) {
const values = updatingChart.textContent.split(',').map(Number);
const chart = PeityAPI.create(updatingChart, 'line', {
width: 200,
height: 40,
stroke: 'var(--info-500)',
fill: 'var(--info-200)',
min: 0,
max: 10
});
setInterval(function () {
values.shift();
values.push(Math.floor(Math.random() * 10));
updatingChart.textContent = values.join(',');
chart.draw();
}, 500);
}
// Bar charts with negative values (red/green coloring)
document.querySelectorAll('.bar-negative').forEach(element => {
const values = element.textContent.split(',').map(Number);
PeityAPI.create(element, 'bar', {
height: 40,
width: 110,
fill: values.map(value => value > 0 ? 'var(--success-500)' : 'var(--danger-500)')
});
});
// Bar charts with color transitions
document.querySelectorAll('.bar-transition').forEach(element => {
const values = element.textContent.split(',').map(Number);
PeityAPI.create(element, 'bar', {
height: 40,
width: 110,
fill: values.map((_, i, all) => {
const g = parseInt((i / all.length) * 255);
return `rgb(255, ${g}, ${g})`;
})
});
});
// Process all remaining elements with data-peity attribute
document.querySelectorAll('[data-peity]').forEach(element => {
// Skip elements already handled by specific selectors
if (element.classList.contains('peity-pie') ||
element.classList.contains('peity-donut') ||
element.classList.contains('peity-line') ||
element.classList.contains('peity-bar') ||
element.classList.contains('updating-chart') ||
element.classList.contains('bar-negative') ||
element.classList.contains('bar-transition')) {
return;
}
// Auto-detect chart type based on content
const content = element.textContent.trim();
let type;
if (content.includes('/')) {
type = 'donut'; // Fraction data is best for donut
} else if (content.includes(',')) {
// If it has commas, it's likely a series
const hasNegative = content.split(',').some(val => parseFloat(val) < 0);
type = hasNegative ? 'bar' : 'line'; // Bars handle negative values better visually
} else {
type = 'pie'; // Default fallback
}
// Get options from data attribute
let options = {};
const dataAttr = element.getAttribute('data-peity');
if (dataAttr) {
try {
options = JSON.parse(dataAttr);
} catch (e) {
console.warn('Invalid data-peity format for element', element);
}
}
// Apply global background color for donut/pie charts
if ((type === 'donut' || type === 'pie') && content.includes('/')) {
// For fraction notation (e.g. "1/4"), make sure we have a background color
if (!options.fill || !Array.isArray(options.fill) || options.fill.length < 2) {
options.fill = options.fill || [];
// If we have one color already, keep it and add the background
if (options.fill.length === 1) {
options.fill.push(type === 'donut' ? defaults.donutBackground : defaults.pieBackground);
} else {
// No colors defined yet, set defaults
options.fill = [
type === 'donut' ? 'var(--primary-500)' : 'var(--success-500)',
type === 'donut' ? defaults.donutBackground : defaults.pieBackground
];
}
}
}
// Create chart
PeityAPI.create(element, type, options);
});
} catch (error) {
console.error('Error initializing Peity charts:', error);
}
/***************************************************************/
/* Sales Performance Table */
/***************************************************************/
// Initialize SmartTables with the table ID and options
const table = new SmartTables('sales-performance-table', {
data: {
type: "json",
source: "json/MOCK_DATA_SALES_PERF.json",
columns: [
{ data: "CustomerID", title: "ID" },
{
data: "Name",
title: "Full Name",
render: function (data) {
// Assuming data is in format "LastName, FirstName"
if (!data) return '';
const nameParts = data.split(',');
if (nameParts.length === 2) {
const lastName = nameParts[0].trim();
const firstName = nameParts[1].trim();
return firstName + ' ' + lastName;
}
return data;
}
},
{
data: "PurchaseDate",
title: "Purchase Date",
render: function (data) {
if (!data) return '';
try {
const date = new Date(data);
if (isNaN(date.getTime())) return data;
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}-${month}-${year}`;
} catch (e) {
return data;
}
}
},
{
data: "CustomerCVV",
title: "CVV",
render: function (data) {
if (!data) return '';
const cvvId = 'cvv-' + Math.random().toString(36).substring(2, 10);
return `
<div class="d-flex align-items-center">
<span id="${cvvId}-masked">***</span>
<span id="${cvvId}-actual" class="d-none">${data}</span>
<button class="btn btn-link btn-sm p-0 ms-2 cvv-toggle" data-cvv-id="${cvvId}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
</button>
</div>
`;
}
},
{
data: "Country",
title: "Country",
render: function (data) {
if (!data) return '';
// Get country code (assuming data has the country name)
let countryCode = '';
// Simple mapping for common countries
const countryMap = {
'United States': 'us',
'USA': 'us',
'US': 'us',
'United Kingdom': 'gb',
'UK': 'gb',
'Canada': 'ca',
'Australia': 'au',
'Germany': 'de',
'France': 'fr',
'Italy': 'it',
'Spain': 'es',
'Japan': 'jp',
'China': 'cn',
'India': 'in',
'Brazil': 'br',
'Mexico': 'mx',
'Russia': 'ru'
};
// Try to get code from map
countryCode = countryMap[data] || data.toLowerCase().substring(0, 2);
// Using a div placeholder instead of img to completely prevent loading
return `
<div class="d-flex align-items-center">
<div class="flag-placeholder d-inline-block me-1 border-faded"
data-country="${countryCode}"
style="width: 20px; height: 15px; background-color: #f5f5f5;">
</div>
<span>${data}</span>
</div>
`;
}
},
{ data: "InvoiceAmount", title: "Invoice Amount" },
{ data: "CustomerEmail", title: "Email" }
]
},
export: true,
print: true,
responsive: true,
hooks: {
afterInit: function () {
// Add event listener for CVV toggles
document.addEventListener('click', function (e) {
if (e.target.closest('.cvv-toggle')) {
const button = e.target.closest('.cvv-toggle');
const cvvId = button.getAttribute('data-cvv-id');
const maskedElement = document.getElementById(cvvId + '-masked');
const actualElement = document.getElementById(cvvId + '-actual');
if (maskedElement && actualElement) {
// Toggle visibility
if (maskedElement.classList.contains('d-none')) {
maskedElement.classList.remove('d-none');
actualElement.classList.add('d-none');
// Change icon to eye
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
`;
} else {
maskedElement.classList.add('d-none');
actualElement.classList.remove('d-none');
// Change icon to eye-slash
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
</svg>
`;
}
}
}
});
// Wait for the table to be fully rendered before processing flags
// Use a longer timeout to ensure the table is completely initialized
setTimeout(function () {
// Only load flags for the first page after complete initialization
loadFlagsForVisibleRows();
}, 1000);
// Set up event listeners for search input
const searchInputs = document.querySelectorAll('.st-search');
searchInputs.forEach(function (input) {
input.addEventListener('input', function () {
// Debounce the search - wait until typing stops
clearTimeout(input.searchTimer);
input.searchTimer = setTimeout(function () {
// After search completes, load flags
setTimeout(loadFlagsForVisibleRows, 100);
}, 300);
});
});
// Set up event listeners for sorting
const tableHeaders = document.querySelectorAll('#sales-performance-table th');
tableHeaders.forEach(function (th) {
th.addEventListener('click', function () {
// After sorting completes, load flags
setTimeout(loadFlagsForVisibleRows, 100);
});
});
},
onPaginate: function () {
// This is called when user navigates to a new page
// Use a shorter timeout as pagination is usually faster
setTimeout(function () {
loadFlagsForVisibleRows();
}, 50);
},
onFilter: function () {
// This is called when filtering is applied
setTimeout(function () {
loadFlagsForVisibleRows();
}, 100);
},
afterDraw: function () {
// This catches any other table redraw events
setTimeout(function () {
loadFlagsForVisibleRows();
}, 100);
}
}
});
// Global flag to track if initial flags have been loaded
// This prevents multiple loads during initialization
let initialFlagsLoaded = false;
// Track if we're currently loading flags to prevent multiple simultaneous loads
let isLoadingFlags = false;
// Function to load flag images only for visible rows
function loadFlagsForVisibleRows() {
// Prevent multiple simultaneous calls
if (isLoadingFlags) return;
isLoadingFlags = true;
try {
// Get all flag placeholders
const flagPlaceholders = document.querySelectorAll('.flag-placeholder');
let loadedCount = 0;
// Process each placeholder
flagPlaceholders.forEach(function (placeholder) {
// Check if the placeholder is in a visible row
const row = placeholder.closest('tr');
if (row && row.offsetParent !== null) {
// Get country code
const countryCode = placeholder.getAttribute('data-country');
if (!countryCode) return;
// Create and set up the image element
const img = document.createElement('img');
img.src = `https://lipis.github.io/flag-icons/flags/4x3/${countryCode}.svg`;
img.alt = countryCode;
img.className = 'd-inline-block me-1 border-faded';
img.style.width = '20px';
img.style.height = 'auto';
// Replace the placeholder with the image
placeholder.parentNode.replaceChild(img, placeholder);
loadedCount++;
}
});
if (loadedCount > 0) {
console.log(`Loaded ${loadedCount} flags for current view`);
}
initialFlagsLoaded = true;
} finally {
// Reset loading flag
isLoadingFlags = false;
}
}
// Make function accessible globally for manual triggering if needed
window.loadTableFlags = loadFlagsForVisibleRows;
});
@@ -0,0 +1,497 @@
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const chatContainer = document.getElementById('chat_container');
const messageInput = document.getElementById('msgr_input');
const sendButton = document.getElementById('send_button');
const emojiButtons = document.querySelectorAll('.emoji');
const storyCircles = document.querySelectorAll('.story-circle');
// Mock data
const mockResponses = [
"That's interesting. Tell me more.",
"I completely understand what you mean.",
"I hadn't thought about it that way before.",
"That's great news!",
"LOL 😂 That's hilarious!",
"Really? I'm surprised to hear that.",
"I'm not sure I agree, but I see your point.",
"Let's discuss this further when we meet.",
"Thanks for letting me know!",
"Sorry to hear that. Is there anything I can do to help?",
"Can we talk about this tomorrow? I need some time to think.",
"Wow! That's amazing! 👍",
"So how are you liking SmartAdmin?"
];
const mockImages = [
'./img/demo/gallery/1.jpg',
'./img/demo/gallery/2.jpg',
'./img/demo/gallery/3.jpg',
'./img/demo/gallery/4.jpg',
'./img/demo/gallery/5.jpg',
];
//const mockEmojis = ['👍', '❤️', '😂', '👏', '😍', '🎉', '👌', '✨'];
// const mockFiles = [
// { name: 'Project_Brief.pdf', type: 'pdf', size: '1.2 MB' },
// { name: 'Meeting_Notes.docx', type: 'doc', size: '425 KB' },
// { name: 'Presentation.pptx', type: 'ppt', size: '3.8 MB' },
// { name: 'Budget.xlsx', type: 'xls', size: '890 KB' }
// ];
// Initialize Story Circles (top horizontal scrolling avatars)
if (storyCircles.length) {
storyCircles.forEach(circle => {
circle.addEventListener('click', () => {
// Deactivate all stories
storyCircles.forEach(c => c.classList.remove('active'));
// Activate clicked story
circle.classList.add('active');
});
});
}
// Handle emoji selection
if (emojiButtons.length) {
emojiButtons.forEach(emoji => {
emoji.addEventListener('click', (e) => {
e.preventDefault();
// Get emoji value from the data attribute or class
const emojiType = emoji.classList.contains('emoji--like') ? '👍' :
emoji.classList.contains('emoji--love') ? '❤️' :
emoji.classList.contains('emoji--haha') ? '😂' :
emoji.classList.contains('emoji--yay') ? '🎉' :
emoji.classList.contains('emoji--wow') ? '😮' :
emoji.classList.contains('emoji--sad') ? '😢' :
emoji.classList.contains('emoji--angry') ? '😡' : '';
if (emojiType) {
// Send the emoji directly as a message
sendMessage(emojiType);
}
});
});
}
// Send message function
function sendMessage(messageContent = null) {
// Get message from input or passed content
const message = messageContent || messageInput.value.trim();
if (!message) return;
// Clear input if using the input field
if (!messageContent) messageInput.value = '';
// Create message HTML
const currentTime = new Date();
const timeString = currentTime.getHours().toString().padStart(2, '0') + ':' +
currentTime.getMinutes().toString().padStart(2, '0');
// Add message to chat
appendMessage(message, 'sent', timeString);
// Scroll to bottom
scrollToBottom();
// Show typing indicator
showTypingIndicator();
// After a delay, show response
const responseDelay = 1000 + Math.random() * 2000; // 1-3 seconds
setTimeout(() => {
hideTypingIndicator();
generateResponse();
}, responseDelay);
}
// Generate a random response
function generateResponse() {
// Decide what type of response to generate
const responseType = Math.random();
if (responseType < 0.6) { // 60% chance of text
const randomResponse = mockResponses[Math.floor(Math.random() * mockResponses.length)];
appendMessage(randomResponse, 'get', getCurrentTime());
} else if (responseType < 0.7) { // 10% chance of image
const randomImage = mockImages[Math.floor(Math.random() * mockImages.length)];
appendImageMessage(randomImage, 'get', getCurrentTime());
} else if (responseType < 0.9) { // 20% chance of emoji only
const animatedEmojis = ['👍', '❤️', '😂', '🎉', '😮', '😢', '😡'];
const randomEmoji = animatedEmojis[Math.floor(Math.random() * animatedEmojis.length)];
appendMessage(randomEmoji, 'get', getCurrentTime());
}
// else { // 10% chance of file
// const randomFile = mockFiles[Math.floor(Math.random() * mockFiles.length)];
// appendFileMessage(randomFile, 'get', getCurrentTime());
// }
// Scroll to bottom
scrollToBottom();
}
// Show typing indicator
function showTypingIndicator() {
const typingDiv = document.createElement('div');
typingDiv.className = 'chat-segment chat-segment-get typing-indicator';
typingDiv.innerHTML = `
<div class="chat-message">
<div class="typing">
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
chatContainer.appendChild(typingDiv);
scrollToBottom();
}
// Hide typing indicator
function hideTypingIndicator() {
const typingIndicator = document.querySelector('.typing-indicator');
if (typingIndicator) {
typingIndicator.remove();
}
}
// Append message to chat
function appendMessage(message, type, time) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-segment chat-segment-${type}`;
// Check if message is just an emoji
const isJustEmoji = /^(\p{Emoji}\uFE0F?)+$/u.test(message);
// Check if message is one of our specific emojis
const emojiType = message === '👍' ? 'like' :
message === '❤️' ? 'love' :
message === '😂' ? 'haha' :
message === '🎉' ? 'yay' :
message === '😮' ? 'wow' :
message === '😢' ? 'sad' :
message === '😡' ? 'angry' : null;
let messageHtml;
if (emojiType) {
// Create the animated emoji without chat-message wrapper
messageHtml = `
<div class="emoji emoji--${emojiType}">
${emojiType === 'like' ?
`<div class="emoji__hand">
<div class="emoji__thumb"></div>
</div>` :
emojiType === 'love' ?
`<div class="emoji__heart"></div>` :
emojiType === 'haha' ?
`<div class="emoji__face">
<div class="emoji__eyes"></div>
<div class="emoji__mouth">
<div class="emoji__tongue"></div>
</div>
</div>` :
emojiType === 'yay' ?
`<div class="emoji__face">
<div class="emoji__eyebrows"></div>
<div class="emoji__mouth"></div>
</div>` :
emojiType === 'wow' ?
`<div class="emoji__face">
<div class="emoji__eyebrows"></div>
<div class="emoji__eyes"></div>
<div class="emoji__mouth"></div>
</div>` :
emojiType === 'sad' ?
`<div class="emoji__face">
<div class="emoji__eyebrows"></div>
<div class="emoji__eyes"></div>
<div class="emoji__mouth"></div>
</div>` :
emojiType === 'angry' ?
`<div class="emoji__face">
<div class="emoji__eyebrows"></div>
<div class="emoji__eyes"></div>
<div class="emoji__mouth"></div>
</div>` : ''}
</div>
<div class="${type === 'sent' ? 'text-end' : ''} fw-300 text-muted mt-1 fs-xs">
${time}
</div>
`;
} else {
messageHtml = `
<div class="chat-message ${isJustEmoji ? 'emoji-only' : ''}">
<p>${message}</p>
</div>
<div class="${type === 'sent' ? 'text-end' : ''} fw-300 text-muted mt-1 fs-xs">
${time}
</div>
`;
}
messageDiv.innerHTML = messageHtml;
chatContainer.appendChild(messageDiv);
}
// Append image message
function appendImageMessage(imageSrc, type, time) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-segment chat-segment-${type}`;
messageDiv.innerHTML = `
<div class="chat-message">
<p><img src="${imageSrc}" class="img-fluid rounded" alt="Shared image" style="max-height: 200px;"></p>
</div>
<div class="${type === 'sent' ? 'text-end' : ''} fw-300 text-muted mt-1 fs-xs">
${time}
</div>
`;
chatContainer.appendChild(messageDiv);
}
// Append file message
function appendFileMessage(file, type, time) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-segment chat-segment-${type}`;
const fileIconClass = file.type === 'pdf' ? 'file-pdf text-danger' :
file.type === 'doc' ? 'file-word text-primary' :
file.type === 'xls' ? 'file-excel text-success' :
file.type === 'ppt' ? 'file-powerpoint text-warning' : 'file text-muted';
messageDiv.innerHTML = `
<div class="chat-message">
<div class="d-flex align-items-center p-2 rounded bg-white">
<i class="sa sa-${fileIconClass} fs-2x me-2"></i>
<div class="flex-grow-1">
<div class="text-truncate fw-500">${file.name}</div>
<small class="text-muted">${file.size}</small>
</div>
<a href="javascript:void(0);" class="btn btn-sm btn-icon">
<i class="sa sa-download"></i>
</a>
</div>
</div>
<div class="${type === 'sent' ? 'text-end' : ''} fw-300 text-muted mt-1 fs-xs">
${time}
</div>
`;
chatContainer.appendChild(messageDiv);
}
// Helper to get current time string
function getCurrentTime() {
const now = new Date();
return now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0');
}
// Scroll chat to bottom
function scrollToBottom() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// Event listeners
if (messageInput) {
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
}
if (sendButton) {
sendButton.addEventListener('click', () => {
sendMessage();
});
}
// Initialize with some random messages
function populateInitialChat() {
// Clear existing messages except for timestamps
const existingMessages = chatContainer.querySelectorAll('.chat-segment:not(.d-flex)');
existingMessages.forEach(msg => msg.remove());
// Add some initial messages
const messages = [
{ text: "Hi there! How's your day going?", type: 'get', delay: 0 },
{ text: "Pretty good, thanks for asking! Just finished a big project.", type: 'sent', delay: 300 },
{ text: "That's great to hear! Is this the same one you mentioned last week?", type: 'get', delay: 600 },
{ text: "Yes, finally wrapped it up. The client was really happy with the results.", type: 'sent', delay: 900 },
//{ file: mockFiles[0], type: 'sent', isFile: true, delay: 1200 },
{ text: "Thanks for sharing the document! I'll take a look at it.", type: 'get', delay: 1500 },
{ text: "Let me know if you need any clarification.", type: 'sent', delay: 1800 },
{ image: mockImages[2], type: 'get', isImage: true, delay: 2100 },
{ text: "That looks amazing! Is that from the project?", type: 'sent', delay: 2400 },
{ text: "Yes, it's the final design we went with 😊", type: 'get', delay: 2700 }
];
let cumulativeDelay = 0;
messages.forEach(msg => {
setTimeout(() => {
if (msg.isImage) {
appendImageMessage(msg.image, msg.type, getCurrentTime());
} else if (msg.isFile) {
appendFileMessage(msg.file, msg.type, getCurrentTime());
} else {
appendMessage(msg.text, msg.type, getCurrentTime());
}
scrollToBottom();
}, cumulativeDelay);
cumulativeDelay += msg.delay;
});
}
// Add CSS for the typing indicator
const style = document.createElement('style');
style.textContent = `
.typing {
display: flex;
align-items: center;
height: 17px;
}
.typing span {
background-color: #90949c;
width: 7px;
height: 7px;
border-radius: 50%;
margin: 0 2px;
display: block;
animation: typing 1.3s infinite ease-in-out;
}
.typing span:nth-child(1) {
animation-delay: 0s;
}
.typing span:nth-child(2) {
animation-delay: 0.2s;
}
.typing span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-5px);
}
}
.emoji-only {
font-size: 3rem;
}
/* Improved conversation list styling */
#js-slide-right .list-group-item {
transition: background-color 0.2s ease;
border-radius: 8px;
margin: 4px 8px;
border: none;
}
.unread-badge {
background-color: var(--primary-500);
color: white;
font-size: 0.7rem;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* Story circles */
.story-circle {
cursor: pointer;
}
.story-circle.active .profile-image {
border-color: var(--primary-500);
}
.story-circle .profile-image {
border: 2px solid var(--bs-body-bg);
}
.story-circle.has-story .profile-image {
border: 2px solid var(--primary-500);
}
/* Enhanced Emoji Styles */
.chat-segment .emoji {
transform: scale(2);
margin: 16px;
display: inline-block;
}
.chat-segment-sent .emoji {
margin-left: auto;
}
.chat-segment-get .emoji {
margin-right: auto;
}
`;
document.head.appendChild(style);
// Initialize the chat
populateInitialChat();
// Add send button if it doesn't exist
if (!sendButton) {
const inputGroup = messageInput.parentElement;
const newSendButton = document.createElement('button');
newSendButton.id = 'send_button';
newSendButton.className = 'btn btn-icon fs-xl width-1 flex-shrink-0';
newSendButton.setAttribute('type', 'button');
newSendButton.setAttribute('data-bs-toggle', 'tooltip');
newSendButton.setAttribute('data-bs-original-title', 'Send');
newSendButton.setAttribute('data-bs-placement', 'top');
newSendButton.innerHTML = '<svg class="sa-icon sa-bold sa-icon-subtlelight"><use href="icons/sprite.svg#send"></use></svg>';
// Append to input group instead of trying to insert before a specific element
inputGroup.appendChild(newSendButton);
// Add click handler
newSendButton.addEventListener('click', () => {
sendMessage();
});
}
function insertEmoji(element) {
// Get the emoji character from the data attribute
const emoji = element.getAttribute('data-emoji');
// Get the chat input
const chatInput = document.getElementById('msgr_input');
// Insert the emoji at cursor position or append to end
if (chatInput) {
const startPos = chatInput.selectionStart;
const endPos = chatInput.selectionEnd;
const text = chatInput.value;
const before = text.substring(0, startPos);
const after = text.substring(endPos, text.length);
chatInput.value = before + emoji + after;
// Move cursor position after the inserted emoji
chatInput.selectionStart = startPos + emoji.length;
chatInput.selectionEnd = startPos + emoji.length;
// Focus back on the input
chatInput.focus();
}
// Close the dropdown if it's open
const dropdown = bootstrap.Dropdown.getInstance(element.closest('.dropdown-menu').previousElementSibling);
if (dropdown) {
dropdown.hide();
}
}
});
@@ -0,0 +1,174 @@
/*
You can use this wrapper to initialize peity charts globally. It will automatically detect the chart
type based on the content of the element.
- You can also use the data-peity attribute to pass in options for the chart.
- The data-peity attribute can be a JSON string or a string that can be parsed into a JSON object.
- The wrapper will also handle the:
-- updating of the chart with new data.
-- negative values for the bar charts.
-- color transitions for the bar charts.
-- updating of the chart with new data.
*/
import { PeityAPI } from './../thirdparty/peity.es6.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Global default settings for charts
const defaults = {
// Default background color for donut and pie charts (replaces the gray)
donutBackground: 'var(--bs-border-color)', // Light purple background instead of gray
pieBackground: 'var(--bs-border-color)' // Same for pie charts
};
// Helper to create peity charts (similar to jQuery plugin style)
function createPeity(selector, type, customOptions = {}) {
document.querySelectorAll(selector).forEach(element => {
// Parse data-peity attribute for options
let options = {...customOptions};
const dataAttr = element.getAttribute('data-peity');
if (dataAttr) {
try {
options = {...options, ...JSON.parse(dataAttr)};
} catch (e) {
console.warn('Invalid data-peity format for', selector);
}
}
// Apply defaults for donut and pie charts background colors
if (type === 'donut' || type === 'pie') {
// If fill is not defined or is an empty array, set default colors
if (!options.fill || options.fill.length === 0) {
options.fill = type === 'donut' ?
['var(--primary-500)', defaults.donutBackground] :
['var(--primary-500)', defaults.pieBackground];
}
// If fill is defined but doesn't include a background color (for fractions like "1/4")
else if (Array.isArray(options.fill) && options.fill.length === 1) {
options.fill.push(type === 'donut' ? defaults.donutBackground : defaults.pieBackground);
}
// If data contains multiple values (e.g., "10,4,4,6"), don't add background
}
PeityAPI.create(element, type, options);
});
}
try {
// Simple chart initializations with default settings
createPeity('.peity-pie', 'pie');
createPeity('.peity-donut', 'donut');
createPeity('.peity-line', 'line');
createPeity('.peity-bar', 'bar');
// Updating chart with animation
const updatingChart = document.querySelector('.updating-chart');
if (updatingChart) {
const values = updatingChart.textContent.split(',').map(Number);
const chart = PeityAPI.create(updatingChart, 'line', {
width: 200,
height: 40,
stroke: 'var(--info-500)',
fill: 'var(--info-200)',
min: 0,
max: 10
});
setInterval(function() {
values.shift();
values.push(Math.floor(Math.random() * 10));
updatingChart.textContent = values.join(',');
chart.draw();
}, 500);
}
// Bar charts with negative values (red/green coloring)
document.querySelectorAll('.bar-negative').forEach(element => {
const values = element.textContent.split(',').map(Number);
PeityAPI.create(element, 'bar', {
height: 40,
width: 110,
fill: values.map(value => value > 0 ? 'var(--success-500)' : 'var(--danger-500)')
});
});
// Bar charts with color transitions
document.querySelectorAll('.bar-transition').forEach(element => {
const values = element.textContent.split(',').map(Number);
PeityAPI.create(element, 'bar', {
height: 40,
width: 110,
fill: values.map((_, i, all) => {
const g = parseInt((i / all.length) * 255);
return `rgb(255, ${g}, ${g})`;
})
});
});
// Process all remaining elements with data-peity attribute
document.querySelectorAll('[data-peity]').forEach(element => {
// Skip elements already handled by specific selectors
if (element.classList.contains('peity-pie') ||
element.classList.contains('peity-donut') ||
element.classList.contains('peity-line') ||
element.classList.contains('peity-bar') ||
element.classList.contains('updating-chart') ||
element.classList.contains('bar-negative') ||
element.classList.contains('bar-transition')) {
return;
}
// Auto-detect chart type based on content
const content = element.textContent.trim();
let type;
if (content.includes('/')) {
type = 'donut'; // Fraction data is best for donut
} else if (content.includes(',')) {
// If it has commas, it's likely a series
const hasNegative = content.split(',').some(val => parseFloat(val) < 0);
type = hasNegative ? 'bar' : 'line'; // Bars handle negative values better visually
} else {
type = 'pie'; // Default fallback
}
// Get options from data attribute
let options = {};
const dataAttr = element.getAttribute('data-peity');
if (dataAttr) {
try {
options = JSON.parse(dataAttr);
} catch (e) {
console.warn('Invalid data-peity format for element', element);
}
}
// Apply global background color for donut/pie charts
if ((type === 'donut' || type === 'pie') && content.includes('/')) {
// For fraction notation (e.g. "1/4"), make sure we have a background color
if (!options.fill || !Array.isArray(options.fill) || options.fill.length < 2) {
options.fill = options.fill || [];
// If we have one color already, keep it and add the background
if (options.fill.length === 1) {
options.fill.push(type === 'donut' ? defaults.donutBackground : defaults.pieBackground);
} else {
// No colors defined yet, set defaults
options.fill = [
type === 'donut' ? 'var(--primary-500)' : 'var(--success-500)',
type === 'donut' ? defaults.donutBackground : defaults.pieBackground
];
}
}
}
// Create chart
PeityAPI.create(element, type, options);
});
} catch (error) {
console.error('Error initializing Peity charts:', error);
}
});
@@ -0,0 +1,24 @@
function swapPositions() {
// Toggle the 'position-relative' class on the element with id 'js-position-change'
var positionChangeEl = document.getElementById("js-position-change");
if (positionChangeEl) {
positionChangeEl.classList.toggle("position-relative");
}
// Helper function to toggle text content between two given strings
function toggleText(element, text1, text2) {
if (!element) return;
// Check current text (trimmed of whitespace)
if (element.textContent.trim() === text1) {
element.textContent = text2;
}
else {
element.textContent = text1;
}
}
// Toggle the text for the element with id 'js-position-text'
var positionTextEl = document.getElementById("js-position-text");
toggleText(positionTextEl, ".position-static", ".position-relative");
// Toggle the text for the element with id 'js-position-btn'
var positionBtnEl = document.getElementById("js-position-btn");
toggleText(positionBtnEl, "Change to RELATIVE", "Change to STATIC");
}
@@ -0,0 +1,69 @@
// Show comment actions when input is focused
document.addEventListener('DOMContentLoaded', function () {
const commentInput = document.querySelector('.comment-input-wrapper input');
const commentActions = document.querySelector('.comment-actions');
if (commentInput && commentActions) {
commentInput.addEventListener('focus', function () {
commentActions.classList.remove('d-none');
});
document.addEventListener('click', function (e) {
if (!commentInput.contains(e.target) && !commentActions.contains(e.target)) {
if (commentInput.value.length === 0) {
commentActions.classList.add('d-none');
}
}
});
}
});
// Form validation for all modals
document.addEventListener('DOMContentLoaded', function () {
// Get all forms that need validation
const forms = document.querySelectorAll('.needs-validation');
// Function to handle form submission
function handleFormSubmit(event) {
const form = event.target.closest('.modal').querySelector('form');
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
else {
// If form is valid, you can add AJAX submission here
// For demo purposes, we'll just close the modal
const modal = bootstrap.Modal.getInstance(event.target.closest('.modal'));
modal.hide();
// Show success message (optional)
// This would be replaced with actual form submission in a real application
// alert('Form submitted successfully!');
}
form.classList.add('was-validated');
}
// Attach click event handlers to submit buttons
document.getElementById('submitNewsForm').addEventListener('click', handleFormSubmit);
document.getElementById('submitProjectForm').addEventListener('click', handleFormSubmit);
document.getElementById('submitProfileForm').addEventListener('click', handleFormSubmit);
// Optional: Add validation as user types for better UX
Array.from(forms).forEach(form => {
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.addEventListener('input', function () {
if (!this.checkValidity()) {
this.classList.add('is-invalid');
}
else {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
}
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
// Your inline JavaScript code here
// Refresh panel content
function updatePanelContent(panel) {
const panelContent = panel.querySelector('.panel-content > .js-refreshed-content');
const timestamp = new Date().toLocaleString(); // Get current date and time
panelContent.innerHTML = 'Refreshed content - ' + timestamp;
}
// generate random Graph
function getRandomGraphUrl() {
var chartConfig = {
type: 'bar',
data:
{
labels: ['A', 'B', 'C', 'D', 'E', 'F'],
datasets: [
{
label: 'Random Data',
data: [
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100)
],
backgroundColor: ['rgba(33, 150, 243, 0.5)', // #2196F3
'rgba(29, 201, 183, 0.5)', // #1dc9b7
'rgba(170, 134, 209, 0.5)', // #a86d1
'rgba(255, 194, 65, 0.5)', // #ffc241
'rgba(253, 57, 149, 0.5)', // #fd3995
'rgba(158, 158, 158, 0.5)' // grey for the 6th bar
],
borderColor: ['rgba(33, 150, 243, 1)', // #2196F3
'rgba(29, 201, 183, 1)', // #1dc9b7
'rgba(170, 134, 209, 1)', // #a86d1
'rgba(255, 194, 65, 1)', // #ffc241
'rgba(253, 57, 149, 1)', // #fd3995
'rgba(158, 158, 158, 1)' // grey for the 6th bar
],
borderWidth: 1
}]
},
options:
{
responsive: true,
maintainAspectRatio: false,
scales:
{
y:
{
beginAtZero: true
}
}
}
};
var encodedConfig = encodeURIComponent(JSON.stringify(chartConfig));
return 'https://quickchart.io/chart?c=' + encodedConfig;
}
function loadRandomGraph() {
var imgElement = document.getElementById('randomGraph');
imgElement.src = getRandomGraphUrl();
// add timestamp to the panel
const panelContent = document.querySelector('.js-last-refresh');
const timestamp = new Date().toLocaleString(); // Get current date and time
panelContent.innerHTML = 'Chart updated at <strong>' + timestamp + '</strong>';
}
loadRandomGraph();
+185
View File
@@ -0,0 +1,185 @@
document.addEventListener('DOMContentLoaded', function() {
// Image Preview Modal Functionality
const imageTab = document.querySelector('#tab-images');
if (imageTab) {
// Create modal container with proper Bootstrap modal structure
const modalContainer = document.createElement('div');
modalContainer.className = 'modal fade image-preview-modal';
modalContainer.id = 'imagePreviewModal';
modalContainer.tabIndex = '-1';
modalContainer.setAttribute('aria-hidden', 'true');
// Create modal content
modalContainer.innerHTML = `
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-body p-0 position-relative d-flex align-items-center justify-content-center">
<div class="d-flex flex-column flex-lg-row shadow rounded overflow-hidden border border-3 border-light">
<!-- Left Info Panel -->
<div class="order-2 order-lg-1 flex-shrink-0" style="width: 300px;">
<div class="d-flex flex-column h-100 p-0">
<div class="flex-grow-1 p-3">
<h5 class="image-title fw-bold mb-3"></h5>
<p class="image-description text-muted mb-4"></p>
<div class="mb-3">
<div class="text-muted mb-1 fs-sm">Date</div>
<div class="image-date"></div>
</div>
<div class="mb-3">
<div class="text-muted mb-1 fs-sm">Source</div>
<div class="image-source text-primary text-decoration-underline link-offset-1 link-underline link-underline-opacity-75"></div>
</div>
<div class="mb-3">
<div class="text-muted mb-1 fs-sm">Tags</div>
<div class="image-category badge bg-secondary"></div>
</div>
</div>
<div class="px-3 pt-2 pb-0">
<p class="text-muted fs-nano mb-2">
Images may be subject to copyright. Please check the source for more information.
</p>
<div class="d-flex gap-2 pb-3">
<button type="button" class="btn btn-default btn-sm flex-grow-1">
Save
</button>
<button type="button" class="btn btn-default btn-sm flex-grow-1">
Share
</button>
</div>
</div>
</div>
</div>
<!-- Right Image Container -->
<div class="position-relative bg-light order-1 order-lg-2" style="width: auto; height: auto; max-height: 90vh;">
<button type="button" class="btn btn-icon btn-danger border border-dark position-absolute top-0 end-0 m-2 z-1" data-bs-dismiss="modal" aria-label="Close">
<svg class="sa-icon sa-bold sa-icon-2x sa-icon-light">
<use href="icons/sprite.svg#x"></use>
</svg>
</button>
<div class="d-flex align-items-center justify-content-center h-100 p-0">
<button type="button" class="btn btn-icon align-items-center justify-content-center text-light btn-dark bg-dark bg-opacity-50 rounded-circle position-absolute top-50 start-0 translate-middle-y ms-4 d-none d-sm-flex fs-3 z-1" id="prevImage">
<i class="sa sa-chevron-left"></i>
</button>
<img src="" class="img-preview" style="max-height: 90vh; max-width: 100%; object-fit: contain;" alt="Preview">
<button type="button" class="btn btn-icon align-items-center justify-content-center text-light btn-dark bg-dark bg-opacity-50 rounded-circle position-absolute top-50 end-0 translate-middle-y me-4 d-none d-sm-flex fs-3 z-1" id="nextImage">
<i class="sa sa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modalContainer);
// Initialize Bootstrap modal
const modal = new bootstrap.Modal(modalContainer);
// Get all preview images
const previewImages = imageTab.querySelectorAll('a[href="#"]');
let currentImageIndex = 0;
// Add click event to each preview image
previewImages.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const imgElement = link.querySelector('img');
const imgSrc = imgElement.src;
// Add -big before the file extension
const bigImgSrc = imgSrc.replace(/(\.[^.]+)$/, '-big$1');
showPreview(bigImgSrc, link);
currentImageIndex = index;
updateNavigationButtons();
});
});
// Navigation buttons
const prevBtn = modalContainer.querySelector('#prevImage');
const nextBtn = modalContainer.querySelector('#nextImage');
prevBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (currentImageIndex > 0) {
currentImageIndex--;
const link = previewImages[currentImageIndex];
const imgElement = link.querySelector('img');
const imgSrc = imgElement.src;
const bigImgSrc = imgSrc.replace(/(\.[^.]+)$/, '-big$1');
updatePreview(bigImgSrc, link);
updateNavigationButtons();
}
});
nextBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (currentImageIndex < previewImages.length - 1) {
currentImageIndex++;
const link = previewImages[currentImageIndex];
const imgElement = link.querySelector('img');
const imgSrc = imgElement.src;
const bigImgSrc = imgSrc.replace(/(\.[^.]+)$/, '-big$1');
updatePreview(bigImgSrc, link);
updateNavigationButtons();
}
});
// Helper functions
function showPreview(imgSrc, link) {
updatePreview(imgSrc, link);
modal.show();
}
function updatePreview(imgSrc, link) {
const previewImg = modalContainer.querySelector('.img-preview');
previewImg.src = imgSrc;
// Update image information
modalContainer.querySelector('.image-title').textContent = link.dataset.imgTitle || '';
modalContainer.querySelector('.image-description').textContent = link.dataset.imgDescription || '';
modalContainer.querySelector('.image-category').textContent = link.dataset.imgCategory || '';
modalContainer.querySelector('.image-date').textContent = link.dataset.imgDate || '';
modalContainer.querySelector('.image-source').textContent = link.dataset.imgSource || '';
}
function hidePreview() {
modal.hide();
}
function updateNavigationButtons() {
prevBtn.classList.toggle('hidden', currentImageIndex === 0);
nextBtn.classList.toggle('hidden', currentImageIndex === previewImages.length - 1);
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!modalContainer.classList.contains('show')) return;
switch(e.key) {
case 'Escape':
hidePreview();
break;
case 'ArrowLeft':
if (currentImageIndex > 0) prevBtn.click();
break;
case 'ArrowRight':
if (currentImageIndex < previewImages.length - 1) nextBtn.click();
break;
}
});
// Clean up modal backdrop when modal is hidden
modalContainer.addEventListener('hidden.bs.modal', function () {
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
});
}
});
@@ -0,0 +1,8 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
// Wait for the DOM to load before initializing
document.addEventListener('DOMContentLoaded', () => {
// Initialize SmartTables with the table ID and options
const table = new SmartTables('myTable',
{});
});
@@ -0,0 +1,63 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
// Wait for the DOM to load before initializing
document.addEventListener('DOMContentLoaded', () => {
// Initialize SmartTables with the table ID and options
const table = new SmartTables('myTable', {
responsive: {
enabled: true,
breakpoint: 768,
columnPrikorities: {
0: 1, // TradeID - highest priority (never hide)
1: 2, // Symbol - second highest priority
2: 3, // Qty - third priority
3: 4, // BuyPrice - fourth priority
4: 5, // SellPrice - fifth priority
5: 6 // BuyDate - sixth priority
}
},
debug: true,
// Add hooks for customizing cell rendering
hooks: {
afterInit: function () {
// Get all table cells after initialization
const tbody = document.querySelector('#myTable tbody');
if (!tbody) return;
// Process each row
Array.from(tbody.querySelectorAll('tr')).forEach(row => {
// Apply formatting to specific columns
Array.from(row.cells).forEach((cell, index) => {
const text = cell.textContent.trim();
// Check for null values in any column
if (text === 'null') {
cell.textContent = 'null'; // Replace with em dash
cell.classList.add('text-muted', 'fst-italic');
return;
}
// Format Profit column (7)
if (index === 7) {
if (text.includes('-')) {
cell.classList.add('text-danger', 'fw-bold');
} else {
cell.classList.add('text-success', 'fw-bold');
}
}
// Format Net column (9)
if (index === 9) {
if (text.includes('-')) {
cell.classList.add('text-danger', 'fw-bold');
} else {
cell.classList.add('text-success', 'fw-bold');
}
}
});
});
}
}
});
});
@@ -0,0 +1,291 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
// Wait for the DOM to load before initializing
document.addEventListener('DOMContentLoaded', () => {
// DOM elements
const tableContainer = document.getElementById('tableContainer');
const thresholdSlider = document.getElementById('threshold');
const thresholdValue = document.getElementById('thresholdValue');
const minMatchLengthSlider = document.getElementById('minMatchLength');
const minMatchLengthValue = document.getElementById('minMatchLengthValue');
const multiWordThresholdSlider = document.getElementById('multiWordThreshold');
const multiWordThresholdValue = document.getElementById('multiWordThresholdValue');
const maxDistanceSlider = document.getElementById('maxDistance');
const maxDistanceValue = document.getElementById('maxDistanceValue');
const descriptionEl = document.getElementById('description');
const buildTableBtn = document.getElementById('buildTable');
const destroyTableBtn = document.getElementById('destroyTable');
const searchExample = document.getElementById('searchExample');
const resetBtn = document.getElementById('resetSettings');
// Table instance
let table = null;
// Initialize sliders with default values and ranges
thresholdSlider.min = 0;
thresholdSlider.max = 1;
thresholdSlider.step = 0.1;
thresholdSlider.value = 0;
minMatchLengthSlider.min = 1;
minMatchLengthSlider.max = 5;
minMatchLengthSlider.step = 1;
minMatchLengthSlider.value = 1;
multiWordThresholdSlider.min = 0;
multiWordThresholdSlider.max = 1;
multiWordThresholdSlider.step = 0.1;
multiWordThresholdSlider.value = 1;
maxDistanceSlider.min = 0;
maxDistanceSlider.max = 10;
maxDistanceSlider.step = 1;
maxDistanceSlider.value = 0;
// Function to toggle controls state
function toggleControlsState(disabled) {
// Disable/enable all sliders
thresholdSlider.disabled = disabled;
minMatchLengthSlider.disabled = disabled;
multiWordThresholdSlider.disabled = disabled;
maxDistanceSlider.disabled = disabled;
resetBtn.disabled = disabled;
}
// Update slider display values
function updateSliderValues() {
thresholdValue.textContent = thresholdSlider.value;
minMatchLengthValue.textContent = minMatchLengthSlider.value;
multiWordThresholdValue.textContent = multiWordThresholdSlider.value;
maxDistanceValue.textContent = maxDistanceSlider.value;
updateDescription();
}
// Provide an explanation based on current settings
function updateDescription() {
const threshold = parseFloat(thresholdSlider.value);
const minMatchLength = parseInt(minMatchLengthSlider.value);
const multiWordThreshold = parseFloat(multiWordThresholdSlider.value);
const maxDistance = parseInt(maxDistanceSlider.value);
let desc = "";
// Threshold description
if (threshold === 0) {
desc += "• <strong>Exact matching only</strong> - No partial matches<br>";
} else if (threshold < 0.3) {
desc += "• <strong>Minor fuzzy matching</strong> - Requires high similarity<br>";
} else if (threshold < 0.6) {
desc += "• <strong>Moderate fuzzy matching</strong> - Allows reasonable variations<br>";
} else if (threshold < 1) {
desc += "• <strong>High fuzzy matching</strong> - Will find loosely related terms<br>";
} else {
desc += "• <strong>Maximum fuzzy matching</strong> - Will track down distant relatives like an overenthusiastic family reunion<br>";
}
// Min Match Length description
if (minMatchLength === 1) {
desc += "• Will match even with a <strong>single character</strong><br>";
} else {
desc += "• Requires at least <strong>" + minMatchLength + " characters</strong> to match<br>";
}
// Multi Word Threshold description
if (multiWordThreshold === 1) {
desc += "• <strong>All words</strong> in a multi-word search must match<br>";
} else if (multiWordThreshold > 0.5) {
desc += "• <strong>Most words</strong> in a multi-word search must match<br>";
} else if (multiWordThreshold > 0) {
desc += "• <strong>Some words</strong> in a multi-word search must match<br>";
} else {
desc += "• <strong>Any word</strong> in a multi-word search can match<br>";
}
// Max Distance description
if (maxDistance === 0) {
desc += "• <strong>No typo tolerance</strong> - Characters must match exactly<br>";
} else if (maxDistance === 1) {
desc += "• <strong>Single typo tolerance</strong> - Allows 1 character mismatch<br>";
} else if (maxDistance < 5) {
desc += "• <strong>Moderate typo tolerance</strong> - Allows " + maxDistance + " character mismatches<br>";
} else {
desc += "• <strong>High typo tolerance</strong> - Allows many character mismatches<br>";
}
// Example
let example = "";
if (threshold === 0 && maxDistance === 0) {
example = "Try searching for exact terms like 'John' or 'Do'";
} else if (threshold > 0 || maxDistance > 0) {
if (maxDistance > 0) {
example = "Try searches with typos like 'Jhon' instead of 'John' or 'Subrey' instead of 'Surgery'";
} else {
example = "Try partial searches like 'Jo' for 'John' or 'Surg' for 'Surgery'";
}
}
searchExample.textContent = example;
descriptionEl.innerHTML = desc;
}
// Add event listeners to sliders
thresholdSlider.addEventListener('input', updateSliderValues);
minMatchLengthSlider.addEventListener('input', updateSliderValues);
multiWordThresholdSlider.addEventListener('input', updateSliderValues);
maxDistanceSlider.addEventListener('input', updateSliderValues);
// Reset settings to default values
resetBtn.addEventListener('click', () => {
thresholdSlider.value = 0.7;
minMatchLengthSlider.value = 2;
multiWordThresholdSlider.value = 0.5;
maxDistanceSlider.value = 2;
updateSliderValues();
});
// Build table with current settings
buildTableBtn.addEventListener('click', () => {
// If table already exists, destroy it first
if (table) {
table.destroy();
table = null;
}
// Get current slider values
const fuzzySettings = {
threshold: parseFloat(thresholdSlider.value),
minMatchLength: parseInt(minMatchLengthSlider.value),
multiWordThreshold: parseFloat(multiWordThresholdSlider.value),
maxDistance: parseInt(maxDistanceSlider.value)
};
// Log settings to confirm values
console.log('Building table with fuzzy search settings:', fuzzySettings);
// Show table container
tableContainer.classList.remove('d-none');
// Initialize SmartTables with the table ID and fuzzy settings
table = new SmartTables('myTable', {
data: {
type: "json",
source: "json/MOCK_DATA_HOSPITAL.json",
columns: [
{ data: "PatientID", title: "ID" },
{ data: "PatientName", title: "First Name" },
{ data: "Phone", title: "Phone" },
{ data: "DOB", title: "DOB" },
{ data: "Service", title: "Service" },
{ data: "ServiceDate", title: "Service Date" },
{
data: "Severity",
title: "Severity",
render: function(value) {
let severityClass = {
"Mild": "badge bg-success",
"Moderate": "badge bg-warning text-dark",
"Severe": "badge bg-danger"
}[value] || "badge bg-dark";
return '<span class="' + severityClass + '">' + value + '</span>';
}
},
{ data: "BillPaid", title: "Bill Paid" },
{ data: "BillDue", title: "Bill Due" },
{ data: "Department", title: "Department" },
{ data: "Staff", title: "Doctor" },
{ data: "Nurse", title: "Nurse" },
]
},
fuzzyMatch: fuzzySettings,
perPage: 6,
search: true,
sort: true,
pagination: true,
debug: false,
loading: {
enabled: true,
duration: 800
},
responsive: {
enabled: true,
breakpoint: 768,
columnPriorities: {
0: 1, // ID - highest priority (never hide)
1: 2, // First Name - second highest priority
2: 3, // Last Name - third priority
3: 4, // Gender - fourth priority
4: 5, // Phone - fifth priority
5: 6, // DOB - sixth priority
6: 7, // Service - seventh priority
7: 8, // Service Date - eighth priority
8: 9, // Severity - ninth priority
9: 10, // Bill Paid - tenth priority
10: 11, // Bill Due - eleventh priority
11: 12 // Department - twelfth priority
}
}
});
// Update button states
buildTableBtn.disabled = true;
destroyTableBtn.disabled = false;
// Disable all fuzzy settings controls
toggleControlsState(true);
// Log the actual fuzzy settings used by the table
console.log('Table created with actual fuzzy settings:', table.options.fuzzyMatch);
// Add additional verification for search functionality
// setTimeout(() => {
// console.log('Verifying table search functionality:');
// console.log('- Search method type:', typeof table.handleSearch);
// console.log('- Fuzzy match method type:', typeof table.fuzzyMatch);
// console.log('- Is server-side:', table.options.data.serverSide);
// // Log detailed fuzzy search settings
// console.log('DETAILED FUZZY SEARCH SETTINGS:');
// console.log('- Threshold:', table.options.fuzzyMatch.threshold, '(0-1, higher = more fuzzy)');
// console.log('- Min Match Length:', table.options.fuzzyMatch.minMatchLength, '(min characters to match)');
// console.log('- Multi-word Threshold:', table.options.fuzzyMatch.multiWordThreshold, '(0-1, lower = more lenient)');
// console.log('- Max Distance:', table.options.fuzzyMatch.maxDistance, '(max character mismatches allowed)');
// // Add a test search to demonstrate fuzzy matching
// const testSearchTerms = ["john", "jon", "medical", "surgery", "surgry"];
// console.log('FUZZY SEARCH TEST EXAMPLES:');
// testSearchTerms.forEach(term => {
// console.log(`Testing search term: "${term}"`);
// // Create a temp input to simulate search
// const tempInput = document.createElement('input');
// tempInput.value = term;
// // Call the search method directly
// const searchFunction = table.handleSearch.bind(table);
// searchFunction(term);
// console.log(`Results found: ${table.filteredRows.length} rows`);
// });
// }, 1000);
});
// Destroy table
destroyTableBtn.addEventListener('click', () => {
if (table) {
table.destroy();
table = null;
}
tableContainer.classList.add('d-none');
// Update button states
buildTableBtn.disabled = false;
destroyTableBtn.disabled = true;
// Re-enable all fuzzy settings controls
toggleControlsState(false);
});
// Initialize slider values and description
updateSliderValues();
// Disable destroy button initially since no table exists
destroyTableBtn.disabled = true;
});
@@ -0,0 +1,53 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
// Wait for the DOM to load before initializing
document.addEventListener('DOMContentLoaded', () => {
const importBtn = document.getElementById('importBtn');
const tableContainer = document.getElementById('tableContainer');
let table = null;
let errorContainer = document.getElementById('errorContainer');
// Initialize the table immediately but keep the container hidden
table = new SmartTables('myTable', {
import: true,
export: true,
responsive: {
enabled: true,
breakpoint: 768
}
});
// Handle import button click
importBtn.addEventListener('click', () => {
// Hide the import data container
const hideImportData = document.getElementById('hide-import-data');
hideImportData.classList.add('d-none');
// Show the table container
tableContainer.classList.remove('d-none');
try {
// First, create the import modal if it doesn't exist
if (typeof table.createImportModal === 'function') {
table.createImportModal();
// Now find the modal and show it using Bootstrap's Modal API
const importModal = document.getElementById('importModal');
if (importModal) {
// Create a new Bootstrap modal instance and show it
const bsModal = new bootstrap.Modal(importModal);
bsModal.show();
console.log('Import modal opened successfully');
} else {
throw new Error('Import modal element not found after creation');
}
} else {
throw new Error('createImportModal method not found on table instance');
}
} catch (error) {
console.error('Failed to open import modal:', error);
errorContainer.classList.remove('d-none');
}
});
});
@@ -0,0 +1,130 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
// Wait for the DOM to load before initializing
document.addEventListener('DOMContentLoaded', () => {
console.log('Initializing SmartTables with JSON data source');
function getEmailProviderIcon(email) {
const domain = email.toLowerCase().split('@')[1]?.split('.')[0]; // Extract domain (e.g., "hotmail" from "user@hotmail.com")
switch (domain) {
case 'hotmail':
case 'outlook':
return 'fab fa-microsoft'; // Microsoft icon for Hotmail/Outlook
case 'gmail':
return 'fab fa-google'; // Google icon for Gmail
case 'yahoo':
return 'fab fa-yahoo'; // Yahoo icon for Yahoo
default:
return 'fas fa-envelope'; // Default email icon for other providers
}
}
// Initialize SmartTables with the table ID and options
const table = new SmartTables('myTable', {
data: {
type: "json",
source: "json/smarttable-data.json",
columns: [
{ data: "ID", title: "ID" },
{ data: "FirstName", title: "First Name" },
{ data: "LastName", title: "Last Name" },
{ data: "Gender", title: "Gender" },
{ data: "Phone", title: "Phone" },
{
data: "Status",
title: "Payment",
render: function(value) {
let statusClass = {
"Success": "badge bg-success",
"Pending": "badge bg-warning text-dark",
"Rejected": "badge bg-danger",
"Cash": "badge bg-secondary"
}[value] || "badge bg-dark";
return '<span class="' + statusClass + '">' + value + '</span>';
}
},
{
data: "Due",
title: "Sales",
render: function(value) {
return '$' + parseFloat(value).toFixed(2);
}
},
{
data: "Product",
title: "Product",
render: function(value, row) {
if (!value) return ''; // Handle null/undefined values
// Check if the row has a ProductImage
if (row.ProductImage) {
return `<a href="#" class="product-name text-decoration-underline" data-bs-toggle="popover" data-bs-trigger="hover" data-bs-html="true" data-bs-content="<img src='${row.ProductImage}' alt='${value}' style='max-width: 200px; max-height: 200px;' />">${value}</a>`;
}
return value; // Return the product name as plain text if no image
}
},
{ data: "Department", title: "POS" },
{
data: "Email",
title: "Email",
render: function (value) {
if (!value) return ''; // Handle null/undefined values
const lowerCaseEmail = value.toLowerCase(); // Convert email to lowercase
const iconClass = getEmailProviderIcon(lowerCaseEmail); // Get the appropriate icon
return `<span class="bg-secondary bg-opacity-10 border border-success border-opacity-50 py-1 px-2 fs-sm rounded"><i class="${iconClass} me-2 text-success"></i>${lowerCaseEmail}</span>`;
}
},
{ data: "City", title: "Area" },
{ data: "Address", title: "Address" },
{ data: "Company", title: "Company" },
{ data: "CreatedDate", title: "Created Date" }
]
},
perPage: 15,
search: true,
sort: true,
pagination: true,
export: true,
print: true,
import: true,
debug: false,
responsive: {
enabled: true,
breakpoint: 768,
columnPriorities: {
0: 1, // ID - highest priority (never hide)
1: 2, // First Name - second highest priority
2: 3, // Last Name - third priority
3: 4, // Gender - fourth priority
4: 5, // Phone - fifth priority
5: 6 // Status - lowest priority (hide first)
}
},
hooks: {
beforeInit: function() {
console.log('SmartTables: Before initialization');
},
afterInit: function() {
console.log('SmartTables: After initialization complete');
},
afterDraw: function() {
console.log('SmartTables: Table drawn');
// Initialize popovers for products with images
const popoverElements = document.querySelectorAll('.product-name');
popoverElements.forEach(element => {
// Destroy any existing popover to avoid duplicates
if (element._popover) {
bootstrap.Popover.getInstance(element).dispose();
}
// Initialize new popover with explicit top placement and high z-index
new bootstrap.Popover(element, {
placement: 'top', // Forces the popover to appear on top
trigger: 'hover', // Ensures popover appears on hover
customClass: 'popover-body-p-0', // Custom class to control styling
});
});
}
}
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,144 @@
import { SmartTables } from '../optional/smartTables/smartTables.bundle.js';
document.addEventListener('DOMContentLoaded', () => {
// Define column definitions
let columnDefs = [
{ data: "id", title: "ID" },
{ data: "name", title: "Name" },
{ data: "age", title: "Age" },
{
data: "salary",
title: "Salary",
render: function(value) {
let salary = parseFloat(value);
if (isNaN(salary)) return "$0.00";
return "$" + salary.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
},
{
data: "bonus",
title: "Bonus",
render: function(value) {
let bonus = parseFloat(value) || 0;
let formattedBonus = "$" + bonus.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
// Check if bonus is less than $3,000
let shouldBeRed = bonus < 3000;
return shouldBeRed ?
`<span class="text-danger fw-bold">${formattedBonus}</span>` :
formattedBonus;
}
},
{
data: "performance",
title: "Performance",
render: function(value) {
let performance = Math.min(Math.max(parseInt(value), 1), 100);
let progressClass;
if (performance < 30) {
progressClass = "bg-danger";
} else if (performance > 70) {
progressClass = "bg-success-700";
} else if (performance >= 50 && performance <= 70) {
progressClass = "bg-success-500";
} else {
progressClass = "bg-warning text-dark";
}
return '<div class="progress" style="height: 20px;">' +
'<div class="progress-bar ' + progressClass + '" ' +
'role="progressbar" ' +
'data-width="' + performance + '" ' +
'aria-valuenow="' + performance + '" ' +
'aria-valuemin="0" ' +
'aria-valuemax="100" ' +
'style="width: ' + performance + '%;">' +
performance + '%' +
'</div>' +
'</div>';
}
},
{ data: "department", title: "Department" },
{ data: "hire_date", title: "Hired" },
{
data: "overtime_hours",
title: "Overtime Hrs",
render: function(value) {
let hours = parseFloat(value) || 0;
const maxHours = 50;
let widthPercent = Math.min((hours / maxHours) * 100, 100);
let barColor = hours > 40 ? "bg-danger" :
hours > 20 ? "bg-warning text-dark" :
"bg-success";
return `
<div class="progress" style="height: 20px; width: 100%;">
<div class="progress-bar ${barColor}"
role="progressbar"
style="width: ${widthPercent}%;"
aria-valuenow="${hours}"
aria-valuemin="0"
aria-valuemax="${maxHours}">
${hours} hrs
</div>
</div>
`;
}
},
{ data: "projects_completed", title: "Completed" },
{
data: "satisfaction_score",
title: "Score",
render: function(value) {
let score = parseFloat(value) || 0;
score = Math.min(Math.max(score, 0), 10);
let badgeClass, label;
if (score <= 2) {
badgeClass = "bg-danger-300";
label = "Poor";
} else if (score <= 4) {
badgeClass = "bg-warning-300";
label = "Fair";
} else if (score <= 6) {
badgeClass = "bg-success-400";
label = "Average";
} else if (score <= 8) {
badgeClass = "bg-success-500";
label = "Good";
} else {
badgeClass = "bg-success-700";
label = "Excellent";
}
return `<span class="badge ${badgeClass}">${label} (${score})</span>`;
}
},
{ data: "remote_work_days", title: "Remote Work Days" },
{ data: "training_hours", title: "Training Hrs" },
{ data: "email", title: "Email" },
];
// Initilize tables
const smartTable = new SmartTables('demo-table', {
data: {
type: 'ajax',
source: 'https://getwebora.com/smartadmin/json//mock-server.php?scenario=success',
serverSide: true,
method: 'GET',
columns: columnDefs
},
perPage: 10,
debug: true,
responsive: {
enabled: true,
breakpoint: 768,
},
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,235 @@
// IndexedDB setup
let db;
const DB_NAME = 'IconStackDB';
const STORE_NAME = 'savedIcons';
const DB_VERSION = 1;
// Toast management
let currentToast = null;
// Initialize IndexedDB
function initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
db = request.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
store.createIndex('name', 'name', { unique: true });
store.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
// Function to retrieve all saved icons
async function getAllSavedIcons() {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Function to show toast notifications
function showToast(message, type = 'primary') {
if (currentToast) {
currentToast.hide();
}
// Check if toast container exists, create if not
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(toastContainer);
}
// Create toast element
const toastId = 'toast-' + Date.now();
const toast = document.createElement('div');
toast.className = `toast hide align-items-center border-0 py-2 px-3 bg-${type} text-white`;
toast.id = toastId;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.style.setProperty('--bs-toast-max-width', 'auto');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body d-flex align-items-center justify-content-center">
${message}
</div>
<button type="button" class="btn btn-system ms-auto" data-bs-dismiss="toast" aria-label="Close">
<svg class="sa-icon sa-icon-light">
<use href="icons/sprite.svg#x"></use>
</svg>
</button>
</div>
`;
toastContainer.appendChild(toast);
// Initialize and show the toast
currentToast = new bootstrap.Toast(toast, {
autohide: true,
delay: 3000
});
// Remove toast after it's hidden
toast.addEventListener('hidden.bs.toast', function() {
currentToast = null;
toast.remove();
});
currentToast.show();
}
// Function to delete an icon from IndexedDB
async function deleteIcon(iconId) {
try {
if (confirm('Are you sure you want to delete this icon?')) {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
await new Promise((resolve, reject) => {
const request = store.delete(iconId);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
showToast('Icon deleted successfully', 'success');
await populateIconList(); // Refresh the list
}
} catch (error) {
console.error('Error deleting icon:', error);
showToast('Failed to delete icon', 'danger');
}
}
// Function to copy icon HTML to clipboard
function copyIconToClipboard(iconHTML) {
navigator.clipboard.writeText(iconHTML)
.then(() => showToast('Icon copied to clipboard!', 'success'))
.catch(() => showToast('Failed to copy icon', 'danger'));
}
// Function to populate the icon list
async function populateIconList() {
try {
const icons = await getAllSavedIcons();
const iconList = document.getElementById('iconList');
// Clear existing content
iconList.innerHTML = '';
// Sort icons by creation date (newest first)
icons.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
icons.forEach(icon => {
const li = document.createElement('li');
li.className = 'col-4 col-sm-3 col-md-3 col-lg-2 col-xl-2 col-xxl-1 mb-g';
li.innerHTML = `
<div class="d-flex flex-column align-items-center p-2 m-0 w-100 shadow-hover-2 border rounded position-relative show-child-on-hover overflow-hidden" style="font-size: 4rem;">
<div class="show-on-hover-parent bg-secondary bg-opacity-50 position-absolute top-0 start-0 w-100 h-100 z-1">
<div class="d-flex flex-row align-items-end justify-content-center h-100 gap-1 pb-2">
<button type="button" class="btn btn-xs btn-success copy-btn">
COPY
</button>
<button type="button" class="btn btn-xs btn-danger delete-btn">
DEL
</button>
</div>
</div>
<div class="pb-1 d-flex icon-container">
<div class="stack-icon">
${icon.html}
</div>
</div>
<div class="text-muted fs-nano icon-name">
${icon.name}
</div>
</div>
`;
iconList.appendChild(li);
// Add event listeners after the element is added to the DOM
const copyBtn = li.querySelector('.copy-btn');
const deleteBtn = li.querySelector('.delete-btn');
const iconContainer = li.querySelector('.icon-container');
// Format icon HTML for copying
const iconHTML = `<div class="stack-icon">${icon.html}</div>`;
// Add click handlers
copyBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent event bubbling
copyIconToClipboard(iconHTML);
});
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent event bubbling
deleteIcon(icon.id);
});
// Keep the container click for convenience
iconContainer.addEventListener('click', () => {
copyIconToClipboard(iconHTML);
});
});
// Show message if no icons found
if (icons.length === 0) {
iconList.innerHTML = `
<div class="col-12 text-center text-muted py-5">
<h4>No saved icons found</h4>
<p>Create and save some icons using the <a href="stackgenerator.html">Stack Generator</a> to see them here.</p>
</div>
`;
}
} catch (error) {
console.error('Error loading icons:', error);
showToast('Failed to load icons', 'danger');
}
}
// Function to filter icons based on search input
function filterIcons() {
const searchTerm = document.getElementById('searchIcons').value.toLowerCase();
const icons = document.querySelectorAll('#iconList li');
icons.forEach(icon => {
const name = icon.querySelector('.icon-name').textContent.toLowerCase();
const matches = name.includes(searchTerm);
icon.style.display = matches ? '' : 'none';
});
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', async function() {
try {
await initDB();
await populateIconList();
// Add search functionality
const searchInput = document.getElementById('searchIcons');
searchInput.addEventListener('input', filterIcons);
console.log('Stack Library initialized successfully');
} catch (error) {
console.error('Error initializing Stack Library:', error);
showToast('Failed to initialize Stack Library', 'danger');
}
});
@@ -0,0 +1,7 @@
// Import the streamline module
import { initStreamlines } from '../thirdparty/streamline.es6.js';
document.addEventListener('DOMContentLoaded', function() {
// Initialize all streamline charts
initStreamlines();
});
@@ -0,0 +1,727 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function () {
'use strict';
/***************************************************************/
/* Subscription Dashboard Chart #subscription-chart */
/***************************************************************/
if (document.getElementById('subscription-chart')) {
const categories = ['2025-01', '2025-02', '2025-03', '2025-04', '2025-05', '2025-06', '2025-07', '2025-08', '2025-09', '2025-10', '2025-11', '2025-12'];
const visitsData = [23686, 30820, 59622, 146465, 78160, 79520, 36148, 48721, 158303, 155174, 104830, 86895];
const subscriptionsData = [1545, 1350, 1270, 1830, 1955, 1865, 2034, 2544, 1956, 2211, 1540, 1670];
const subscriptionChartOptions = {
series: [
{
name: 'Visits',
type: 'area',
data: visitsData
},
{
name: 'Subscriptions',
type: 'line',
data: subscriptionsData
}
],
chart: {
height: 335,
type: 'line',
zoom: {
enabled: false
},
stacked: false,
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.bootstrapVars.bodyColor.rgba(0.1), // Visits (gray, area)
window.colorMap.primary[400].hex // Subscriptions (teal, line)
],
stroke: {
width: [1, 2],
curve: 'smooth',
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.8), window.colorMap.primary[400].hex],
dashArray: [4, 0], // Visits dashed, Subscriptions solid
},
fill: {
type: ['solid', 'solid'],
opacity: [0.15, 1],
},
markers: {
size: [3, 3],
colors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.7), window.colorMap.primary[600].hex],
strokeColors: [window.colorMap.bootstrapVars.bodyColor.rgba(0.7), window.colorMap.primary[600].hex],
strokeWidth: 2,
hover: {
sizeOffset: 2
}
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: [
{
seriesName: 'Visits',
min: 20000,
max: 170000,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val.toLocaleString();
}
}
},
{
seriesName: 'Subscriptions',
opposite: true,
min: 1200,
max: 2700,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val.toLocaleString();
}
}
}
],
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
yaxis: {
lines: {
show: true
}
},
xaxis: {
lines: {
show: false
}
}
},
tooltip: {
shared: true,
intersect: false,
theme: 'dark',
y: [
{
formatter: function (val) {
return val.toLocaleString();
}
},
{
formatter: function (val) {
return val.toLocaleString();
}
}
]
}
};
const subscriptionChart = new ApexCharts(
document.getElementById('subscription-chart'),
subscriptionChartOptions
);
subscriptionChart.render();
}
/***************************************************************/
/* User Activity Chart #user-activity-chart */
/***************************************************************/
if (document.getElementById('user-activity-chart')) {
const categories = ['Blogging', 'Videos', 'Ads', 'Comments', 'Shares', 'Likes', 'Funny'];
const userActivityChartOptions = {
series: [
{
name: 'Morning',
data: [65, 59, 90, 81, 56, 55, 40]
},
{
name: 'Night',
data: [28, 48, 40, 19, 96, 27, 100]
}
],
chart: {
height: 350,
width: '100%',
type: 'radar',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0,
sparkline: {
enabled: false
},
margin: 0,
padding: {
top: -10,
right: -10,
bottom: -10,
left: -10
}
},
colors: [
window.colorMap.success[400].hex, // Morning (light purple)
window.colorMap.primary[400].hex // Night (teal)
],
stroke: {
width: 0,
colors: [window.colorMap.success[400].hex, window.colorMap.primary[400].hex]
},
fill: {
opacity: 0.2
},
markers: {
size: 5,
colors: [window.colorMap.success[400].hex, window.colorMap.primary[400].hex],
strokeColors: '#fff',
strokeWidth: 2,
hover: {
sizeOffset: 2
}
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '10px'
}
}
},
yaxis: {
max: 100,
tickAmount: 5,
show: true,
labels: {
show: true,
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '10px'
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (val) {
return val;
}
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3
},
plotOptions: {
radar: {
size: undefined,
offsetX: 0,
offsetY: 0,
padding: 0,
polygons: {
strokeColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
connectorColors: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
fill: {
colors: undefined
}
}
}
}
};
const userActivityChart = new ApexCharts(
document.getElementById('user-activity-chart'),
userActivityChartOptions
);
userActivityChart.render();
}
/***************************************************************/
/* Data Stream Chart #data-stream-chart */
/***************************************************************/
if (document.getElementById('data-stream-chart')) {
const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
const positiveData = [0, 25, 70, 85, 25, 0, 60];
const negativeData = [-45, -50, -30, -25, -60, -120, 0];
const dataStreamChartOptions = {
series: [
{
name: 'Positive',
data: positiveData
},
{
name: 'Negative',
data: negativeData
}
],
chart: {
type: 'bar',
height: 350,
maxHeight: '100%',
stacked: true,
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.success[400].hex, // Positive (teal)
window.colorMap.primary[500].hex // Negative (blue)
],
plotOptions: {
bar: {
columnWidth: '50%',
borderRadius: 1
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: categories,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
}
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
min: -150,
max: 150,
tickAmount: 6,
labels: {
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex,
fontSize: '12px'
},
formatter: function (val) {
return val;
}
}
},
legend: {
show: true,
position: 'top',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 18,
height: 6,
radius: 2
},
itemMargin: {
horizontal: 12,
vertical: 0
}
},
grid: {
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.1),
strokeDashArray: 3,
yaxis: {
lines: {
show: true
}
},
xaxis: {
lines: {
show: true
}
}
},
tooltip: {
shared: true,
intersect: false,
theme: 'dark',
y: {
formatter: function (val) {
return val;
}
}
}
};
const dataStreamChart = new ApexCharts(
document.getElementById('data-stream-chart'),
dataStreamChartOptions
);
dataStreamChart.render();
}
/***************************************************************/
/* Demographic Marketing Chart #demographic-marketing-chart */
/***************************************************************/
if (document.getElementById('demographic-marketing-chart')) {
const countries = ['USA', 'Germany', 'Australia', 'Canada', 'France'];
const data = [25, 30, 15, 10, 20]; // Percentages
const demographicMarketingChartOptions = {
series: data,
chart: {
type: 'pie',
height: 350,
maxHeight: '100%',
toolbar: {
show: false
},
fontFamily: 'inherit',
parentHeightOffset: 0
},
colors: [
window.colorMap.primary[100].hex, // USA (light purple)
window.colorMap.primary[400].hex, // Germany (darker purple)
window.colorMap.success[100].hex, // Australia (light blue)
window.colorMap.success[300].hex, // Canada (light teal)
window.colorMap.success[500].hex // France (teal)
],
labels: countries,
dataLabels: {
enabled: false
},
plotOptions: {
pie: {
donut: {
size: '0%'
}
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
fontSize: '14px',
fontFamily: 'inherit',
labels: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
},
markers: {
width: 12,
height: 12,
radius: 2
},
itemMargin: {
horizontal: 10,
vertical: 2
}
},
tooltip: {
theme: 'dark',
y: {
formatter: function (val) {
return val + '%';
}
},
style: {
fontSize: '14px',
fontFamily: 'inherit'
},
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const country = w.globals.labels[seriesIndex];
const value = series[seriesIndex];
return '<div class="apexcharts-tooltip-box" style="padding: 2px 7px; background-color: var(--bs-body-bg);">' +
'<span style="color: var(--bs-body-color); font-size: 0.825rem !important;">' + country + ': ' + value + '%</span>' +
'</div>';
}
},
stroke: {
width: 1,
colors: ['var(--bs-body-bg)']
}
};
const demographicMarketingChart = new ApexCharts(
document.getElementById('demographic-marketing-chart'),
demographicMarketingChartOptions
);
demographicMarketingChart.render();
}
/***************************************************************/
/* Campaign Modal Functionality */
/***************************************************************/
const discountSlider = document.getElementById('discountAmount');
const discountValue = document.getElementById('discountAmountValue');
if (discountSlider && discountValue) {
discountSlider.addEventListener('input', function () {
discountValue.textContent = this.value;
});
}
// Toggle discount amount field visibility based on offer type
const offerTypeRadios = document.querySelectorAll('input[name="offerType"]');
const discountAmountContainer = document.getElementById('discountAmount')?.closest('.mb-3');
if (offerTypeRadios.length && discountAmountContainer) {
offerTypeRadios.forEach(radio => {
radio.addEventListener('change', function () {
// Only show discount slider for discount type offers
discountAmountContainer.style.display = (this.value === 'discount') ? 'block' : 'none';
});
});
}
// Show/hide custom audience options when "Custom Segment" is selected
const targetAudienceSelect = document.getElementById('targetAudience');
if (targetAudienceSelect) {
targetAudienceSelect.addEventListener('change', function () {
if (this.value === 'custom') {
// Check if custom audience options already exist
if (!document.getElementById('customAudienceOptions')) {
const customOptions = document.createElement('div');
customOptions.id = 'customAudienceOptions';
customOptions.className = 'mt-3 p-3 border border-light rounded';
customOptions.innerHTML = `
<p class="fw-bold mb-2">Define Custom Audience</p>
<div class="mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentRecent">
<label class="form-check-label" for="segmentRecent">
Recently active (last 30 days)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentHighValue">
<label class="form-check-label" for="segmentHighValue">
High-value subscribers
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="segmentLocation">
<label class="form-check-label" for="segmentLocation">
Specific locations
</label>
</div>
</div>
`;
targetAudienceSelect.parentNode.appendChild(customOptions);
} else {
document.getElementById('customAudienceOptions').style.display = 'block';
}
} else if (document.getElementById('customAudienceOptions')) {
document.getElementById('customAudienceOptions').style.display = 'none';
}
});
}
// Update form fields based on campaign type
const campaignTypeSelect = document.getElementById('campaignType');
if (campaignTypeSelect) {
campaignTypeSelect.addEventListener('change', function () {
// Get offer type radio buttons
const offerRadios = document.querySelectorAll('input[name="offerType"]');
switch (this.value) {
case 'acquisition':
// For acquisition, preselect free trial
offerRadios.forEach(radio => {
if (radio.value === 'freeTrial') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerFreeTrial').dispatchEvent(new Event('change'));
break;
case 'retention':
// For retention, preselect discount
offerRadios.forEach(radio => {
if (radio.value === 'discount') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerDiscount').dispatchEvent(new Event('change'));
// Set a higher default discount for retention
if (discountSlider) {
discountSlider.value = 25;
discountValue.textContent = '25';
}
break;
case 'upgrade':
// For upgrade, preselect upgrade promotion
offerRadios.forEach(radio => {
if (radio.value === 'upgrade') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerUpgrade').dispatchEvent(new Event('change'));
break;
case 'winback':
// For winback, preselect bundle offer
offerRadios.forEach(radio => {
if (radio.value === 'bundle') radio.checked = true;
else radio.checked = false;
});
// Trigger change to update visibility
document.getElementById('offerBundle').dispatchEvent(new Event('change'));
break;
}
});
}
// Handle form submission - Save Draft
const saveDraftBtn = document.getElementById('saveDraftBtn');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', function () {
const campaignName = document.getElementById('campaignName').value;
if (!campaignName) {
showAlert('danger', 'Please enter a campaign name');
return;
}
showAlert('success', 'Campaign draft saved successfully');
});
}
// Handle form submission - Launch Campaign
const launchCampaignBtn = document.getElementById('launchCampaignBtn');
if (launchCampaignBtn) {
launchCampaignBtn.addEventListener('click', function () {
const campaignName = document.getElementById('campaignName').value;
const campaignType = document.getElementById('campaignType').value;
const startDate = document.getElementById('startDate').value;
const targetAudience = document.getElementById('targetAudience').value;
// Basic validation
if (!campaignName || !campaignType || !startDate || !targetAudience) {
showAlert('danger', 'Please fill in all required fields');
return;
}
// Here you would normally send data to server
// For demo, we'll just show success and close modal
showAlert('success', 'Campaign launched successfully!');
// Close the modal after brief delay
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('buildCampaignModal'));
if (modal) modal.hide();
}, 1500);
});
}
// Update discount value display
document.getElementById('discountAmount').addEventListener('input', function () {
document.getElementById('discountAmountValue').textContent = this.value + '%';
});
// Toggle discount section based on offer type selection
document.querySelectorAll('input[name="offerType"]').forEach(radio => {
radio.addEventListener('change', function () {
document.getElementById('discountSection').style.display =
this.value === 'discount' ? 'block' : 'none';
});
});
// Show alert function
function showAlert(type, message) {
// Check if alert container exists, if not create it
let alertContainer = document.querySelector('.campaign-alert-container');
if (!alertContainer) {
alertContainer = document.createElement('div');
alertContainer.className = 'campaign-alert-container position-fixed top-0 end-0 p-3';
alertContainer.style.zIndex = '5000';
document.body.appendChild(alertContainer);
}
// Create alert element
const alertElement = document.createElement('div');
alertElement.className = `alert alert-${type} alert-dismissible fade show`;
alertElement.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add alert to container
alertContainer.appendChild(alertElement);
// Auto dismiss after 5 seconds
setTimeout(() => {
alertElement.classList.remove('show');
setTimeout(() => {
alertElement.remove();
}, 150);
}, 5000);
}
});
@@ -0,0 +1,420 @@
// Cache DOM elements
let emailList;
let selectAllCheckbox;
let refreshButton;
let deleteButton;
let spamButton;
let composeModal;
let sendButton;
let saveDraftButton;
let showMoreButton;
let attachmentsContainer;
// Initialize the application
document.addEventListener('DOMContentLoaded', function () {
initializeElements();
loadEmails();
setupEventListeners();
setupAttachments();
});
// Initialize DOM elements
function initializeElements() {
emailList = document.getElementById('js-emails');
selectAllCheckbox = document.getElementById('js-msg-select-all');
refreshButton = document.querySelector('.js-refresh').closest('button');
deleteButton = document.querySelector('.js-delete').closest('button');
spamButton = document.querySelector('.js-spam').closest('button');
composeModal = document.getElementById('default-example-modal-lg-center');
sendButton = composeModal?.querySelector('.btn-primary');
saveDraftButton = composeModal?.querySelector('.btn-secondary');
showMoreButton = document.querySelector('a.fs-xs.text-secondary');
attachmentsContainer = document.getElementById('message-attachments');
}
// Setup event listeners
function setupEventListeners() {
// Star toggling
emailList.addEventListener('click', handleEmailInteractions);
// Checkbox selection
selectAllCheckbox?.addEventListener('change', handleSelectAll);
// Refresh button
refreshButton?.addEventListener('click', handleRefresh);
// Delete functionality
deleteButton?.addEventListener('click', handleDelete);
// Spam functionality
spamButton?.addEventListener('click', handleSpam);
// Send email
sendButton?.addEventListener('click', handleSendEmail);
// Save draft
saveDraftButton?.addEventListener('click', handleSaveDraft);
// Show more attachments
showMoreButton?.addEventListener('click', handleShowMore);
}
// Handle all email interactions (star, click to read)
function handleEmailInteractions(e) {
// Handle star clicking
const starIcon = e.target.closest('.mail-starred');
if (starIcon) {
const listItem = starIcon.closest('li');
const emailId = listItem.querySelector('.form-check-input').id.replace('msg-', '');
// Toggle starred class
listItem.classList.toggle('starred');
// Update in localStorage
const emails = JSON.parse(localStorage.getItem('emails') || '[]');
const email = emails.find(email => email.id === emailId);
if (email) {
email.starred = !email.starred;
localStorage.setItem('emails', JSON.stringify(emails));
}
// Prevent further handling
e.stopPropagation();
return;
}
// Handle clicking on email content to read
const mailSender = e.target.closest('.js-email-content');
if (mailSender) {
// Navigate to read page
window.location.href = 'systemmail-read.html';
}
}
// Handle select all checkbox
function handleSelectAll(e) {
const checkboxes = emailList.querySelectorAll('.form-check-input');
checkboxes.forEach(checkbox => {
checkbox.checked = e.target.checked;
});
}
// Handle refresh button
function handleRefresh() {
emailList.classList.add('refreshing');
// Show loading spinner
refreshButton.querySelector('i').classList.add('fa-spin');
// Simulate refresh
setTimeout(() => {
loadEmails(); // Reload emails
emailList.classList.remove('refreshing');
refreshButton.querySelector('i').classList.remove('fa-spin');
}, 1000);
}
// Handle delete functionality
function handleDelete() {
const selectedEmails = emailList.querySelectorAll('.form-check-input:checked');
if (selectedEmails.length === 0) return;
// Count for toast message
const count = selectedEmails.length;
// Process each selected email
selectedEmails.forEach(checkbox => {
const listItem = checkbox.closest('li');
// Add deleting class for animation
listItem.style.overflow = 'hidden';
listItem.classList.add('deleting');
// Remove after animation completes
setTimeout(() => {
listItem.remove();
}, 300);
});
// Show toast message
showToast(`${count} message${count > 1 ? 's' : ''} deleted`);
}
// Handle spam functionality
function handleSpam() {
const selectedEmails = emailList.querySelectorAll('.form-check-input:checked');
if (selectedEmails.length === 0) return;
// Count for toast message
const count = selectedEmails.length;
// Process each selected email
selectedEmails.forEach(checkbox => {
const listItem = checkbox.closest('li');
// Add deleting class for animation
listItem.style.overflow = 'hidden';
listItem.classList.add('deleting');
// Remove after animation completes
setTimeout(() => {
listItem.remove();
}, 300);
});
// Show toast message
showToast(`${count} message${count > 1 ? 's' : ''} moved to spam`);
}
// Handle send email
function handleSendEmail(e) {
e.preventDefault();
// Get form data
const recipients = document.getElementById('message-to').value;
const subject = document.querySelector('input[placeholder="Subject"]').value;
const content = document.getElementById('fake_textarea').innerHTML;
// Validate
if (!recipients || !subject || !content) {
alert('Please fill in all required fields');
return;
}
// Close modal
const modal = bootstrap.Modal.getInstance(composeModal);
modal.hide();
// Show success toast
showToast('Email sent successfully!');
// Reset form completely
resetComposeForm(true);
}
// Handle save draft
function handleSaveDraft(e) {
e.preventDefault();
// Close modal
const modal = bootstrap.Modal.getInstance(composeModal);
modal.hide();
// Show success toast with check mark
showToast('<i class="fas fa-check me-2"></i> Draft saved', 'success');
}
// Show toast message
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `mail-toast bg-${type}-500`;
toast.innerHTML = message;
document.body.appendChild(toast);
// Show toast
setTimeout(() => toast.classList.add('show'), 100);
// Remove toast
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Reset compose form
function resetComposeForm(clearAll = false) {
document.getElementById('message-to').value = '';
document.getElementById('message-to-cc').value = '';
document.querySelector('input[placeholder="Subject"]').value = '';
// Reset attachments
if (attachmentsContainer) {
if (clearAll) {
// Clear all attachments
attachmentsContainer.innerHTML = '';
// Add back the "show more" link
const showMoreLink = document.createElement('a');
showMoreLink.href = '#';
showMoreLink.className = 'fs-xs text-secondary';
showMoreLink.textContent = 'show 3 more';
showMoreLink.addEventListener('click', handleShowMore);
attachmentsContainer.appendChild(showMoreLink);
// Hide the container if it should be empty
attachmentsContainer.classList.add('if-empty-display-none');
} else {
// Keep only the first two default attachments
const attachments = attachmentsContainer.querySelectorAll('.alert');
attachments.forEach((attachment, index) => {
if (index > 1) {
attachment.remove();
} else {
attachment.classList.remove('hidden-attachment');
}
});
// Reset show more link
if (showMoreButton) {
showMoreButton.style.display = 'none';
}
}
}
// Reset textarea with signature
document.getElementById('fake_textarea').innerHTML = `
<p><br></p>
<p><br></p>
<p>Best regards,</p>
<div class="d-flex d-column align-items-start mb-3 gap-2">
<img src="img/demo/avatars/avatar-admin.png" alt="SmartAdmin WebApp" class="me-3 mt-1 rounded-circle width-2">
<div class="border-left pl-3">
<span class="fw-500 fs-lg d-block l-h-n">Sunny A.</span>
<span class="fw-400 fs-nano d-block l-h-n mb-1">Software Engineer</span>
</div>
</div>
<div class="text-muted fs-nano">
PRIVATE AND CONFIDENTIAL. This e-mail, its contents and attachments are private and confidential and is intended for the recipient only. Any disclosure, copying or unauthorized use of such information is prohibited. If you receive this message in error, please notify us immediately and delete the original and any copies and attachments.
</div>
`;
}
// Handle show more attachments
function handleShowMore(e) {
e.preventDefault();
const hiddenAttachments = document.querySelectorAll('.hidden-attachment');
// If there are hidden attachments, show them
if (hiddenAttachments.length > 0) {
hiddenAttachments.forEach(attachment => {
attachment.classList.remove('hidden-attachment');
});
e.target.textContent = 'hide attachments';
}
// Otherwise, hide all but the first two attachments
else {
const allAttachments = attachmentsContainer.querySelectorAll('.alert');
allAttachments.forEach((attachment, index) => {
if (index > 1) {
attachment.classList.add('hidden-attachment');
}
});
// Update show more text
updateShowMoreText();
}
}
// Setup attachments in the compose modal
function setupAttachments() {
if (!attachmentsContainer) return;
// Create additional hidden attachments
const hiddenAttachments = [
{ name: 'report.docx', type: 'primary' },
{ name: 'presentation.pptx', type: 'primary' },
{ name: 'data.xlsx', type: 'primary' }
];
// Add hidden attachments
hiddenAttachments.forEach((attachment, index) => {
const attachmentEl = document.createElement('div');
attachmentEl.className = `alert m-0 p-0 badge bg-${attachment.type}-50 border-${attachment.type} ps-2 ${index > 0 ? 'hidden-attachment' : ''}`;
attachmentEl.innerHTML = `${attachment.name} <button data-bs-dismiss="alert" class="btn btn-icon btn-xs ms-1 rounded-0 border border-${attachment.type} border-top-0 border-bottom-0 border-end-0" type="button">
<i class="fas fa-times"></i>
</button>`;
// Insert before the "show more" link
attachmentsContainer.insertBefore(attachmentEl, showMoreButton);
});
// Update show more link text
updateShowMoreText();
}
// Update "show more" text based on hidden attachments
function updateShowMoreText() {
if (!showMoreButton) return;
const hiddenAttachments = document.querySelectorAll('.hidden-attachment');
if (hiddenAttachments.length > 0) {
showMoreButton.textContent = `show ${hiddenAttachments.length} more`;
showMoreButton.style.display = '';
} else {
showMoreButton.style.display = 'none';
}
}
// Load emails from JSON
function loadEmails() {
fetch('./json/MOCK_MAIL.json')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
return response.json();
})
.then(data => {
if (!emailList) {
console.error('Email list container not found!');
return;
}
// Store in localStorage for star functionality
localStorage.setItem('emails', JSON.stringify(data));
// Clear existing content
emailList.innerHTML = '';
// Iterate over each email
data.forEach((email, index) => {
const li = document.createElement('li');
li.className = `${email.read ? '' : 'unread'} ${email.starred ? 'starred' : ''}`.trim();
li.style.cursor = 'pointer';
const checkboxId = `msg-${email.id}`;
const time = new Date(email.timestamp).toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
hour12: true
});
li.innerHTML = `
<div class="d-flex align-items-center px-3 px-sm-4 py-2 py-lg-0 height-4 height-mobile-auto gap-2">
<div class="form-check form-check-hitbox me-2 order-1 mb-0">
<input type="checkbox" class="form-check-input" id="${checkboxId}">
<label class="form-check-label" for="${checkboxId}"></label>
</div>
<div class="d-flex align-self-end align-self-lg-center order-3 order-lg-2 me-lg-3 me-0 mb-1 mb-lg-0 flex-shrink-0">
<svg class="mail-starred sa-icon">
<use href="icons/sprite.svg#star"></use>
</svg>
</div>
<div class="js-email-content d-flex flex-column flex-lg-row flex-grow-1 align-items-stretch order-2 order-lg-3" style="min-width: 0;">
<div class="mail-sender flex-shrink-0 align-self-start align-self-lg-center width-sm width-max-sm text-truncate">${email.sender}</div>
<div class="d-flex flex-column flex-lg-row flex-grow-1 w-100 overflow-hidden">
<div class="mail-subject flex-shrink-0 align-self-start align-self-lg-center me-2 text-truncate width-max-100">${email.subject}</div>
<div class="d-flex align-items-center flex-grow-1 w-100 overflow-hidden">
<div class="mail-body d-block text-truncate w-100 pe-lg-5 text-muted">
<span class="hidden-sm">-</span> ${email.bodyPreview}
</div>
</div>
</div>
</div>
<div class="fs-sm text-muted ms-auto hide-on-hover-parent order-4 position-on-mobile-absolute pos-top pos-right pt-1 pt-lg-0 mt-2 me-3 me-sm-4 mt-lg-0 me-lg-0 flex-shrink-0">${time}</div>
</div>
`;
// Prevent checkbox from triggering navigation
const checkbox = li.querySelector('.form-check-input');
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
});
emailList.appendChild(li);
});
})
.catch(error => console.error('Error loading emails:', error));
}
@@ -0,0 +1,268 @@
/**
* Table Style Generator
* Handles real-time style updates for Bootstrap tables
*/
document.addEventListener('DOMContentLoaded', function() {
// Get elements
const tableStyleForm = document.getElementById('tableStyleForm');
const stylePreviewTable = document.getElementById('stylePreviewTable');
const generatedClasses = document.getElementById('generatedClasses');
const codeClasses = document.getElementById('codeClasses');
const copyStyleBtn = document.getElementById('copyStyleBtn');
const resetStylesBtn = document.getElementById('resetStylesBtn');
const showRowClasses = document.getElementById('showRowClasses');
const collapseColumnsBtn = document.getElementById('collapseColumnsBtn');
const expandColumnsBtn = document.getElementById('expandColumnsBtn');
const tableContainer = document.getElementById('tableContainer');
const headerAccent = document.getElementById('headerAccent');
const tableHeader = stylePreviewTable.querySelector('thead tr');
const styleBordered = document.getElementById('styleBordered');
const styleBorderless = document.getElementById('styleBorderless');
// Handle conflicts between bordered and borderless
styleBordered.addEventListener('change', function() {
if (this.checked && styleBorderless.checked) {
styleBorderless.checked = false;
}
updateTableStyle();
});
styleBorderless.addEventListener('change', function() {
if (this.checked && styleBordered.checked) {
styleBordered.checked = false;
}
updateTableStyle();
});
// Table rows with contextual classes
const tableRows = stylePreviewTable.querySelectorAll('tbody tr');
const contextualClasses = ['table-primary', 'table-secondary', 'table-success', 'table-danger', 'table-warning', 'table-info'];
const rowClasses = Array.from(tableRows).map((row, index) => {
// Assign a specific contextual class based on index
return contextualClasses[index % contextualClasses.length];
});
// Function to update the table style based on form values
function updateTableStyle() {
// Start with base class
const classes = ['table'];
let tableWrapperClass = '';
// Get theme (table-dark, table-light)
const theme = document.querySelector('input[name="tableTheme"]:checked').value;
if (theme) {
classes.push(theme);
}
// Get styles (striped, hover, bordered, borderless)
const styleCheckboxes = document.querySelectorAll('input[name="tableStyle"]:checked');
styleCheckboxes.forEach(checkbox => {
if (checkbox.value === 'table-responsive') {
tableWrapperClass = 'table-responsive';
} else {
classes.push(checkbox.value);
}
});
// Get size (sm, nano)
const size = document.querySelector('input[name="tableSize"]:checked').value;
if (size) {
classes.push(size);
}
// Get accent color for the whole table
const accent = document.getElementById('tableAccent').value;
if (accent) {
classes.push(accent);
}
// Apply header accent using classes only, not inline styles
const headerAccentValue = headerAccent.value;
// First remove any existing background classes
tableHeader.classList.remove(
'bg-primary', 'bg-secondary', 'bg-success',
'bg-danger', 'bg-warning', 'bg-info',
'bg-dark', 'bg-light', 'text-white'
);
// Add the new one if selected
if (headerAccentValue) {
tableHeader.classList.add(headerAccentValue);
// If we're using a dark background, make the text white
if (['bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', 'bg-dark'].includes(headerAccentValue)) {
tableHeader.classList.add('text-white');
}
}
// Get caption position (now using radio buttons)
const captionPosition = document.querySelector('input[name="captionPosition"]:checked').value;
if (captionPosition) {
stylePreviewTable.classList.remove('caption-top', 'caption-bottom');
stylePreviewTable.classList.add(captionPosition);
} else {
stylePreviewTable.classList.remove('caption-top', 'caption-bottom');
}
// Check if we should show row contextual classes
if (showRowClasses.checked) {
tableRows.forEach((row, index) => {
// First remove any existing contextual classes
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
// Add the contextual class based on index
if (index < rowClasses.length) {
row.classList.add(rowClasses[index]);
}
});
} else {
// Remove all contextual classes from rows
tableRows.forEach(row => {
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
});
}
// Update the table classes
stylePreviewTable.className = classes.join(' ');
// Update wrapper for responsiveness
if (tableWrapperClass && !tableContainer.classList.contains(tableWrapperClass)) {
tableContainer.className = tableWrapperClass;
} else if (!tableWrapperClass) {
tableContainer.className = '';
}
// Update the generated classes display and note about additional header styling if applied
const displayClasses = classes.join(' ');
let displayText = displayClasses;
// Add note about header styling if applied
if (headerAccentValue) {
displayText += `\n\n<!-- Additional header styling -->\n<thead>\n <tr class="${headerAccentValue}${['bg-primary', 'bg-secondary', 'bg-success', 'bg-danger', 'bg-dark'].includes(headerAccentValue) ? ' text-white' : ''}">\n <!-- your header cells here -->\n </tr>\n</thead>`;
}
// Add note about contextual row classes if enabled
if (showRowClasses.checked) {
displayText += `\n\n<!-- Row contextual classes -->\n<tbody>\n <tr class="table-primary">...</tr>\n <tr class="table-secondary">...</tr>\n <tr class="table-success">...</tr>\n <tr class="table-danger">...</tr>\n <tr class="table-warning">...</tr>\n <tr class="table-info">...</tr>\n</tbody>`;
}
generatedClasses.textContent = displayText;
codeClasses.textContent = displayClasses;
}
// Add event listeners to all form controls
tableStyleForm.addEventListener('change', updateTableStyle);
// Copy button functionality
copyStyleBtn.addEventListener('click', function() {
// Create a textarea element to copy from
const textarea = document.createElement('textarea');
textarea.value = generatedClasses.textContent;
document.body.appendChild(textarea);
textarea.select();
try {
// Execute copy command
document.execCommand('copy');
// Show success feedback
const originalText = copyStyleBtn.innerHTML;
copyStyleBtn.innerHTML = '<svg class="sa-icon me-1"><use href="icons/sprite.svg#check"/></svg> Copied!';
// Reset button text after delay
setTimeout(() => {
copyStyleBtn.innerHTML = originalText;
}, 2000);
} catch (err) {
console.error('Could not copy text: ', err);
}
// Remove the temporary textarea
document.body.removeChild(textarea);
});
// Reset button functionality
resetStylesBtn.addEventListener('click', function() {
// Reset form controls
document.getElementById('themeDefault').checked = true;
document.getElementById('sizeDefault').checked = true;
document.getElementById('tableAccent').value = '';
document.getElementById('headerAccent').value = '';
document.getElementById('captionDefault').checked = true;
document.getElementById('showRowClasses').checked = false;
document.getElementById('expandColumnsBtn').checked = true;
// Reset checkboxes
const checkboxes = document.querySelectorAll('input[name="tableStyle"]');
checkboxes.forEach(checkbox => {
if (checkbox.id === 'styleResponsive') {
checkbox.checked = true;
} else {
checkbox.checked = false;
}
});
// Restore columns to default view
expandColumns();
// Update table style
updateTableStyle();
});
// Function to hide columns
function collapseColumns() {
// Hide columns with d-md-table-cell and d-lg-table-cell classes
const mdCells = document.querySelectorAll('.d-md-table-cell');
mdCells.forEach(cell => {
cell.classList.add('d-none');
cell.classList.remove('d-md-table-cell');
});
const lgCells = document.querySelectorAll('.d-lg-table-cell');
lgCells.forEach(cell => {
cell.classList.add('d-none');
cell.classList.remove('d-lg-table-cell');
});
}
// Function to show all columns
function expandColumns() {
// Restore hidden columns
const hiddenCells = document.querySelectorAll('td.d-none, th.d-none');
hiddenCells.forEach(cell => {
if (cell.textContent.includes('Address') || cell.textContent.includes('City') ||
cell.parentElement.cells[4] === cell || cell.parentElement.cells[5] === cell) {
cell.classList.remove('d-none');
cell.classList.add('d-md-table-cell');
} else if (cell.textContent.includes('State') || cell.textContent.includes('Country') ||
cell.parentElement.cells[6] === cell || cell.parentElement.cells[7] === cell) {
cell.classList.remove('d-none');
cell.classList.add('d-lg-table-cell');
}
});
}
// Toggle columns for responsive preview - updated for radio buttons
collapseColumnsBtn.addEventListener('change', function() {
if (this.checked) {
collapseColumns();
}
});
expandColumnsBtn.addEventListener('change', function() {
if (this.checked) {
expandColumns();
}
});
// Hide row contextual classes by default
if (!showRowClasses.checked) {
tableRows.forEach(row => {
row.classList.remove('table-primary', 'table-secondary', 'table-success',
'table-danger', 'table-warning', 'table-info');
});
}
// Initialize the table with default styles
updateTableStyle();
});
+104
View File
@@ -0,0 +1,104 @@
(function() {
// Function to create the tree view
function createTreeView(data, container, level) {
var treeItem = document.createElement('div');
treeItem.className = 'tree-item ' + (data.type === 'directory' ? 'directory-item' : 'file-item');
// Auto-expand directories up to level 4
var isExpanded = data.type === 'directory' && level <= 2;
if (isExpanded) {
treeItem.className += ' expanded';
} else if (data.type === 'directory') {
treeItem.className += ' collapsed';
}
var itemContent = document.createElement('div');
itemContent.className = 'tree-item-content';
var toggleIcon = document.createElement('span');
toggleIcon.className = 'toggle-icon';
itemContent.appendChild(toggleIcon);
var icon = document.createElement('span');
icon.className = 'tree-item-icon ' + (data.type === 'directory' ? 'directory-icon' : 'file-icon');
icon.innerHTML = data.type === 'directory' ? '📁' : '📄';
itemContent.appendChild(icon);
var name = document.createElement('span');
name.className = 'tree-item-name';
name.textContent = data.name;
itemContent.appendChild(name);
if (data.path) {
var path = document.createElement('span');
path.className = 'path';
path.textContent = data.path;
itemContent.appendChild(path);
}
treeItem.appendChild(itemContent);
if (data.type === 'directory' && data.children && data.children.length > 0) {
var childrenContainer = document.createElement('div');
childrenContainer.className = 'tree-item-children';
// Sort children: directories first, then files, both alphabetically
var sortedChildren = data.children.slice();
sortedChildren.sort(function(a, b) {
if (a.type === b.type) {
return a.name.localeCompare(b.name);
}
return a.type === 'directory' ? -1 : 1;
});
for (var i = 0; i < sortedChildren.length; i++) {
createTreeView(sortedChildren[i], childrenContainer, level + 1);
}
treeItem.appendChild(childrenContainer);
// Add click event to toggle directory
itemContent.addEventListener('click', function(e) {
e.stopPropagation();
if (treeItem.classList.contains('expanded')) {
treeItem.classList.remove('expanded');
treeItem.classList.add('collapsed');
} else {
treeItem.classList.remove('collapsed');
treeItem.classList.add('expanded');
}
});
}
container.appendChild(treeItem);
}
// Function to load directory data using AJAX
function loadDirectoryData() {
var treeViewContainer = document.getElementById('tree-view');
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var directoryData = JSON.parse(xhr.responseText);
treeViewContainer.innerHTML = '';
createTreeView(directoryData, treeViewContainer, 1);
} catch (e) {
treeViewContainer.innerHTML = '<div class="error">Error parsing JSON data: ' + e.message + '</div>';
}
} else {
treeViewContainer.innerHTML = '<div class="error">Failed to load directory data. Status: ' + xhr.status + '</div>';
}
}
};
xhr.open('GET', 'json/directory-tree.json', true);
xhr.send();
}
// Load the directory data when the page loads
window.addEventListener('load', loadDirectoryData);
})();
@@ -0,0 +1,131 @@
document.addEventListener('DOMContentLoaded', function() {
// Use direct filtering implementation instead of SmartFilter
implementDirectFiltering();
// Function to remove classes with a specific prefix
function removeClassPrefix(elements, prefix) {
elements.forEach(element => {
const classes = Array.from(element.classList);
classes.forEach(cls => {
if (cls.startsWith(prefix)) {
element.classList.remove(cls);
}
});
});
}
// Select all radio inputs with name="contactview"
const radioButtons = document.querySelectorAll('input[type="radio"][name="contactview"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
const jsContacts = document.querySelector('#js-contacts');
const cards = jsContacts.querySelectorAll('.card');
const colXlElements = jsContacts.querySelectorAll('[class*="col-xl-"]');
const expandButtons = jsContacts.querySelectorAll('.js-expand-btn');
const doubleCardBodies = jsContacts.querySelectorAll('.card-body + .card-body');
if (this.value === 'grid') {
// Handle cards
removeClassPrefix(cards, 'mb-');
cards.forEach(card => card.classList.add('mb-g'));
// Handle col-xl classes
removeClassPrefix(colXlElements, 'col-xl-');
colXlElements.forEach(el => el.classList.add('col-xl-4'));
// Handle expand buttons
expandButtons.forEach(btn => btn.classList.add('d-none'));
// Handle double card bodies
doubleCardBodies.forEach(body => body.classList.add('show'));
}
else if (this.value === 'table') {
// Handle cards
removeClassPrefix(cards, 'mb-');
cards.forEach(card => card.classList.add('mb-1'));
// Handle col-xl classes
removeClassPrefix(colXlElements, 'col-xl-');
colXlElements.forEach(el => el.classList.add('col-xl-12'));
// Handle expand buttons
expandButtons.forEach(btn => btn.classList.remove('d-none'));
// Handle double card bodies
doubleCardBodies.forEach(body => body.classList.remove('show'));
}
});
});
// Direct filtering implementation that doesn't rely on SmartFilter
function implementDirectFiltering() {
const filterInput = document.getElementById('js-filter-contacts');
const clearBtn = document.getElementById('js-clear-filter');
const counterEl = document.getElementById('filter-result-counter');
// Input filtering
filterInput.addEventListener('input', function() {
const filterValue = this.value.toLowerCase();
const cards = document.querySelectorAll('#js-contacts .card');
const columns = document.querySelectorAll('#js-contacts [class*="col-xl-"]');
let visibleCount = 0;
// First, hide all columns
columns.forEach(col => {
col.style.display = 'none';
});
// Filter cards based on their data-filter-tags attribute
cards.forEach(card => {
const filterTags = card.getAttribute('data-filter-tags') || '';
const parentColumn = card.closest('[class*="col-xl-"]');
if (filterValue === '' || filterTags.toLowerCase().includes(filterValue)) {
if (parentColumn) {
parentColumn.style.display = ''; // Show the column
}
visibleCount++;
}
});
// Update UI
if (filterValue) {
counterEl.textContent = `Showing ${visibleCount} of ${cards.length} contacts`;
filterInput.classList.add('border-primary');
clearBtn.classList.remove('d-none');
} else {
counterEl.textContent = '';
filterInput.classList.remove('border-primary');
clearBtn.classList.add('d-none');
}
});
// Clear button functionality
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
filterInput.value = '';
// Show all columns
const columns = document.querySelectorAll('#js-contacts [class*="col-xl-"]');
columns.forEach(col => {
col.style.display = '';
});
// Reset UI
counterEl.textContent = '';
filterInput.classList.remove('border-primary');
clearBtn.classList.add('d-none');
});
// Set up keyboard events for convenience
filterInput.addEventListener('keydown', function(e) {
// Clear on Escape key
if (e.key === 'Escape') {
e.preventDefault();
this.value = '';
clearBtn.click();
}
});
}
});
@@ -0,0 +1,241 @@
// Show/hide display type section based on visibility mode
document.querySelectorAll('input[name="visibilityMode"]').forEach(radio => {
radio.addEventListener('change', function () {
const displayTypeSection = document.getElementById('displayTypeSection');
displayTypeSection.style.display = this.value === 'visible' ? 'block' : 'none';
});
});
// Handle "All Breakpoints" checkbox logic
const allCheckbox = document.getElementById('all');
const breakpointCheckboxes = document.querySelectorAll('.breakpoint-checkbox');
allCheckbox.addEventListener('change', function () {
const isChecked = this.checked;
breakpointCheckboxes.forEach(cb => {
cb.checked = isChecked;
});
});
breakpointCheckboxes.forEach(cb => {
cb.addEventListener('change', function () {
const allChecked = Array.from(breakpointCheckboxes).every(cb => cb.checked);
allCheckbox.checked = allChecked;
});
});
document.getElementById('generateBtn').addEventListener('click', function () {
// Get visibility mode
const visibilityMode = document.querySelector('input[name="visibilityMode"]:checked').value;
// Get selected breakpoints
const selectedBreakpoints = Array.from(document.querySelectorAll('.breakpoint-checkbox'))
.filter(cb => cb.checked)
.map(cb => cb.value);
// Get selected display type (for visible mode, default to 'block' for hidden mode visibility)
const displayType = visibilityMode === 'visible' ?
document.querySelector('input[name="displayType"]:checked').value : 'block';
// Validate selections
if (selectedBreakpoints.length === 0) {
document.getElementById('classOutput').textContent = 'Please select at least one breakpoint.';
document.getElementById('cssOutput').textContent = '/* No CSS generated */';
document.getElementById('testDiv').className = 'd-none'; // Hide test div
return;
}
// Define all breakpoints
const allBreakpoints = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
const mediaQueries = {
'sm': '@media (min-width: 576px)',
'md': '@media (min-width: 768px)',
'lg': '@media (min-width: 992px)',
'xl': '@media (min-width: 1200px)',
'xxl': '@media (min-width: 1400px)'
};
// Determine unselected breakpoints
const unselectedBreakpoints = allBreakpoints.filter(bp => !selectedBreakpoints.includes(bp));
// Generate BS5 classes based on visibility mode
const classes = [];
const cssLines = [];
const selector = '.my-element'; // Example selector
if (visibilityMode === 'hidden') {
// Default: visible (using displayType)
classes.push(`d-${displayType}`);
cssLines.push(`${selector} { display: ${displayType} !important; }`);
// Hide on selected breakpoints
selectedBreakpoints.forEach(bp => {
if (bp === 'xs') {
classes[0] = 'd-none'; // Override default for xs
classes.push('d-sm-block');
cssLines[0] = `${selector} { display: none !important; }`;
cssLines.push(`${mediaQueries['sm']} {`);
cssLines.push(` ${selector} { display: ${displayType} !important; }`);
cssLines.push('}');
} else {
classes.push(`d-${bp}-none`);
cssLines.push(`${mediaQueries[bp]} {`);
cssLines.push(` ${selector} { display: none !important; }`);
cssLines.push('}');
}
});
// Ensure visibility on unselected breakpoints after hiding
unselectedBreakpoints.forEach(bp => {
if (bp !== 'xs' && selectedBreakpoints.includes(allBreakpoints[allBreakpoints.indexOf(bp) - 1])) {
classes.push(`d-${bp}-${displayType}`);
cssLines.push(`${mediaQueries[bp]} {`);
cssLines.push(` ${selector} { display: ${displayType} !important; }`);
cssLines.push('}');
}
});
} else { // visibilityMode === 'visible'
// Default: hidden
classes.push('d-none');
cssLines.push(`${selector} { display: none !important; }`);
// Show on selected breakpoints
selectedBreakpoints.forEach(bp => {
const prefix = bp === 'xs' ? 'd' : `d-${bp}`;
classes.push(`${prefix}-${displayType}`);
if (bp !== 'xs') {
cssLines.push(`${mediaQueries[bp]} {`);
cssLines.push(` ${selector} { display: ${displayType} !important; }`);
cssLines.push('}');
} else {
cssLines[0] = `${selector} { display: ${displayType} !important; }`;
}
});
// Ensure hidden on unselected breakpoints after showing
unselectedBreakpoints.forEach(bp => {
if (bp !== 'xs' && selectedBreakpoints.includes(allBreakpoints[allBreakpoints.indexOf(bp) - 1])) {
classes.push(`d-${bp}-none`);
cssLines.push(`${mediaQueries[bp]} {`);
cssLines.push(` ${selector} { display: none !important; }`);
cssLines.push('}');
}
});
}
// Apply classes to the test div
const testDiv = document.getElementById('testDiv');
testDiv.className = 'd-none'; // Reset classes
testDiv.classList.add(...classes);
// Output the classes
document.getElementById('classOutput').textContent = classes.join(' ');
// Output the CSS equivalent
document.getElementById('cssOutput').textContent = cssLines.join('\n');
});
// Additional script to enhance the page functionality
document.addEventListener('DOMContentLoaded', function () {
// Track current breakpoint
const currentBreakpointEl = document.getElementById('currentBreakpoint');
const breakpointIndicators = document.querySelectorAll('.breakpoint-indicator');
// Copy buttons functionality
const copyClassesBtn = document.getElementById('copyClasses');
const copyCSSBtn = document.getElementById('copyCSS');
const classOutput = document.getElementById('classOutput');
const cssOutput = document.getElementById('cssOutput');
// Copy functionality
copyClassesBtn.addEventListener('click', function () {
const text = classOutput.textContent;
copyToClipboard(text, this);
});
copyCSSBtn.addEventListener('click', function () {
const text = cssOutput.textContent;
copyToClipboard(text, this);
});
function copyToClipboard(text, button) {
const originalText = button.innerHTML;
navigator.clipboard.writeText(text).then(function () {
button.innerHTML = '<i class="fal fa-check me-1"></i> Copied!';
setTimeout(function () {
button.innerHTML = originalText;
}, 2000);
});
}
// Show/hide display type section based on visibility mode
const visibilityRadios = document.querySelectorAll('input[name="visibilityMode"]');
const displayTypeSection = document.getElementById('displayTypeSection');
visibilityRadios.forEach(radio => {
radio.addEventListener('change', function () {
if (this.value === 'visible') {
displayTypeSection.style.display = 'block';
} else {
displayTypeSection.style.display = 'none';
}
});
});
// Detect current breakpoint
function updateCurrentBreakpoint() {
let current = 'xs';
const width = window.innerWidth;
if (width >= 1400) current = 'xxl';
else if (width >= 1200) current = 'xl';
else if (width >= 992) current = 'lg';
else if (width >= 768) current = 'md';
else if (width >= 576) current = 'sm';
currentBreakpointEl.textContent = current;
// Update indicators
breakpointIndicators.forEach(indicator => {
const bp = indicator.getAttribute('data-bp');
if ((bp === 'xs' && width < 576) ||
(bp === 'sm' && width >= 576) ||
(bp === 'md' && width >= 768) ||
(bp === 'lg' && width >= 992) ||
(bp === 'xl' && width >= 1200) ||
(bp === 'xxl' && width >= 1400)) {
indicator.classList.add('active');
} else {
indicator.classList.remove('active');
}
});
}
// Initial update and listen for resize
updateCurrentBreakpoint();
window.addEventListener('resize', updateCurrentBreakpoint);
// Select all checkboxes when "All Breakpoints" is checked
const allCheckbox = document.getElementById('all');
const breakpointCheckboxes = document.querySelectorAll('.breakpoint-checkbox');
allCheckbox.addEventListener('change', function () {
breakpointCheckboxes.forEach(checkbox => {
checkbox.checked = this.checked;
checkbox.disabled = this.checked;
});
});
// When a class is generated, show the copy buttons
const generateBtn = document.getElementById('generateBtn');
generateBtn.addEventListener('click', function () {
setTimeout(() => {
if (classOutput.textContent !== 'Select options and click "Generate Classes" to see the result.') {
copyClassesBtn.classList.remove('d-none');
copyCSSBtn.classList.remove('d-none');
}
}, 100);
});
});