Compare commits

..

4 commits

5 changed files with 160 additions and 23 deletions

View file

@ -1,25 +1,63 @@
<script type="importmap">
{
"imports": {
"nyooom/render": "https://cdn.jsdelivr.net/npm/nyooom/render.js",
"controller-registry": "./src/controller-registry.js"
}
}
</script>
<script type="module"> <script type="module">
import controllers from "./src/controller-registry.js" // Import nyooom to easily generate HTML nodes
import {html} from "https://cdn.jsdelivr.net/npm/nyooom/render.js" import {html} from "nyooom/render"
Object.defineProperty(HTMLElement.prototype, "controllers", { // The actual library
get() { return controllers.list(this) } import controllers from "controller-registry"
// Define a basic word filter controller for input elements
controllers.define("filter", (element, detach) => {
element.addEventListener("input", event => {
if (element.value.toLowerCase() === "scunthorpe") {
element.value = ""
alert("Evil word detected, deleting input permanently!")
}
}, detach) // Alternatively: {signal: detach.signal}
}) })
controllers.define("red", async (element, disconnected) => { // Define a more complex filter for form elements to enable or disable filters
const before = element.style.color controllers.define("optional-filter", async (element, detach) => {
element.style.color = "red" const checkBox = html.div(html.label(
element.addEventListener("click", () => element.controllers.remove("red"), disconnected) html.input({
await disconnected type: "checkbox",
element.style.color = before checked: true,
}) input: ({target: {checked}}) => {
element
controllers.define("asterisk", async (element, disconnected) => { .querySelectorAll("input:not([type='checkbox'])")
const asterisk = html.span("*", {controller: ["red"]}) .forEach(input => controllers.list(input).toggle("filter", checked))
element.append(asterisk) }
await disconnected }),
asterisk.remove() html.span("Enable filter")
))
element.append(checkBox)
// Detach is a promise, so we can just pause the function until
// it's time for cleanup to happen
await detach
checkBox.remove()
}) })
</script> </script>
<h1 controller="asterisk">Hello, World!</h1> <style>
[controller~="filter"] {
outline: 3px solid #82f6;
border-radius: .2em;
}
</style>
<form controller="optional-filter">
<div>
<label>
<span>Where are you from?</span>
<input controller="filter"></input>
</label>
</div>
</form>

7
jsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"target": "esnext",
"allowJs": true,
"checkJs": true
}
}

13
package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "controller-registry",
"exports": {
".": "./src/controller-registry.js",
},
"type": "module",
"license": "MIT",
"version": "0.0.1",
"url": "https://git.but.gay/darkwiiplayer/controller-registry",
"scripts": {
"definitions": "tsc src/*.js --declaration --allowJs --emitDeclarationOnly"
}
}

48
src/controller-registry.d.ts vendored Normal file
View file

@ -0,0 +1,48 @@
export class ControllerList {
/**
* @param {HTMLElement} element
* @param {String} attribute
* */
constructor(element: HTMLElement, attribute?: string);
/** @param {String} name */
contains(name: string): any;
/** @param {String} name */
add(name: string): void;
/** @param {String} name */
remove(name: string): void;
/**
* @param {String} name
* @param {String} replacement
*/
replace(name: string, replacement: string): boolean;
/**
* @param {String} name
* @param {Boolean} force
*/
toggle(name: string, force: boolean): void;
#private;
}
export class ControllerRegistry {
/** @typedef {HTMLElement} Root */
/**
* @param {Root} root
* @param {String} attribute
*/
constructor(root: HTMLElement, attribute?: string);
/**
* @param {Root} root
*/
upgrade(root: HTMLElement): void;
/**
* @param {String} name
* @param {Function} callback
*/
define(name: string, callback: Function): void;
get(name: any): any;
list(element: any): any;
getName(controller: any): void;
whenDefined(name: any): void;
#private;
}
declare const _default: ControllerRegistry;
export default _default;

View file

@ -22,18 +22,25 @@ export class ControllerList {
this.#element.setAttribute(this.#attribute, [...set].join(" ")) this.#element.setAttribute(this.#attribute, [...set].join(" "))
} }
/** @param {String} name */
contains(name) { contains(name) {
return this.#set.has(name) return this.#set.has(name)
} }
/** @param {String} name */
add(name) { add(name) {
this.toggle(name, true) this.toggle(name, true)
} }
/** @param {String} name */
remove(name) { remove(name) {
this.toggle(name, false) this.toggle(name, false)
} }
/**
* @param {String} name
* @param {String} replacement
*/
replace(name, replacement) { replace(name, replacement) {
const set = this.#set const set = this.#set
if (set.has(name)) { if (set.has(name)) {
@ -46,6 +53,10 @@ export class ControllerList {
} }
} }
/**
* @param {String} name
* @param {Boolean} force
*/
toggle(name, force) { toggle(name, force) {
const set = this.#set const set = this.#set
if (force === true) { if (force === true) {
@ -83,29 +94,42 @@ export class ControllerRegistry {
/** @type {WeakMap<HTMLElement,Map<String,Object>>} */ /** @type {WeakMap<HTMLElement,Map<String,Object>>} */
#attached = new WeakMap() #attached = new WeakMap()
/** @type {Map<String,Set<HTMLElement>} */ /** @type {Map<String,Set<HTMLElement>>} */
#waiting = new Map() #waiting = new Map()
/** @type {Map<Strong,(HTMLElement, AbortSignal)=>void>} */ /** @type {Map<String,(element: HTMLElement, signal: AbortSignal)=>void>} */
#defined = new Map() #defined = new Map()
#attribute #attribute
/** @typedef {HTMLElement} Root */
/**
* @param {Root} root
* @param {String} attribute
*/
constructor(root, attribute="controller") { constructor(root, attribute="controller") {
this.#attribute = attribute this.#attribute = attribute
this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false}) this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false})
this.upgrade(root) this.upgrade(root)
} }
/**
* @param {Root} root
*/
upgrade(root) { upgrade(root) {
root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element)) root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element))
} }
/**
* @param {String} name
* @param {Function} callback
*/
define(name, callback) { define(name, callback) {
if (("function" == typeof callback) && callback.prototype) { if (("function" == typeof callback) && callback.prototype) {
callback = async (element, disconnected) => { callback = async (element, disconnected) => {
const {proxy, revoke} = Proxy.reevocable(element) const {proxy, revoke} = Proxy.revocable(element, {})
const controller = new callback(proxy) const controller = new callback(proxy, disconnected)
await disconnected await disconnected
revoke() revoke()
if ("detach" in controller) controller.detach(element) if ("detach" in controller) controller.detach(element)
@ -126,8 +150,15 @@ export class ControllerRegistry {
return this.#defined.get(name) return this.#defined.get(name)
} }
#listMap = new WeakMap()
list(element) { list(element) {
return new ControllerList(element, this.#attribute) if (this.#listMap.has(element)) {
return this.#listMap.get(element)
} else {
const list = new ControllerList(element, this.#attribute)
this.#listMap.set(element, list)
return list
}
} }
getName(controller) { getName(controller) {