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">
import controllers from "./src/controller-registry.js"
import {html} from "https://cdn.jsdelivr.net/npm/nyooom/render.js"
// Import nyooom to easily generate HTML nodes
import {html} from "nyooom/render"
Object.defineProperty(HTMLElement.prototype, "controllers", {
get() { return controllers.list(this) }
// The actual library
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) => {
const before = element.style.color
element.style.color = "red"
element.addEventListener("click", () => element.controllers.remove("red"), disconnected)
await disconnected
element.style.color = before
})
controllers.define("asterisk", async (element, disconnected) => {
const asterisk = html.span("*", {controller: ["red"]})
element.append(asterisk)
await disconnected
asterisk.remove()
// Define a more complex filter for form elements to enable or disable filters
controllers.define("optional-filter", async (element, detach) => {
const checkBox = html.div(html.label(
html.input({
type: "checkbox",
checked: true,
input: ({target: {checked}}) => {
element
.querySelectorAll("input:not([type='checkbox'])")
.forEach(input => controllers.list(input).toggle("filter", checked))
}
}),
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>
<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(" "))
}
/** @param {String} name */
contains(name) {
return this.#set.has(name)
}
/** @param {String} name */
add(name) {
this.toggle(name, true)
}
/** @param {String} name */
remove(name) {
this.toggle(name, false)
}
/**
* @param {String} name
* @param {String} replacement
*/
replace(name, replacement) {
const set = this.#set
if (set.has(name)) {
@ -46,6 +53,10 @@ export class ControllerList {
}
}
/**
* @param {String} name
* @param {Boolean} force
*/
toggle(name, force) {
const set = this.#set
if (force === true) {
@ -83,29 +94,42 @@ export class ControllerRegistry {
/** @type {WeakMap<HTMLElement,Map<String,Object>>} */
#attached = new WeakMap()
/** @type {Map<String,Set<HTMLElement>} */
/** @type {Map<String,Set<HTMLElement>>} */
#waiting = new Map()
/** @type {Map<Strong,(HTMLElement, AbortSignal)=>void>} */
/** @type {Map<String,(element: HTMLElement, signal: AbortSignal)=>void>} */
#defined = new Map()
#attribute
/** @typedef {HTMLElement} Root */
/**
* @param {Root} root
* @param {String} attribute
*/
constructor(root, attribute="controller") {
this.#attribute = attribute
this.#observer.observe(root, {subtree: true, childList: true, attributes: true, attributeFilter: [attribute], attributeOldValue: false})
this.upgrade(root)
}
/**
* @param {Root} root
*/
upgrade(root) {
root.querySelectorAll(`[${this.#attribute}]`).forEach(element => this.#update(element))
}
/**
* @param {String} name
* @param {Function} callback
*/
define(name, callback) {
if (("function" == typeof callback) && callback.prototype) {
callback = async (element, disconnected) => {
const {proxy, revoke} = Proxy.reevocable(element)
const controller = new callback(proxy)
const {proxy, revoke} = Proxy.revocable(element, {})
const controller = new callback(proxy, disconnected)
await disconnected
revoke()
if ("detach" in controller) controller.detach(element)
@ -126,8 +150,15 @@ export class ControllerRegistry {
return this.#defined.get(name)
}
#listMap = new WeakMap()
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) {