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
text/typescript
//
// 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)
};
})();