mirror of
https://github.com/ae-utbm/sith.git
synced 2026-04-02 16:59:43 +00:00
Create InheritedHtmlElements interface and create an ElementOnce interface
This commit is contained in:
@@ -1,17 +1,30 @@
|
|||||||
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts";
|
import {
|
||||||
|
type InheritedHtmlElement,
|
||||||
|
inheritHtmlElement,
|
||||||
|
registerComponent,
|
||||||
|
} from "#core:utils/web-components.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ElementOnce web components
|
||||||
|
*
|
||||||
|
* Those elements ensures that their content is always included only once on a document
|
||||||
|
* They are compatible with elements that are not managed with our Web Components
|
||||||
|
**/
|
||||||
|
export interface ElementOnce<K extends keyof HTMLElementTagNameMap>
|
||||||
|
extends InheritedHtmlElement<K> {
|
||||||
|
getElementQuerySelector(): string;
|
||||||
|
refresh(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an abstract class for ElementOnce types Web Components
|
* Create an abstract class for ElementOnce types Web Components
|
||||||
*
|
|
||||||
* Those class aren't really abstract because that would be complicated with the
|
|
||||||
* multiple inheritance involved
|
|
||||||
* Instead, we just raise an unimplemented error
|
|
||||||
**/
|
**/
|
||||||
function elementOnce<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
export function elementOnce<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
||||||
return class ElementOnce extends inheritHtmlElement(tagName) {
|
abstract class ElementOnceImpl
|
||||||
getElementQuerySelector(): string {
|
extends inheritHtmlElement(tagName)
|
||||||
throw new Error("Unimplemented");
|
implements ElementOnce<K>
|
||||||
}
|
{
|
||||||
|
abstract getElementQuerySelector(): string;
|
||||||
|
|
||||||
clearNode() {
|
clearNode() {
|
||||||
while (this.firstChild) {
|
while (this.firstChild) {
|
||||||
@@ -37,7 +50,8 @@ function elementOnce<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
|||||||
// We need to manually clear the containing node to trigger the observer
|
// We need to manually clear the containing node to trigger the observer
|
||||||
this.clearNode();
|
this.clearNode();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
return ElementOnceImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set of ElementOnce type components to refresh with the observer
|
// Set of ElementOnce type components to refresh with the observer
|
||||||
@@ -47,12 +61,34 @@ const registeredComponents: Set<string> = new Set();
|
|||||||
* Helper to register ElementOnce types Web Components
|
* Helper to register ElementOnce types Web Components
|
||||||
* It's a wrapper around registerComponent that registers that component on
|
* It's a wrapper around registerComponent that registers that component on
|
||||||
* a MutationObserver that activates a refresh on them when elements are removed
|
* a MutationObserver that activates a refresh on them when elements are removed
|
||||||
|
*
|
||||||
|
* You are not supposed to unregister an element
|
||||||
**/
|
**/
|
||||||
function registerElementOnce(name: string, options?: ElementDefinitionOptions) {
|
export function registerElementOnce(name: string, options?: ElementDefinitionOptions) {
|
||||||
registeredComponents.add(name);
|
registeredComponents.add(name);
|
||||||
return registerComponent(name, options);
|
return registerComponent(name, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh all ElementOnce components on the document based on the tag name of the removed element
|
||||||
|
const refreshElement = <
|
||||||
|
T extends keyof HTMLElementTagNameMap,
|
||||||
|
K extends keyof HTMLElementTagNameMap,
|
||||||
|
>(
|
||||||
|
components: HTMLCollectionOf<ElementOnce<T>>,
|
||||||
|
removedTagName: K,
|
||||||
|
) => {
|
||||||
|
for (const element of components) {
|
||||||
|
// We can't guess if an element is compatible before we get one
|
||||||
|
// We exit the function completely if it's not compatible
|
||||||
|
if (element.inheritedTagName.toUpperCase() !== removedTagName.toUpperCase()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.refresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Since we need to pause the observer, we make an helper to start it with consistent arguments
|
||||||
const startObserver = (observer: MutationObserver) => {
|
const startObserver = (observer: MutationObserver) => {
|
||||||
observer.observe(document, {
|
observer.observe(document, {
|
||||||
// We want to also listen for elements contained in the header (eg: link)
|
// We want to also listen for elements contained in the header (eg: link)
|
||||||
@@ -61,27 +97,9 @@ const startObserver = (observer: MutationObserver) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh all ElementOnce components on the document based on their tag name
|
|
||||||
// They should all be be extended from the `elementOnce` factory
|
|
||||||
const refreshElement = (componentName: string, tagName: string) => {
|
|
||||||
for (const element of document.getElementsByTagName(componentName)) {
|
|
||||||
const node = element as unknown as {
|
|
||||||
inheritedTagName: string;
|
|
||||||
refresh(): null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// We can't guess if an element is compatible before we get one
|
|
||||||
// We exit the function completely if it's not compatible
|
|
||||||
if (node.inheritedTagName.toUpperCase() !== tagName.toUpperCase()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
node.refresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Refresh ElementOnce components when changes happens
|
// Refresh ElementOnce components when changes happens
|
||||||
const observer = new MutationObserver((mutations: MutationRecord[]) => {
|
const observer = new MutationObserver((mutations: MutationRecord[]) => {
|
||||||
|
// To avoid infinite recursion, we need to pause the observer while manipulation nodes
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
for (const node of mutation.removedNodes) {
|
for (const node of mutation.removedNodes) {
|
||||||
@@ -89,10 +107,16 @@ const observer = new MutationObserver((mutations: MutationRecord[]) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const registered of registeredComponents) {
|
for (const registered of registeredComponents) {
|
||||||
refreshElement(registered, (node as HTMLElement).tagName);
|
refreshElement(
|
||||||
|
document.getElementsByTagName(registered) as HTMLCollectionOf<
|
||||||
|
ElementOnce<"html"> // The specific tag doesn't really matter
|
||||||
|
>,
|
||||||
|
(node as HTMLElement).tagName as keyof HTMLElementTagNameMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// We then resume the observer
|
||||||
startObserver(observer);
|
startObserver(observer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,16 +19,35 @@ export function registerComponent(name: string, options?: ElementDefinitionOptio
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class used to create HTML tags inheriting from HTMLElements
|
* Safari doesn't support inheriting from HTML tags on web components
|
||||||
* You should not use this class outside of typing !
|
* The technique is to:
|
||||||
*
|
* create a new web component
|
||||||
* Please, see the `inheritHtmlElement` factory if you want to use this class.
|
* create the desired type inside
|
||||||
|
* move all attributes to the child component
|
||||||
|
* store is at as `node` inside the parent
|
||||||
**/
|
**/
|
||||||
export abstract class InheritedHtmlElement<
|
export interface InheritedHtmlElement<K extends keyof HTMLElementTagNameMap>
|
||||||
K extends keyof HTMLElementTagNameMap,
|
extends HTMLElement {
|
||||||
> extends HTMLElement {
|
readonly inheritedTagName: K;
|
||||||
abstract readonly inheritedTagName: K;
|
node: HTMLElementTagNameMap[K];
|
||||||
protected node: HTMLElementTagNameMap[K];
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator function that creates an InheritedHtmlElement compatible class
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* class MyClass extends inheritHtmlElement("select") {
|
||||||
|
* // do whatever
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
**/
|
||||||
|
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
||||||
|
return class InheritedHtmlElementImpl
|
||||||
|
extends HTMLElement
|
||||||
|
implements InheritedHtmlElement<K>
|
||||||
|
{
|
||||||
|
readonly inheritedTagName = tagName;
|
||||||
|
node: HTMLElementTagNameMap[K];
|
||||||
|
|
||||||
connectedCallback(autoAddNode?: boolean) {
|
connectedCallback(autoAddNode?: boolean) {
|
||||||
this.node = document.createElement(this.inheritedTagName);
|
this.node = document.createElement(this.inheritedTagName);
|
||||||
@@ -39,6 +58,10 @@ export abstract class InheritedHtmlElement<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We move compatible attributes to the child element
|
||||||
|
// This avoids weird inconsistencies between attributes
|
||||||
|
// when we manipulate the dom in the future
|
||||||
|
// This is especially important when using attribute based reactivity
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
this.removeAttributeNode(attr);
|
this.removeAttributeNode(attr);
|
||||||
this.node.setAttributeNode(attr);
|
this.node.setAttributeNode(attr);
|
||||||
@@ -52,26 +75,5 @@ export abstract class InheritedHtmlElement<
|
|||||||
this.appendChild(this.node);
|
this.appendChild(this.node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safari doesn't support inheriting from HTML tags on web components
|
|
||||||
* The technique is to:
|
|
||||||
* create a new web component
|
|
||||||
* create the desired type inside
|
|
||||||
* pass all attributes to the child component
|
|
||||||
* store is at as `node` inside the parent
|
|
||||||
*
|
|
||||||
* Since we can't use the generic type to instantiate the node, we create a generator function
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* class MyClass extends inheritHtmlElement("select") {
|
|
||||||
* // do whatever
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
**/
|
|
||||||
export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagName: K) {
|
|
||||||
return class InheritedHtmlElementImpl extends InheritedHtmlElement<K> {
|
|
||||||
readonly inheritedTagName = tagName;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user