Created
July 25, 2024 14:50
-
-
Save petsel/e7329bf34df08d38d38e1d9303ffa90c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
// 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