/**
* MixinUI Framework JavaScript
* A lightweight UI framework with namespaced components
*/
// Create MixinUI namespace
const MixinUI = {
version: '1.0.0',
/**
* Initialize the framework
* @param {Object} options - Configuration options
*/
init: function(options = {}) {
console.log('MixinUI Framework initializing...');
// Merge default options with user options
const settings = Object.assign({
debug: false,
plugins: []
}, options);
// Store settings
this.settings = settings;
// Initialize plugins
if (settings.plugins && settings.plugins.length) {
this.initPlugins(settings.plugins);
}
// Debug mode
if (settings.debug) {
console.log('Debug mode enabled');
this.debug = true;
}
// Initialize components
this.initComponents();
console.log('MixinUI Framework initialized');
return this;
},
/**
* Initialize plugins
* @param {Array} plugins - List of plugins
*/
initPlugins: function(plugins) {
console.log('Initializing plugins...');
plugins.forEach(plugin => {
if (typeof plugin.init === 'function') {
plugin.init(this);
console.log(`Plugin ${plugin.name || 'unnamed'} initialized`);
}
});
},
/**
* Initialize components
*/
initComponents: function() {
// Initialize modals
this.initModals();
// Initialize tooltips
this.initTooltips();
// Initialize tabs
this.initTabs();
// Initialize dropdowns
this.initDropdowns();
},
/**
* Initialize modals
*/
initModals: function() {
const modalTriggers = document.querySelectorAll('[data-mixinui-toggle="modal"]');
modalTriggers.forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
const targetId = trigger.getAttribute('data-mixinui-target');
const modal = document.querySelector(targetId);
if (modal) {
this.showModal(modal);
}
});
});
const modalCloseButtons = document.querySelectorAll('[data-mixinui-dismiss="modal"]');
modalCloseButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const modal = button.closest('.mixinui_modal');
if (modal) {
this.hideModal(modal);
}
});
});
// Close modal when clicking on backdrop
document.addEventListener('click', (e) => {
if (e.target.classList.contains('mixinui_modal') && !e.target.classList.contains('mixinui_modal-dialog')) {
this.hideModal(e.target);
}
});
// Close modal with ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const openModal = document.querySelector('.mixinui_modal.mixinui_show');
if (openModal) {
this.hideModal(openModal);
}
}
});
},
/**
* Show modal
* @param {Element} modal - Modal element
*/
showModal: function(modal) {
// Create backdrop
const backdrop = document.createElement('div');
backdrop.className = 'mixinui_modal-backdrop';
document.body.appendChild(backdrop);
// Show modal
modal.style.display = 'block';
// Trigger animation
setTimeout(() => {
modal.classList.add('mixinui_show');
}, 50);
// Prevent background scrolling
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = '15px';
// Add open class to body
document.body.classList.add('mixinui_modal-open');
},
/**
* Hide modal
* @param {Element} modal - Modal element
*/
hideModal: function(modal) {
// Remove show class
modal.classList.remove('mixinui_show');
const backdrop = document.querySelector('.mixinui_modal-backdrop');
// Delay removal to allow animation
setTimeout(() => {
modal.style.display = 'none';
if (backdrop) {
document.body.removeChild(backdrop);
}
// Restore background scrolling
document.body.style.overflow = '';
document.body.style.paddingRight = '';
// Remove open class from body
document.body.classList.remove('mixinui_modal-open');
}, 300);
},
/**
* Initialize tooltips
*/
initTooltips: function() {
const tooltipTriggers = document.querySelectorAll('[data-mixinui-toggle="tooltip"]');
tooltipTriggers.forEach(trigger => {
trigger.addEventListener('mouseenter', () => {
const title = trigger.getAttribute('title') || trigger.getAttribute('data-mixinui-title');
const placement = trigger.getAttribute('data-mixinui-placement') || 'top';
if (title) {
this.showTooltip(trigger, title, placement);
trigger.setAttribute('data-mixinui-title', title);
trigger.removeAttribute('title');
}
});
trigger.addEventListener('mouseleave', () => {
this.hideTooltip();
});
});
},
/**
* Show tooltip
* @param {Element} element - Target element
* @param {String} text - Tooltip text
* @param {String} placement - Tooltip placement
*/
showTooltip: function(element, text, placement = 'top') {
// Remove existing tooltips
this.hideTooltip();
// Create tooltip
const tooltip = document.createElement('div');
tooltip.className = `mixinui_tooltip mixinui_bs-tooltip-${placement}`;
tooltip.setAttribute('role', 'tooltip');
const arrow = document.createElement('div');
arrow.className = 'mixinui_arrow';
const inner = document.createElement('div');
inner.className = 'mixinui_tooltip-inner';
inner.textContent = text;
tooltip.appendChild(arrow);
tooltip.appendChild(inner);
document.body.appendChild(tooltip);
// Position tooltip
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
switch (placement) {
case 'top':
tooltip.style.top = `${rect.top + scrollTop - tooltip.offsetHeight - 10}px`;
tooltip.style.left = `${rect.left + scrollLeft + (rect.width / 2) - (tooltip.offsetWidth / 2)}px`;
break;
case 'bottom':
tooltip.style.top = `${rect.bottom + scrollTop + 10}px`;
tooltip.style.left = `${rect.left + scrollLeft + (rect.width / 2) - (tooltip.offsetWidth / 2)}px`;
break;
case 'left':
tooltip.style.top = `${rect.top + scrollTop + (rect.height / 2) - (tooltip.offsetHeight / 2)}px`;
tooltip.style.left = `${rect.left + scrollLeft - tooltip.offsetWidth - 10}px`;
break;
case 'right':
tooltip.style.top = `${rect.top + scrollTop + (rect.height / 2) - (tooltip.offsetHeight / 2)}px`;
tooltip.style.left = `${rect.right + scrollLeft + 10}px`;
break;
}
// Show tooltip
setTimeout(() => {
tooltip.classList.add('mixinui_show');
}, 50);
},
/**
* Hide tooltip
*/
hideTooltip: function() {
const tooltip = document.querySelector('.mixinui_tooltip');
if (tooltip) {
tooltip.classList.remove('mixinui_show');
setTimeout(() => {
if (tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
}, 300);
}
},
/**
* Initialize tabs
*/
initTabs: function() {
const tabLinks = document.querySelectorAll('[data-mixinui-toggle="tab"]');
tabLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetId = link.getAttribute('href') || link.getAttribute('data-mixinui-target');
const tabContent = document.querySelector(targetId);
if (tabContent) {
// Get tab container
const tabContainer = link.closest('.mixinui_nav-tabs, .mixinui_nav-pills');
const contentContainer = tabContent.parentNode;
// Remove active class
tabContainer.querySelectorAll('.mixinui_nav-link').forEach(item => {
item.classList.remove('mixinui_active');
});
contentContainer.querySelectorAll('.mixinui_tab-pane').forEach(pane => {
pane.classList.remove('mixinui_active', 'mixinui_show');
});
// Add active class
link.classList.add('mixinui_active');
tabContent.classList.add('mixinui_active', 'mixinui_show');
}
});
});
},
/**
* Initialize dropdowns
*/
initDropdowns: function() {
const dropdownToggles = document.querySelectorAll('[data-mixinui-toggle="dropdown"]');
dropdownToggles.forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const dropdown = toggle.closest('.mixinui_dropdown');
if (dropdown) {
const menu = dropdown.querySelector('.mixinui_dropdown-menu');
// Toggle show state
dropdown.classList.toggle('mixinui_show');
if (menu) {
menu.classList.toggle('mixinui_show');
}
}
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
const openDropdowns = document.querySelectorAll('.mixinui_dropdown.mixinui_show');
openDropdowns.forEach(dropdown => {
if (!dropdown.contains(e.target)) {
dropdown.classList.remove('mixinui_show');
const menu = dropdown.querySelector('.mixinui_dropdown-menu');
if (menu) {
menu.classList.remove('mixinui_show');
}
}
});
});
},
/**
* Debug log
* @param {String} message - Log message
* @param {*} data - Additional data
*/
log: function(message, data) {
if (this.debug) {
console.log(`[MixinUI] ${message}`, data || '');
}
},
/**
* DOM selector
* @param {String} selector - CSS selector
* @param {Element} context - Context element, defaults to document
* @returns {Element|NodeList} - Matching element(s)
*/
$: function(selector, context = document) {
const result = context.querySelectorAll(selector);
return result.length === 1 ? result[0] : result;
},
/**
* Add class
* @param {Element} element - Target element
* @param {String} className - Class name
*/
addClass: function(element, className) {
element.classList.add(className);
return element;
},
/**
* Remove class
* @param {Element} element - Target element
* @param {String} className - Class name
*/
removeClass: function(element, className) {
element.classList.remove(className);
return element;
},
/**
* Toggle class
* @param {Element} element - Target element
* @param {String} className - Class name
* @param {Boolean} force - Force add or remove
*/
toggleClass: function(element, className, force) {
element.classList.toggle(className, force);
return element;
},
/**
* Check if element has class
* @param {Element} element - Target element
* @param {String} className - Class name
* @returns {Boolean} - Whether element has class
*/
hasClass: function(element, className) {
return element.classList.contains(className);
},
/**
* Create element
* @param {String} tag - Tag name
* @param {Object} attributes - Attributes object
* @param {String|Element} content - Content or child element
* @returns {Element} - Created element
*/
createElement: function(tag, attributes = {}, content = '') {
const element = document.createElement(tag);
// Set attributes
Object.keys(attributes).forEach(key => {
if (key === 'class' || key === 'className') {
element.className = attributes[key];
} else if (key === 'style' && typeof attributes[key] === 'object') {
Object.assign(element.style, attributes[key]);
} else {
element.setAttribute(key, attributes[key]);
}
});
// Set content
if (content) {
if (typeof content === 'string') {
element.innerHTML = content;
} else if (content instanceof Element) {
element.appendChild(content);
}
}
return element;
},
/**
* Add event listener
* @param {Element} element - Target element
* @param {String} event - Event type
* @param {Function} callback - Callback function
* @param {Object} options - Event options
*/
on: function(element, event, callback, options = false) {
element.addEventListener(event, callback, options);
return element;
},
/**
* Remove event listener
* @param {Element} element - Target element
* @param {String} event - Event type
* @param {Function} callback - Callback function
* @param {Object} options - Event options
*/
off: function(element, event, callback, options = false) {
element.removeEventListener(event, callback, options);
return element;
},
/**
* Trigger event
* @param {Element} element - Target element
* @param {String} eventType - Event type
* @param {Object} detail - Event details
*/
trigger: function(element, eventType, detail = {}) {
const event = new CustomEvent(eventType, {
bubbles: true,
cancelable: true,
detail: detail
});
element.dispatchEvent(event);
return element;
},
/**
* Debounce function
* @param {Function} func - Function to execute
* @param {Number} wait - Wait time in milliseconds
* @returns {Function} - Debounced function
*/
debounce: function(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
},
/**
* Throttle function
* @param {Function} func - Function to execute
* @param {Number} limit - Time limit in milliseconds
* @returns {Function} - Throttled function
*/
throttle: function(func, limit) {
let inThrottle;
return function(...args) {
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
};
// Initialize MixinUI when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize framework
MixinUI.init({
debug: false
});
});