UNPKG

material-inspired-component-library

Version:

The Material-Inspired Component Library (MICL) offers a collection of beautifully crafted components leveraging native HTML markup, designed to align with the Material Design 3 guidelines.

191 lines (174 loc) 7.36 kB
// // Copyright © 2025 Hermana AS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import _bottomsheet, { bottomsheetSelector } from './components/bottomsheet'; import _button, { buttonSelector } from './components/button'; import _list, { listSelector } from './components/list'; import _menu, { menuSelector } from './components/menu'; import _navigationrail, { navigationrailSelector } from './components/navigationrail'; import _slider, { sliderSelector } from './components/slider'; import _textfield, { textfieldSelector, textareaSelector, selectSelector } from './components/textfield'; interface ComponentEntry<T extends HTMLElement> { component: { initialize?: (element: T) => void, input? : (event: Event) => void, keydown? : (event: Event) => void, cleanup? : (element: T) => void }; type: new () => T; } export default (() => { const componentMap: Record<string, ComponentEntry<any>> = { [bottomsheetSelector] : { component: _bottomsheet, type: HTMLDialogElement }, [buttonSelector] : { component: _button, type: HTMLButtonElement }, [listSelector] : { component: _list, type: HTMLElement }, [menuSelector] : { component: _menu, type: HTMLElement }, [navigationrailSelector]: { component: _navigationrail, type: HTMLLabelElement }, [selectSelector] : { component: _textfield, type: HTMLSelectElement }, [sliderSelector] : { component: _slider, type: HTMLInputElement }, [textareaSelector] : { component: _textfield, type: HTMLTextAreaElement }, [textfieldSelector] : { component: _textfield, type: HTMLInputElement } }; const selector = Object.keys(componentMap).join(','); const initializeComponent = (element: HTMLElement): void => { for (const [selector, { component, type }] of Object.entries(componentMap)) { if ( element.matches(selector) && element instanceof type && typeof component.initialize === 'function' ) { component.initialize(element); return; } } }; const initializeComponents = (parent: HTMLDocument | HTMLElement): void => { parent.querySelectorAll<HTMLElement>(selector).forEach(initializeComponent); parent.querySelectorAll<HTMLElement>('[class*="micl-"]').forEach(element => { if (window.getComputedStyle(element).getPropertyValue('--micl-ripple') === '1') { element.addEventListener('pointerdown', e => { if ((e.currentTarget as Element).classList.contains('micl-card--nonactionable')) { return; } e.stopPropagation(); let r = element.getBoundingClientRect(); element.style.setProperty('--micl-x', `${e.clientX - r.left}px`); element.style.setProperty('--micl-y', `${e.clientY - r.top}px`); }); } }); }; const cleanupComponent = (element: HTMLElement): void => { for (const [selector, { component, type }] of Object.entries(componentMap)) { if ( element.matches(selector) && element instanceof type && typeof component.cleanup === 'function' ) { component.cleanup(element); return; } } }; const cleanupComponents = (parent: HTMLDocument | HTMLElement): void => { parent.querySelectorAll<HTMLElement>(selector).forEach(cleanupComponent); }; const activate = () => { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type !== 'childList') { return; } mutation.addedNodes.forEach(node => { if (node instanceof HTMLElement) { if (node.matches(selector)) { initializeComponent(node); } node.querySelectorAll<HTMLElement>(selector).forEach(initializeComponent); } }); mutation.removedNodes.forEach(node => { if (node instanceof HTMLElement) { if (node.matches(selector)) { cleanupComponent(node); } cleanupComponents(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); initializeComponents(document); // Delegated Event Handlers document.addEventListener('input', event => { for (const [selector, { component, type }] of Object.entries(componentMap)) { if ( (event.target as Element).matches(selector) && event.target instanceof type && typeof component.input === 'function' ) { component.input(event); return; } } }); document.addEventListener('keydown', event => { for (const [selector, { component, type }] of Object.entries(componentMap)) { if ( (event.target as Element).matches(selector) && event.target instanceof type && typeof component.keydown === 'function' ) { component.keydown(event); return; } } }); }; const loaded = () => { document.removeEventListener('DOMContentLoaded', loaded); activate(); }; if (document.readyState !== 'loading') { activate(); } else { document.addEventListener('DOMContentLoaded', loaded); } return { initialize: () => initializeComponents(document), cleanup : () => cleanupComponents(document) }; })();