demarches-normaliennes/app/javascript/controllers/turbo_event_controller.ts

135 lines
3.4 KiB
TypeScript
Raw Normal View History

2022-04-14 20:46:28 +02:00
import invariant from 'tiny-invariant';
import { z } from 'zod';
2022-05-09 18:48:52 +02:00
import morphdom from 'morphdom';
2022-04-14 20:46:28 +02:00
import { ApplicationController, Detail } from './application_controller';
2022-04-14 20:46:28 +02:00
export class TurboEventController extends ApplicationController {
2022-04-14 20:46:28 +02:00
static values = {
type: String,
detail: Object
};
declare readonly typeValue: string;
declare readonly detailValue: Detail;
connect(): void {
this.globalDispatch(this.typeValue, this.detailValue);
this.element.remove();
}
}
2022-05-09 18:48:52 +02:00
const MutationAction = z.enum([
'show',
'hide',
'focus',
'enable',
'disable',
'morph'
]);
2022-04-14 20:46:28 +02:00
type MutationAction = z.infer<typeof MutationAction>;
const Mutation = z.union([
z.object({
action: MutationAction,
delay: z.number().optional(),
2022-05-09 18:48:52 +02:00
target: z.string(),
html: z.string().optional()
2022-04-14 20:46:28 +02:00
}),
z.object({
action: MutationAction,
delay: z.number().optional(),
2022-05-09 18:48:52 +02:00
targets: z.string(),
html: z.string().optional()
2022-04-14 20:46:28 +02:00
})
]);
type Mutation = z.infer<typeof Mutation>;
addEventListener('dom:mutation', (event) => {
const detail = (event as CustomEvent).detail;
const mutation = Mutation.parse(detail);
mutate(mutation);
});
const Mutations: Record<MutationAction, (mutation: Mutation) => void> = {
hide: (mutation) => {
for (const element of findElements(mutation)) {
element.classList.add('hidden');
}
},
show: (mutation) => {
for (const element of findElements(mutation)) {
element.classList.remove('hidden');
}
},
focus: (mutation) => {
for (const element of findElements(mutation)) {
element.focus();
}
},
disable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = true;
}
},
enable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = false;
}
2022-05-09 18:48:52 +02:00
},
morph: (mutation) => {
invariant(mutation.html, 'morph action requires html');
for (const element of findElements<HTMLInputElement>(mutation)) {
morphdom(element, mutation.html, {
onBeforeElUpdated(fromEl, toEl) {
if (isTouchedInput(fromEl)) {
fromEl.removeAttribute('data-touched');
mergeInputValue(
fromEl as HTMLInputElement,
toEl as HTMLInputElement
);
}
if (fromEl.isEqualNode(toEl)) {
return false;
}
return true;
}
});
}
2022-04-14 20:46:28 +02:00
}
};
2022-05-09 18:48:52 +02:00
function mergeInputValue(fromEl: HTMLInputElement, toEl: HTMLInputElement) {
toEl.value = fromEl.value;
toEl.checked = fromEl.checked;
}
function isTouchedInput(element: HTMLElement): boolean {
return (
['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName) &&
!!element.getAttribute('data-touched')
);
}
2022-04-14 20:46:28 +02:00
function mutate(mutation: Mutation) {
const fn = Mutations[mutation.action];
invariant(fn, `Could not find mutation ${mutation.action}`);
if (mutation.delay) {
setTimeout(() => fn(mutation), mutation.delay);
} else {
fn(mutation);
}
}
function findElements<Element extends HTMLElement = HTMLElement>(
mutation: Mutation
): Element[] {
if ('target' in mutation) {
const element = document.querySelector<Element>(`#${mutation.target}`);
invariant(element, `Could not find element with id ${mutation.target}`);
return [element];
} else if ('targets' in mutation) {
return [...document.querySelectorAll<Element>(mutation.targets)];
}
invariant(false, 'Could not find element');
}