Skip to content

Instantly share code, notes, and snippets.

@petsel
Created July 25, 2024 14:50
Show Gist options
  • Save petsel/e7329bf34df08d38d38e1d9303ffa90c to your computer and use it in GitHub Desktop.
Save petsel/e7329bf34df08d38d38e1d9303ffa90c to your computer and use it in GitHub Desktop.
<!--
// https://stackoverflow.com/questions/78105670/cutom-elements-cant-extend-from-button-illegal-constructor/78106993
//-->
<script>
(function (globalThis) {
// scope of `dom-safari` or `custom-element-safari` module
'use strict';
const { navigator, customElements } = globalThis;
function isUserAgentSafari(userAgent) {
return (
(/AppleWebKit\/(?<version>[\d.]+).*Safari\/\k<version>/).test(userAgent) &&
!(/(?:Firefox|Chrome|Opera)\//).test(userAgent)
);
}
// GUARD.
if (!isUserAgentSafari(globalThis.navigator.userAgent)) {
return;
}
function isFunction(value) {
return (
typeof value === 'function' &&
typeof value.call === 'function' &&
typeof value.apply === 'function'
);
}
function isHTMLElementSubType(value) {
return (
isFunction(value) &&
(/function\s+HTML.+Element\(\s*\)\s*\{[^\}]+\}/)
.test(
// - prove against spoofed function names via e.g.
//
// ```
// Reflect.defineProperty(HTMLAreaElement, 'name', {
// ... Reflect.getOwnPropertyDescriptor(HTMLAreaElement, 'name'),
// value: 'FooElement',
// });
// ```
Function.prototype.toString.call(value)
)
);
}
function isCustomElementInNeedOfProxy(constructor) {
return isHTMLElementSubType(
Object.getPrototypeOf(constructor)
);
}
function handleCustomElementInstantiation(/* target, args, newTarget */...args) {
return Reflect.construct(/* target, args, newTarget */...args)
}
function createCustomElementProxy(constructor) {
// console.log(
// 'createCustomElementProxy :: before ... prototype ...',
// Object.getPrototypeOf(constructor),
// );
Object.setPrototypeOf(constructor, HTMLElement);
// console.log(
// 'createCustomElementProxy :: after ... prototype ...',
// Object.getPrototypeOf(constructor),
// );
return new Proxy(constructor, { construct: handleCustomElementInstantiation });
}
customElements.define = ((target, proceed, proxyfy, isInNeedOfProxy) => function define (name, constructor, options) {
'use strict';
const { extends: _, ...optionsRest } = options;
return isInNeedOfProxy(constructor)
? proceed.call(target, name, proxyfy(constructor), optionsRest)
: proceed.call(target, name, constructor, options);
})(customElements, customElements.define, createCustomElementProxy, isCustomElementInNeedOfProxy);
const ariaRoleMap = new Map([
['[role="button"], input[type="button"], button', 'button'],
['[role="radio"][aria-checked], input[type="radio"]', 'radio'],
['[role="checkbox"][aria-checked], input[type="checkbox"]', 'checkbox'],
['[role="menuitem"], li', 'menuitem'],
['[role="menuitemradio"][aria-checked], li[aria-checked][role="radio"]', 'menuitemradio'],
['[role="menuitemcheckbox"][aria-checked], li[aria-checked]:not([role="radio"])', 'menuitemcheckbox'],
['[role="option"][aria-selected], [aria-selected], option', 'option'],
['[role="progressbar"], input[type="range"]', 'progressbar'],
['[role="link"], a[href]', 'link'],
['[role="gridcell"], td', 'gridcell'],
// etc. ... see ... [https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques]
]);
function matchAriaRole(node) {
let result = '';
[...ariaRoleMap]
.some(([selector, role]) => {
const isMatch = node.matches(selector);
if (isMatch) {
result = role;
}
return isMatch;
});
return result;
}
function replaceWithCustomElementNode(cbieNode) {
// `cbieNode` ... customized built-in element node.
console.log({ cbieNode });
// `ceNode` ... custom element node.
const ceNode = document.createElement(cbieNode.getAttribute('is'));
const ariaRole = matchAriaRole(cbieNode);
if (ariaRole) {
ceNode.setAttribute('role', ariaRole);
}
cbieNode
.getAttributeNames()
.filter(name => name !== 'is')
.forEach(name => ceNode.setAttribute(name, cbieNode.getAttribute(name)));
cbieNode
.childNodes
.forEach(node => ceNode.appendChild(node.cloneNode(true)));
cbieNode.replaceWith(ceNode);
}
function handleCustomizedBuiltInElementMutation(mutationsList/*, observer*/) {
mutationsList
.reduce((result, { type, addedNodes }) =>
((type === 'childList') && result.concat(
[...addedNodes].filter(node =>
node.nodeType === Node.ELEMENT_NODE &&
node.hasAttribute('is')
)
) || []), []
)
.forEach(replaceWithCustomElementNode);
}
new MutationObserver(
handleCustomizedBuiltInElementMutation
)
.observe(
document.documentElement, {
// attributes: true,
childList: true,
subtree: true,
},
);
document
.addEventListener('DOMContentLoaded', () =>
document
.documentElement
.querySelectorAll('[is]')
.forEach(replaceWithCustomElementNode)
);
}(globalThis || window || this));
</script>
<button is="my-button" id="foo">Click Me!</button>
<button is="my-button" id="bar">Click Me!</button>
<my-button role="button" id="baz">Click Me!</my-button>
<script>
customElements.define("my-button",
class extends HTMLButtonElement {
connectedCallback(){
console.log("connected", this.id);
this.onclick = evt => console.log("You clicked", this.id);
}
}, { extends: 'button'});
</script>
<styl>
[role="button"] {
appearance: button;
-webkit-appearance: button;
display: inline-block;
box-sizing: border-box;
margin-top: 0;
padding-block-start: 2px;
padding-block-end: 3px;
padding-inline-start: 6px;
border-top-width: 2px;
border-right-width: 2px;
border-bottom-width: 2px;
border-left-width: 2px;
border-top-style: outset;
border-right-style: outset;
border-bottom-style: outset;
border-left-style: outset;
border-top-color: buttonface;
border-right-color: buttonface;
border-bottom-color: buttonface;
border-left-color: buttonface;
background-color: buttonface;
color: buttontext;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0;
text-shadow: none;
align-items: flex-start;
text-align: center;
cursor: default;
}
</style>
<!--
// https://stackoverflow.com/questions/78105670/cutom-elements-cant-extend-from-button-illegal-constructor/78106993
//-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment