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.
196 lines (180 loc) • 7.63 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 _checkbox, { checkboxSelector } from './components/checkbox';
import _list, { listSelector } from './components/list';
import _menu, { menuSelector } from './components/menu';
import _slider, { sliderSelector } from './components/slider';
import _textfield, { textfieldSelector, selectSelector, textareaSelector } 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 },
[checkboxSelector] : { component: _checkbox, type: HTMLInputElement },
[listSelector] : { component: _list, type: HTMLElement },
[menuSelector] : { component: _menu, type: HTMLElement },
[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 (
element.parentElement
&& !window.getComputedStyle(element.parentElement).getPropertyValue('--miclripple')
&& window.getComputedStyle(element).getPropertyValue('--miclripple')
) {
element.addEventListener('pointerdown', e => {
if ((e.currentTarget as Element).classList.contains('micl-card--nonactionable')) {
return;
}
const
x = e.clientX,
y = e.clientY,
r = element.getBoundingClientRect(),
d = Math.sqrt(Math.pow(r.width, 2) + Math.pow(r.height, 2)) * 2;
element.style.cssText = `--s:0;--o:1;`;
element.offsetTop;
element.style.cssText = `--t:1;--o:0;--d:${d};--x:${x - r.left};--y:${y - r.top}`;
});
}
});
};
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)
};
})();