mixinui.js

/**
 * 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
  });
});
目录
设置
主题设置
深色模式
字体设置
字体大小
16