Fix breaklines in character-config and fix DOM reactivity with children updates.

This commit is contained in:
Clément Pons 2025-12-09 17:45:29 +01:00
parent 97578132bb
commit 1b0b9ca7f4
5 changed files with 11623 additions and 31 deletions

BIN
db.sqlite

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1757,8 +1757,8 @@ export class CharacterSheet
return [ return [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row justify-end items-center gap-8', [ div('flex flex-row justify-end items-center gap-8', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': weight > character.itempower }], text: () => `Poids total: ${weight}/${character.itempower}` }), dom('span', { class: ['italic text-sm', () => ({ 'text-light-red dark:text-dark-red': weight > character.itempower })], text: () => `Poids total: ${weight}/${character.itempower}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) }], text: () => `Puissance magique: ${power}/${character.capacity}` }), dom('span', { class: ['italic text-sm', () => ({ 'text-light-red dark:text-dark-red': power > (character.capacity === false ? 0 : character.capacity) })], text: () => `Puissance magique: ${power}/${character.capacity}` }),
button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'), button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'),
]), ]),
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, redraw: true, render: e => { div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, redraw: true, render: e => {
@ -1768,21 +1768,31 @@ export class CharacterSheet
const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]); const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]);
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]); const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
return foldable(() => [ return foldable(() => [
markdown(getText(item.description)), markdown(getText(item.description)),
div('flex flex-row justify-center', [ div('flex flex-row justify-center gap-1', [
this.character?.character.campaign ? button(text('Partager'), () => { this.character?.character.campaign ? button(text('Partager'), () => {
}, 'p-1') : undefined, }, 'px-2 text-sm h-5 box-content') : undefined,
button(icon('radix-icons:trash'), () => { button(icon('radix-icons:minus', { width: 12, height: 12 }), () => {
const idx = items.findIndex(_e => _e === e); const idx = items.findIndex(_e => _e === e);
if(idx === -1) return; if(idx === -1) return;
items[idx]!.amount--; items[idx]!.amount--;
if(items[idx]!.amount >= 0) items.splice(idx, 1); if(items[idx]!.amount <= 0) items.splice(idx, 1);
else (items as DOMList<ItemState>)?.render();
this.character!.variable('items', items); this.character!.variable('items', items);
}, 'p-1'), }, 'p-1'),
button(icon('radix-icons:plus', { width: 12, height: 12 }), () => {
const idx = items.findIndex(_e => _e === e);
if(idx === -1) return;
if(item.equippable) items.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
else if(items.find(_e => _e === e)) { items.find(_e => _e === e)!.amount++; (items as DOMList<ItemState>)?.render(); }
else items.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
this.character!.variable('items', items);
}, 'p-1'),
]) ], [div('flex flex-row justify-between', [ ]) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [ div('flex flex-row items-center gap-4', [
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => { item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
@ -1794,10 +1804,10 @@ export class CharacterSheet
]), ]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [ div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price, e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', e.amount?.toString() ?? '-') ]), div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => e.amount?.toString() ?? '-') ]),
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]), div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]),
e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight, e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight,
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.charge ? `${item.charge}` : '-') ]), div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.charge ? `${item.charge}` : '-') ]),
]), ]),
])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col' } }) ])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col' } })
}}) }})

View File

@ -446,7 +446,7 @@ export function foldable(content: NodeChildren | (() => NodeChildren), title: No
} }
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]); const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
const fold = div(['group flex w-full flex-col', settings?.class?.container], [ const fold = div(['group flex w-full flex-col', settings?.class?.container], [
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]), div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]),
contentContainer contentContainer
]); ]);
display(settings?.open ?? true); display(settings?.open ?? true);

View File

@ -1,7 +1,7 @@
import { iconLoaded, loadIcon } from 'iconify-icon'; import { iconLoaded, loadIcon } from 'iconify-icon';
export type RedrawableHTML<T extends keyof HTMLElementTagNameMap> = HTMLElementTagNameMap[T] & { update: (recursive: boolean) => void } export type RedrawableHTML<T extends keyof HTMLElementTagNameMap> = HTMLElementTagNameMap[T] & { update: (recursive: boolean) => void }
export type Node = RedrawableHTML<any> & { update: (recursive: boolean) => void } | SVGElement | Text | undefined; export type Node<T extends keyof HTMLElementTagNameMap = any> = RedrawableHTML<T> & { update: (recursive: boolean) => void } | SVGElement | Text | undefined;
export type NodeChildren = Array<Node> | undefined; export type NodeChildren = Array<Node> | undefined;
export type Class = Reactive<string | Array<Class> | Record<string, boolean> | undefined>; export type Class = Reactive<string | Array<Class> | Record<string, boolean> | undefined>;
@ -26,6 +26,21 @@ export interface NodeProperties
}; };
} }
let defered = false, _set = new Set<() => void>();
const _defer = (fn: () => void) => {
if(!defered)
{
defered = true;
queueMicrotask(() => {
_set.forEach(e => e());
_set.clear();
defered = false;
});
}
_set.add(fn);
}
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation(); export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): RedrawableHTML<T>; export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): RedrawableHTML<T>;
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<T> & { array?: DOMList<U> }; export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): RedrawableHTML<T> & { array?: DOMList<U> };
@ -41,7 +56,6 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
if(children !== undefined && (setup || recursive)) if(children !== undefined && (setup || recursive))
{ {
element.replaceChildren();
if(Array.isArray(children)) if(Array.isArray(children))
{ {
for(const c of children) for(const c of children)
@ -49,43 +63,40 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
if(c !== undefined) if(c !== undefined)
{ {
element.appendChild(c); element.appendChild(c);
recursive && 'update' in c && c.update(true); recursive && 'update' in c && _defer(() => c.update(true));
} }
} }
} }
else if(children.list !== undefined) else if(children.list !== undefined)
{ {
if(setup || recursive)
{
_cache.clear();
children.list.forEach(e => _cache.set(e, children.render(e)));
}
if(setup) if(setup)
{ {
children.list.forEach(e => _cache.set(e, children.render(e)));
const _push = children.list.push; const _push = children.list.push;
children.list.push = (...items: U[]) => { children.list.push = (...items: U[]) => {
items.forEach(e => { items.forEach(e => {
const dom = children.render(e); if(!_cache.has(e)) _cache.set(e, element.appendChild(children.render(e)));
_cache.set(e, dom); else element.appendChild(_cache.get(e));
dom && element.appendChild(dom);
}); });
if(children.redraw) update(false); if(children.redraw) _defer(() => update(false));
return _push.bind(children.list)(...items); return _push.bind(children.list)(...items);
}; };
const _splice = children.list.splice; const _splice = children.list.splice;
children.list.splice = (start: number, deleteCount: number, ...items: U[]) => { children.list.splice = (start: number, deleteCount: number, ...items: U[]) => {
const list = _splice.bind(children.list)(start, deleteCount, ...items); const list = _splice.bind(children.list)(start, deleteCount, ...items);
list.forEach(e => _cache.get(e)?.remove() || _cache.delete(e)); list.forEach(e => { if(!children.list!.find(_e => _e === e)) _cache.delete(e); });
if(children.redraw) update(false); element.array!.render();
return list; return list;
}; };
} }
else if(recursive)
_cache.forEach((v, k) => v && 'update' in v && v.update(true));
element.array = children.list as DOMList<U>; element.array = children.list as DOMList<U>;
element.array.render = (redraw?: boolean) => { element.array.render = (redraw?: boolean) => {
element.replaceChildren(...children.list?.map(e => _cache.get(e)).filter(e => !!e) ?? []); element.replaceChildren(...children.list?.map(e => _cache.get(e)).filter(e => !!e) ?? []);
if((redraw !== undefined || children.redraw !== undefined) && !updating) update(redraw ?? children.redraw!); if((redraw !== undefined || children.redraw !== undefined) && !updating) _defer(() => update(redraw ?? children.redraw!));
} }
element.array.render(); element.array.render();
@ -113,7 +124,7 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
element.appendChild(text as Text); element.appendChild(text as Text);
} }
if(properties?.listeners) if(properties?.listeners && setup)
{ {
for(let [k, v] of Object.entries(properties.listeners)) for(let [k, v] of Object.entries(properties.listeners))
{ {
@ -264,7 +275,7 @@ export function text(data: any, _txt?: Reactive<string>): Text
} }
else return document.createTextNode(''); else return document.createTextNode('');
} }
export function styling(element: SVGElement | RedrawableHTML<any>, properties: { export function styling(element: SVGElement | RedrawableHTML<keyof HTMLElementTagNameMap>, properties: {
class?: Class; class?: Class;
style?: Reactive<Record<string, string | undefined | boolean | number> | string>; style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
}): SVGElement | HTMLElement }): SVGElement | HTMLElement