/** * MDL Components Replacement * Lightweight JavaScript to handle MDL component behaviors * Provides compatibility with existing JS that uses MDL components */ (function() { 'use strict'; // ========================================================================== // Component Handler - MDL Compatibility Layer // ========================================================================== window.componentHandler = { /** * Upgrades all registered components found in the DOM * This is the main entry point for MDL component initialization */ upgradeAllRegistered: function() { // Initialize all textfields document.querySelectorAll('.mdl-textfield').forEach(function(el) { if (!el.MaterialTextfield) { el.MaterialTextfield = new MaterialTextfield(el); } }); // Initialize all checkboxes document.querySelectorAll('.mdl-checkbox').forEach(function(el) { if (!el.MaterialCheckbox) { el.MaterialCheckbox = new MaterialCheckbox(el); } }); // Initialize all switches document.querySelectorAll('.mdl-switch').forEach(function(el) { if (!el.MaterialSwitch) { el.MaterialSwitch = new MaterialSwitch(el); } }); // Initialize all menus document.querySelectorAll('.mdl-menu').forEach(function(el) { if (!el.MaterialMenu) { el.MaterialMenu = new MaterialMenu(el); } }); // Initialize snackbar document.querySelectorAll('.mdl-snackbar').forEach(function(el) { if (!el.MaterialSnackbar) { el.MaterialSnackbar = new MaterialSnackbar(el); } }); // Initialize tooltips document.querySelectorAll('.mdl-tooltip').forEach(function(el) { if (!el.MaterialTooltip) { el.MaterialTooltip = new MaterialTooltip(el); } }); // Initialize layout document.querySelectorAll('.mdl-layout').forEach(function(el) { if (!el.MaterialLayout) { el.MaterialLayout = new MaterialLayout(el); } }); // Initialize tabs document.querySelectorAll('.mdl-tabs').forEach(function(el) { if (!el.MaterialTabs) { el.MaterialTabs = new MaterialTabs(el); } }); }, /** * Upgrades a specific element with a component */ upgradeElement: function(element, jsClass) { if (!element) return; if (jsClass === 'MaterialTextfield' && !element.MaterialTextfield) { element.MaterialTextfield = new MaterialTextfield(element); } else if (jsClass === 'MaterialCheckbox' && !element.MaterialCheckbox) { element.MaterialCheckbox = new MaterialCheckbox(element); } else if (jsClass === 'MaterialSwitch' && !element.MaterialSwitch) { element.MaterialSwitch = new MaterialSwitch(element); } else if (jsClass === 'MaterialMenu' && !element.MaterialMenu) { element.MaterialMenu = new MaterialMenu(element); } else if (jsClass === 'MaterialSnackbar' && !element.MaterialSnackbar) { element.MaterialSnackbar = new MaterialSnackbar(element); } }, /** * Upgrades all unregistered elements (same as upgradeAllRegistered for compatibility) */ upgradeDom: function() { this.upgradeAllRegistered(); } }; // ========================================================================== // MaterialTextfield - Floating Label Input // ========================================================================== function MaterialTextfield(element) { this.element = element; this.input = element.querySelector('.mdl-textfield__input'); this.label = element.querySelector('.mdl-textfield__label'); if (this.input) { this.init(); } } MaterialTextfield.prototype.init = function() { var self = this; // Check initial state this.checkDirty(); // Add event listeners this.input.addEventListener('focus', function() { self.element.classList.add('is-focused'); }); this.input.addEventListener('blur', function() { self.element.classList.remove('is-focused'); self.checkDirty(); }); this.input.addEventListener('input', function() { self.checkDirty(); }); // Handle autofill this.input.addEventListener('animationstart', function(e) { if (e.animationName === 'onAutoFillStart') { self.element.classList.add('is-dirty'); } }); }; MaterialTextfield.prototype.checkDirty = function() { if (this.input.value && this.input.value.length > 0) { this.element.classList.add('is-dirty'); } else { this.element.classList.remove('is-dirty'); } }; MaterialTextfield.prototype.checkValidity = function() { if (this.input.validity) { if (this.input.validity.valid) { this.element.classList.remove('is-invalid'); } else { this.element.classList.add('is-invalid'); } } }; // ========================================================================== // MaterialCheckbox // ========================================================================== function MaterialCheckbox(element) { this.element = element; this.input = element.querySelector('.mdl-checkbox__input'); if (this.input) { this.init(); } } MaterialCheckbox.prototype.init = function() { var self = this; // Check initial state this.updateClasses(); // Add event listener this.input.addEventListener('change', function() { self.updateClasses(); }); }; MaterialCheckbox.prototype.updateClasses = function() { if (this.input.checked) { this.element.classList.add('is-checked'); } else { this.element.classList.remove('is-checked'); } if (this.input.disabled) { this.element.classList.add('is-disabled'); } else { this.element.classList.remove('is-disabled'); } }; MaterialCheckbox.prototype.check = function() { this.input.checked = true; this.updateClasses(); }; MaterialCheckbox.prototype.uncheck = function() { this.input.checked = false; this.updateClasses(); }; // ========================================================================== // MaterialSwitch // ========================================================================== function MaterialSwitch(element) { this.element = element; this.input = element.querySelector('.mdl-switch__input'); if (this.input) { this.init(); } } MaterialSwitch.prototype.init = function() { var self = this; // Check initial state this.updateClasses(); // Add event listener this.input.addEventListener('change', function() { self.updateClasses(); }); }; MaterialSwitch.prototype.updateClasses = function() { if (this.input.checked) { this.element.classList.add('is-checked'); } else { this.element.classList.remove('is-checked'); } if (this.input.disabled) { this.element.classList.add('is-disabled'); } else { this.element.classList.remove('is-disabled'); } }; MaterialSwitch.prototype.on = function() { this.input.checked = true; this.updateClasses(); }; MaterialSwitch.prototype.off = function() { this.input.checked = false; this.updateClasses(); }; // ========================================================================== // MaterialMenu // ========================================================================== function MaterialMenu(element) { this.element = element; this.forElement = null; this.container = null; this.init(); } MaterialMenu.prototype.init = function() { var self = this; // Find the element this menu is for var forId = this.element.getAttribute('for') || this.element.getAttribute('data-mdl-for'); if (forId) { this.forElement = document.getElementById(forId); } // Create container if needed if (!this.element.parentElement.classList.contains('mdl-menu__container')) { this.container = document.createElement('div'); this.container.classList.add('mdl-menu__container'); this.element.parentElement.insertBefore(this.container, this.element); this.container.appendChild(this.element); // Add outline var outline = document.createElement('div'); outline.classList.add('mdl-menu__outline'); this.container.insertBefore(outline, this.element); } else { this.container = this.element.parentElement; } // Position relative to forElement if it exists if (this.forElement) { this.forElement.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); self.toggle(); }); } // Close on click outside document.addEventListener('click', function(e) { if (!self.element.contains(e.target) && self.forElement !== e.target) { self.hide(); } }); // Handle menu item clicks this.element.querySelectorAll('.mdl-menu__item').forEach(function(item) { item.addEventListener('click', function() { self.hide(); }); }); }; MaterialMenu.prototype.show = function() { this.element.classList.add('is-visible'); if (this.container) { this.container.classList.add('is-visible'); } }; MaterialMenu.prototype.hide = function() { this.element.classList.remove('is-visible'); if (this.container) { this.container.classList.remove('is-visible'); } }; MaterialMenu.prototype.toggle = function() { if (this.element.classList.contains('is-visible')) { this.hide(); } else { this.show(); } }; // ========================================================================== // MaterialSnackbar // ========================================================================== function MaterialSnackbar(element) { this.element = element; this.textElement = element.querySelector('.mdl-snackbar__text'); this.actionElement = element.querySelector('.mdl-snackbar__action'); this.active = false; this.actionHandler = null; this.timeout = null; this.queuedNotifications = []; } MaterialSnackbar.prototype.showSnackbar = function(data) { if (this.active) { this.queuedNotifications.push(data); return; } var self = this; this.active = true; // Set text if (this.textElement) { this.textElement.textContent = data.message || ''; } // Set action if (this.actionElement) { if (data.actionText) { this.actionElement.textContent = data.actionText; this.actionElement.style.display = ''; this.actionHandler = data.actionHandler || null; // Remove old listener and add new one var newAction = this.actionElement.cloneNode(true); this.actionElement.parentNode.replaceChild(newAction, this.actionElement); this.actionElement = newAction; if (this.actionHandler) { this.actionElement.addEventListener('click', function() { self.actionHandler(); self.hideSnackbar(); }); } } else { this.actionElement.style.display = 'none'; } } // Show snackbar this.element.classList.add('is-active'); // Auto-hide after timeout var timeout = data.timeout || 2750; this.timeout = setTimeout(function() { self.hideSnackbar(); }, timeout); }; MaterialSnackbar.prototype.hideSnackbar = function() { var self = this; if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } this.element.classList.remove('is-active'); this.active = false; // Check queue if (this.queuedNotifications.length > 0) { setTimeout(function() { self.showSnackbar(self.queuedNotifications.shift()); }, 300); } }; // ========================================================================== // MaterialTooltip // ========================================================================== function MaterialTooltip(element) { this.element = element; this.forElement = null; this.init(); } MaterialTooltip.prototype.init = function() { var self = this; // Find the element this tooltip is for var forId = this.element.getAttribute('for') || this.element.getAttribute('data-mdl-for'); if (forId) { this.forElement = document.getElementById(forId); } if (this.forElement) { this.forElement.addEventListener('mouseenter', function() { self.show(); }); this.forElement.addEventListener('mouseleave', function() { self.hide(); }); this.forElement.addEventListener('touchend', function() { self.hide(); }); } }; MaterialTooltip.prototype.show = function() { if (!this.forElement) return; var rect = this.forElement.getBoundingClientRect(); var tooltipRect = this.element.getBoundingClientRect(); // Position below the element by default var top = rect.bottom + 10; var left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); // Keep within viewport if (left < 8) left = 8; if (left + tooltipRect.width > window.innerWidth - 8) { left = window.innerWidth - tooltipRect.width - 8; } this.element.style.position = 'fixed'; this.element.style.top = top + 'px'; this.element.style.left = left + 'px'; this.element.classList.add('is-active'); }; MaterialTooltip.prototype.hide = function() { this.element.classList.remove('is-active'); }; // ========================================================================== // MaterialLayout // ========================================================================== function MaterialLayout(element) { this.element = element; this.header = element.querySelector('.mdl-layout__header'); this.drawer = element.querySelector('.mdl-layout__drawer'); this.content = element.querySelector('.mdl-layout__content'); this.drawerButton = null; this.obfuscator = null; this.init(); } MaterialLayout.prototype.init = function() { var self = this; // Add drawer button if drawer exists if (this.drawer) { // Create drawer button if it doesn't exist this.drawerButton = this.element.querySelector('.mdl-layout__drawer-button'); if (!this.drawerButton) { this.drawerButton = document.createElement('div'); this.drawerButton.classList.add('mdl-layout__drawer-button'); this.drawerButton.innerHTML = 'menu'; this.element.insertBefore(this.drawerButton, this.element.firstChild); } this.drawerButton.addEventListener('click', function() { self.toggleDrawer(); }); // Create obfuscator this.obfuscator = document.createElement('div'); this.obfuscator.classList.add('mdl-layout__obfuscator'); this.element.appendChild(this.obfuscator); this.obfuscator.addEventListener('click', function() { self.closeDrawer(); }); // Close drawer on navigation link click this.drawer.querySelectorAll('.mdl-navigation__link').forEach(function(link) { link.addEventListener('click', function() { self.closeDrawer(); }); }); } // Handle tabs this.initTabs(); }; MaterialLayout.prototype.initTabs = function() { var self = this; var tabs = this.element.querySelectorAll('.mdl-layout__tab'); var panels = this.element.querySelectorAll('.mdl-layout__tab-panel'); tabs.forEach(function(tab) { tab.addEventListener('click', function(e) { // Don't prevent default for actual links var href = tab.getAttribute('href'); if (href && href.startsWith('#')) { e.preventDefault(); // Remove active class from all tabs tabs.forEach(function(t) { t.classList.remove('is-active'); }); // Add active class to clicked tab tab.classList.add('is-active'); // Show corresponding panel var targetId = href.substring(1); panels.forEach(function(panel) { if (panel.id === targetId) { panel.classList.add('is-active'); } else { panel.classList.remove('is-active'); } }); } }); }); }; MaterialLayout.prototype.toggleDrawer = function() { if (this.drawer.classList.contains('is-visible')) { this.closeDrawer(); } else { this.openDrawer(); } }; MaterialLayout.prototype.openDrawer = function() { this.drawer.classList.add('is-visible'); if (this.obfuscator) { this.obfuscator.classList.add('is-visible'); } }; MaterialLayout.prototype.closeDrawer = function() { this.drawer.classList.remove('is-visible'); if (this.obfuscator) { this.obfuscator.classList.remove('is-visible'); } }; // ========================================================================== // MaterialTabs // ========================================================================== function MaterialTabs(element) { this.element = element; this.tabs = element.querySelectorAll('.mdl-tabs__tab'); this.panels = element.querySelectorAll('.mdl-tabs__panel'); this.init(); } MaterialTabs.prototype.init = function() { var self = this; this.tabs.forEach(function(tab) { tab.addEventListener('click', function(e) { var href = tab.getAttribute('href'); if (href && href.startsWith('#')) { e.preventDefault(); // Remove active from all self.tabs.forEach(function(t) { t.classList.remove('is-active'); }); self.panels.forEach(function(p) { p.classList.remove('is-active'); }); // Add active to clicked tab.classList.add('is-active'); var panel = self.element.querySelector(href); if (panel) { panel.classList.add('is-active'); } } }); }); }; // ========================================================================== // Initialize on DOM ready // ========================================================================== function initComponents() { componentHandler.upgradeAllRegistered(); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initComponents); } else { initComponents(); } // Also expose classes globally for direct instantiation window.MaterialTextfield = MaterialTextfield; window.MaterialCheckbox = MaterialCheckbox; window.MaterialSwitch = MaterialSwitch; window.MaterialMenu = MaterialMenu; window.MaterialSnackbar = MaterialSnackbar; window.MaterialTooltip = MaterialTooltip; window.MaterialLayout = MaterialLayout; window.MaterialTabs = MaterialTabs; })();