Compare commits

..

No commits in common. "78a101b79ddcd6ae1ce1f9323c4abda86950a755" and "94645f9dbfac06889acd7e6cdbaaf821b892baf0" have entirely different histories.

12 changed files with 413 additions and 562 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -11,7 +11,6 @@ import { getText } from "#shared/i18n";
import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor.util";
import { Socket } from "#shared/websocket.util";
import { raw, reactive, reactivity, type Reactive } from '#shared/reactive';
const config = characterConfig as CharacterConfig;
@ -296,8 +295,8 @@ export class CharacterCompiler
set character(value: Character)
{
this._character = reactive(value);
this._result = reactive(defaultCompiledCharacter(value));
this._character = value;
this._result = defaultCompiledCharacter(value);
this._buffer = {
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
@ -307,11 +306,6 @@ export class CharacterCompiler
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
};
reactivity(() => this.character.variables, () => {
console.log("Saving variables");
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => this.saveVariables(), 2000);
})
if(value.people !== undefined)
{
@ -360,14 +354,30 @@ export class CharacterCompiler
return substring;
})
}
variable<T extends keyof CharacterVariables>(prop: T, value: CharacterVariables[T], autosave: boolean = true)
{
this._character.variables[prop] = value;
this._result.variables[prop] = value;
this._variableDirty = true;
if(autosave)
{
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => this.saveVariables(), 2000);
}
}
saveVariables()
{
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
method: 'POST',
body: raw(this._character.variables),
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
})
if(this._variableDirty)
{
this._variableDirty = false;
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
method: 'POST',
body: this._character.variables,
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
})
}
}
saveNotes()
{
@ -1318,7 +1328,7 @@ export class CharacterSheet
});
})
this.ws.handleMessage<{ action: 'set' | 'add' | 'remove', key: keyof CharacterVariables, value: any }>('VARIABLE', (variable) => {
/* const prop = this.character?.character.variables[variable.key];
const prop = this.character?.character.variables[variable.key];
if(variable.action === 'set')
this.character?.variable(variable.key, variable.value, false);
else if(Array.isArray(prop))
@ -1331,7 +1341,7 @@ export class CharacterSheet
if(idx !== -1) prop.splice(idx, 1);
}
this.character?.variable(variable.key, prop, false);
} */
}
})
}
@ -1374,15 +1384,19 @@ export class CharacterSheet
privateNotes.content = this.character!.character.notes!.private!;
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: RedrawableHTML }) => {
character.variables[property] = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
obj.edit.value = (character[property] - character.variables[property]).toString();
const value = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
this.character?.variable(property, clamp(isNaN(value) ? character.variables[property] : value, 0, Infinity));
this.character?.saveVariables();
obj.edit.value = (character[property] - this.character!.character.variables[property]).toString();
obj.readonly.textContent = (character[property] - character.variables[property]).toString();
obj.edit.replaceWith(obj.readonly);
};
const health = {
readonly: dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${character.health - character.variables.health}`,
text: `${character.health - character.variables.health}`,
listeners: { click: () => { health.readonly.replaceWith(health.edit); health.edit.select(); health.edit.focus(); } },
}),
edit: input('text', { defaultValue: (character.health - character.variables.health).toString(), input: (v) => {
@ -1393,7 +1407,7 @@ export class CharacterSheet
const mana = {
readonly: dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${character.mana - character.variables.mana}`,
text: `${character.mana - character.variables.mana}`,
listeners: { click: () => { mana.readonly.replaceWith(mana.edit); mana.edit.select(); mana.edit.focus(); } },
}),
edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => {
@ -1645,52 +1659,50 @@ export class CharacterSheet
}
spellTab(character: CompiledCharacter)
{
const preference = reactive({
sort: localStorage.getItem('character-sort') ?? 'rank',
} as { sort: 'rank' | 'type' | 'element' });
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
const sort = (spells: string[]) => {
localStorage.setItem('character-sort', preference.sort);
switch(preference.sort)
const sort = () => {
switch(sortPreference)
{
case 'rank': return spells.sort((a, b) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0) || SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
case 'type': return spells.sort((a, b) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!) || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
default: return spells;
case 'rank': return container.array.sort((a, b) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0) || SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
case 'type': return container.array.sort((a, b) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
case 'element': return container.array.sort((a, b) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!) || (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
default: return container.array;
}
};
const container = div('flex flex-col gap-2', { render: e => {
const spell = config.spells[e];
if(!spell)
return;
return div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
div('flex flex-row gap-2', [ span('flex flex-row', spell.rank === 4 ? 'Sort unique' : `Sort ${spell.type === 'instinct' ? 'd\'instinct' : spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${spell.rank}`), ...(spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-4 items-center', [ spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof spell.range === 'number' && spell.range > 0 ? `${spell.range} case${spell.range > 1 ? 's' : ''}` : spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof spell.speed === 'number' ? `${spell.speed} minute${spell.speed > 1 ? 's' : ''}` : spell.speed) ])
]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
])
}, list: [...(character.lists.spells ?? []), ...character.variables.spells] });
sort().render();
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-2 items-center', [
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: preference.sort as 'rank' | 'type' | 'element', class: { option: 'px-2 py-1 text-sm' }, onChange: (v) => { preference.sort = v; } }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; sort().render(); } }),
]),
div('flex flex-row gap-2 items-center', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length + (character.lists.spells?.length ?? 0) !== character.spellslots }], text: () => `${character.variables.spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', character.variables.spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '') }),
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'),
])
]),
div('flex flex-col gap-2', { render: e => {
const spell = config.spells[e];
if(!spell)
return;
return div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
div('flex flex-row gap-2', [ span('flex flex-row', `Sort ${spell.type === 'instinct' ? 'd\'instinct' : spell.type === 'knowledge' ? 'de savoir' : 'de précision'} ${spell.rank === 4 ? 'unique' :`de rang ${spell.rank}`}`), ...(spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-4 items-center', [ spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof spell.range === 'number' && spell.range > 0 ? `${spell.range} case${spell.range > 1 ? 's' : ''}` : spell.range === 0 ? 'toucher' : 'personnel'), span(undefined, typeof spell.speed === 'number' ? `${spell.speed} minute${spell.speed > 1 ? 's' : ''}` : spell.speed) ])
]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
])
}, list: () => sort([...(character.lists.spells ?? []), ...character.variables.spells]) }),
container,
])
]
}
//TODO: Update to handle reactivity
spellPanel(character: CompiledCharacter)
{
const availableSpells = Object.values(config.spells).filter(spell => {
@ -1698,48 +1710,61 @@ export class CharacterSheet
if (character.spellranks[spell.type] < spell.rank) return false;
return true;
});
const spells = character.variables.spells;
const textAmount = text(character.variables.spells.length.toString()), textMax = text(character.spellslots.toString());
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }),
div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ text(() => `${spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '')) ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ])
]),
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', { list: availableSpells, render: (spell) => foldable(() => [
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => {
let state = character.lists.spells?.includes(spell.id) ? 'given' : character.variables.spells.includes(spell.id) ? 'choosen' : 'empty';
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
if(state === 'choosen')
{
this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id));
state = 'empty';
}
else if(state === 'empty')
{
this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
state = 'choosen';
}
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
textAmount.textContent = character.variables.spells.length.toString();
}, "px-2 py-1 text-sm font-normal");
toggleButton.disabled = state === 'given';
return foldable(() => [
markdown(spell.description),
], [ div("flex flex-row justify-between gap-2", [
dom("span", { class: "text-lg font-bold", text: spell.name }),
div("flex flex-row items-center gap-6", [
div("flex flex-row text-sm gap-2",
spell.elements.map(el =>
dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class],
text: elementTexts[el].text
})
)
),
div("flex flex-row text-sm gap-1", [
...(spell.rank !== 4 ? [
dom("span", { text: `Rang ${spell.rank}` }),
dom("span", { class: "text-lg font-bold", text: spell.name }),
div("flex flex-row items-center gap-6", [
div("flex flex-row text-sm gap-2",
spell.elements.map(el =>
dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class],
text: elementTexts[el].text
})
)
),
div("flex flex-row text-sm gap-1", [
...(spell.rank !== 4 ? [
dom("span", { text: `Rang ${spell.rank}` }),
text("/"),
dom("span", { text: spellTypeTexts[spell.type] }),
text("/")
] : []),
dom("span", { text: `${spell.cost} mana` }),
text("/"),
dom("span", { text: spellTypeTexts[spell.type] }),
text("/")
] : []),
dom("span", { text: `${spell.cost} mana` }),
text("/"),
dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
]),
toggleButton,
]),
button(text(() => spells.includes(spell.id) ? 'Supprimer' : character.lists.spells?.includes(spell.id) ? 'Inné' : 'Ajouter'), () => {
const idx = spells.findIndex(e => e === spell.id);
if(idx !== -1) spells.splice(idx, 1);
else spells.push(spell.id);
}, "px-2 py-1 text-sm font-normal"),
]),
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } })
})
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
}))
]);
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
@ -1747,17 +1772,17 @@ export class CharacterSheet
itemsTab(character: CompiledCharacter)
{
const items = character.variables.items;
const power = () => items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
const weight = () => items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
const power = items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
const weight = items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
return [
div('flex flex-col gap-2', [
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': 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': 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}` }),
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, 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 => {
const item = config.items[e.id];
if(!item) return;
@ -1776,19 +1801,25 @@ export class CharacterSheet
items[idx]!.amount--;
if(items[idx]!.amount <= 0) items.splice(idx, 1);
else (items as DOMList<ItemState>)?.render();
this.character!.variable('items', items);
}, '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++;
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 items-center gap-4', [
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = v;
this.character!.variable('items', items);
}, class: { container: '!w-5 !h-5' } }) : undefined,
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
]),
@ -1806,13 +1837,41 @@ export class CharacterSheet
}
itemsPanel(character: CompiledCharacter)
{
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = reactive({
const items = Object.values(config.items).map(item => ({ item, dom: foldable(() => [ markdown(getText(item.description)) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
div('flex flex-row 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 w-16 gap-2 justify-between items-center px-2', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.weight?.toString() ?? '-') ]),
div('flex flex-row 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 w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = this.character!.character.variables.items;
if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
else if(list.find(e => e.id === item.id)) list.find(e => e.id === item.id)!.amount++;
else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
(list as DOMList<ItemState>)?.render();
this.character!.variable('items', list);
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) }));
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = {
category: [],
rarity: [],
name: '',
power: { min: 0, max: Infinity },
});
};
const applyFilters = () => {
content.replaceChildren(...items.filter(e =>
(filters.category.length === 0 || filters.category.includes(e.item.category)) &&
(filters.rarity.length === 0 || filters.rarity.includes(e.item.rarity)) &&
(filters.name === '' || e.item.name.toLowerCase().includes(filters.name.toLowerCase()))
).map(e => e.dom));
}
const content = div('grid grid-cols-1 -my-2 overflow-y-auto gap-1');
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Gestion de l'inventaire" }),
@ -1822,39 +1881,13 @@ export class CharacterSheet
}, "p-1"), "Fermer", "left") ])
]),
div('flex flex-row items-center gap-4', [
div('flex flex-row gap-2 items-center', [ text('Catégorie'), multiselect(Object.keys(categoryText).map(e => ({ text: categoryText[e as Category], value: e as Category })), { defaultValue: filters.category, change: v => filters.category = v, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Rareté'), multiselect(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: filters.rarity, change: v => filters.rarity = v, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Nom'), input('text', { defaultValue: filters.name, input: v => { filters.name = v; }, class: 'w-64' }) ]),
div('flex flex-row gap-2 items-center', [ text('Catégorie'), multiselect(Object.keys(categoryText).map(e => ({ text: categoryText[e as Category], value: e as Category })), { defaultValue: filters.category, change: v => { filters.category = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Rareté'), multiselect(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: filters.rarity, change: v => { filters.rarity = v; applyFilters(); }, class: { container: 'w-40' } }) ]),
div('flex flex-row gap-2 items-center', [ text('Nom'), input('text', { defaultValue: filters.name, input: v => { filters.name = v; applyFilters(); }, class: 'w-64' }) ]),
]),
div('grid grid-cols-1 -my-2 overflow-y-auto gap-1', { list: () => Object.values(config.items).filter(item =>
(filters.category.length === 0 || filters.category.includes(item.category)) &&
(filters.rarity.length === 0 || filters.rarity.includes(item.rarity)) &&
(filters.name === '' || item.name.toLowerCase().includes(filters.name.toLowerCase()))
), render: (e) => {
const item = config.items[e.id];
if(!item)
return;
return foldable(() => [ markdown(getText(item.description)) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
div('flex flex-row 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 w-16 gap-2 justify-between items-center px-2', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.weight?.toString() ?? '-') ]),
div('flex flex-row 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 w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = this.character!.character.variables.items;
if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false });
else if(list.find(e => e.id === item.id)) list.find(e => e.id === item.id)!.amount++;
else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] });
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } })
} }),
content,
]);
applyFilters();
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
}

View File

@ -1,10 +1,9 @@
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type RedrawableHTML } from "#shared/dom.util";
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import { Tree } from "#shared/tree";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type RedrawableHTML, type Reactive } from "./dom.util";
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
import { clamp } from "./general.util";
import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom";
import { type Reactive } from '#shared/reactive';
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
{

View File

@ -1,17 +1,18 @@
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
import { loading } from './components.util';
import { _defer, reactivity, type Proxy, type Reactive } from './reactive';
import { deepEquals } from './general.util';
export type RedrawableHTML = HTMLElement & { update?: (recursive: boolean) => void }
export type Node = HTMLElement & { update?: (recursive: boolean) => void } | SVGElement | Text | undefined;
export type NodeChildren = Array<Reactive<Node>> | undefined;
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
export type Class = Reactive<string | Array<Class> | Record<string, boolean> | undefined>;
type Listener<K extends keyof HTMLElementEventMap> = | ((this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any) | {
options?: boolean | AddEventListenerOptions;
listener: (this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any;
} | undefined;
export type Reactive<T> = T | (() => T);
export interface DOMList<T> extends Array<T>{
render(redraw?: boolean): void;
};
@ -20,166 +21,288 @@ export interface NodeProperties
{
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
text?: Reactive<string | number | Text>;
class?: Reactive<Class>;
class?: Class;
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
listeners?: {
[K in keyof HTMLElementEventMap]?: Listener<K>
};
}
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T];
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Reactive<Array<U>> }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node, list?: Reactive<Array<U>> }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
let defered = false, _deferSet = new Set<() => void>();
const _defer = (fn: () => void) => {
if(!defered)
{
defered = true;
queueMicrotask(() => {
_deferSet.forEach(e => e());
_deferSet.clear();
defered = false;
});
}
_deferSet.add(fn);
}
let reactiveEffect: (() => void) | null = null;
const _reactiveCache = new WeakMap();
// Store a Weak map of all the tracked object.
// For each object, we have a map of its properties, allowing us to effectively listen to absolutely everything on the object
// For a given property, we have a set of "effect" (function called on value update)
const _tracker = new WeakMap<{}, Map<string | symbol, Set<() => void>>>();
function trigger<T extends {}>(target: T, key: string | symbol)
{
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
const dependencies = _tracker.get(target)
if(!dependencies) return;
const set = dependencies.get(key);
set?.forEach(e => e());
}
function track<T extends {}>(target: T, key: string | symbol)
{
if(!reactiveEffect) return;
let dependencies = _tracker.get(target);
if(!dependencies)
{
dependencies = new Map();
_tracker.set(target, dependencies);
}
let set = dependencies.get(key);
if(!set)
{
set = new Set();
dependencies.set(key, set);
}
set.add(reactiveEffect);
}
export function reactive<T extends {}>(obj: T): T
{
if(_reactiveCache.has(obj))
return _reactiveCache.get(obj)!;
const proxy = new Proxy<T>(obj, {
get: (target, key, receiver) => {
track(target, key);
const value = Reflect.get(target, key, receiver);
if(value && typeof value === 'object')
return reactive(value);
return value;
},
set: (target, key, value, receiver) => {
const old = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if(old !== value)
_defer(() => trigger(target, key));
return result;
},
});
_reactiveCache.set(obj, proxy);
return proxy;
}
function requireReactive<T>(reactiveProperty: Reactive<T>, effect: (processed: T) => void)
{
if(typeof reactiveProperty !== 'function')
return effect(reactiveProperty);
else
{
// Function wrapping to keep the context safe and secured.
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
const secureEffect = () => effect((reactiveProperty as () => T)());
const secureContext = () => {
reactiveEffect = secureContext;
try {
secureEffect();
} finally {
reactiveEffect = null;
}
};
secureContext();
}
}
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T] & { update?: (recursive: boolean) => void };
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): HTMLElementTagNameMap[T] & { array?: DOMList<U>, update?: (recursive: boolean) => void };
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): HTMLElementTagNameMap[T] & { array?: DOMList<U>, update?: (recursive: boolean) => void }
{
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U>, update: (recursive: boolean) => void };
let setup = true, updating = false;
const _cache = new Map<U, Node>();
if(children)
{
if(Array.isArray(children))
const update = (recursive: boolean) => {
updating = true;
if(children !== undefined && (setup || recursive))
{
for(const c of children)
if(Array.isArray(children))
{
const child = typeof c === 'function' ? c() : c;
child && element.appendChild(child);
for(const c of children)
{
const child = typeof c === 'function' ? c() : c;
if(child !== undefined)
{
element.appendChild(child);
recursive && 'update' in child && _defer(() => child.update!(true));
}
}
}
else if(children.list !== undefined)
{
if(setup)
{
children.list.forEach(e => _cache.set(e, children.render(e)));
const _push = children.list.push;
children.list.push = (...items: U[]) => {
items.forEach(e => {
if(!_cache.has(e))
{
const dom = children.render(e);
_cache.set(e, dom);
dom && element.appendChild(dom);
}
else
{
const dom = _cache.get(e);
dom && element.appendChild(dom);
}
});
if(children.redraw) _defer(() => update(false));
return _push.bind(children.list)(...items);
};
const _splice = children.list.splice;
children.list.splice = (start: number, deleteCount: number, ...items: U[]) => {
const list = _splice.bind(children.list)(start, deleteCount, ...items);
list.forEach(e => { if(!children.list!.find(_e => _e === e)) _cache.delete(e); });
element.array!.render();
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.render = (redraw?: boolean) => {
element.replaceChildren(...children.list?.map(e => _cache.get(e)).filter(e => !!e) ?? []);
if((redraw !== undefined || children.redraw !== undefined) && !updating) _defer(() => update(redraw ?? children.redraw!));
}
element.array.render();
}
}
else if(children.list !== undefined)
if(properties?.attributes)
{
reactivity(children.list, (list) => {
element.replaceChildren();
list?.forEach(e => {
let dom = _cache.get(e);
if(!dom)
{
dom = children.render(e);
_cache.set(e, dom);
}
dom && element.appendChild(dom);
});
for(const [k, v] of Object.entries(properties.attributes))
{
if(!setup && typeof v !== 'function') continue;
const value = typeof v === 'function' ? v() : v;
if(typeof value === 'string' || typeof value === 'number') element.setAttribute(k, value.toString(10));
else if(typeof value === 'boolean') element.toggleAttribute(k, value);
}
}
if(properties?.text && (setup || typeof properties.text === 'function'))
{
requireReactive(properties.text, (text) => {
if(typeof text === 'string')
element.textContent = text;
else if(typeof text === 'number')
element.textContent = text.toString();
else
element.appendChild(text as Text);
})
}
}
if(properties?.attributes)
{
for(const [k, v] of Object.entries(properties.attributes))
if(properties?.listeners && setup)
{
reactivity(properties.attributes[k], (attribute) => {
if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10));
else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute);
});
for(let [k, v] of Object.entries(properties.listeners))
{
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
if(typeof value === 'function')
element.addEventListener(key, value.bind(element));
else if(value)
element.addEventListener(key, value.listener.bind(element), value.options);
}
}
}
if(properties?.text)
{
reactivity(properties.text, (text) => {
if(typeof text === 'string')
element.textContent = text;
else if(typeof text === 'number')
element.textContent = text.toString();
else
element.appendChild(text as Text);
})
}
styling(element, properties ?? {});
updating = false;
};
if(properties?.listeners)
{
for(let [k, v] of Object.entries(properties.listeners))
{
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
if(typeof value === 'function')
element.addEventListener(key, value.bind(element));
else if(value)
element.addEventListener(key, value.listener.bind(element), value.options);
}
}
if(properties?.class)
{
reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes)));
}
if(properties?.style)
{
reactivity(properties.style, (style) => {
if(typeof style === 'string') element.setAttribute('style', style);
else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v);
})
}
update(false);
setup = false;
element.update = update;
return element;
}
export function div(cls?: Reactive<Class>, children?: NodeChildren): HTMLElementTagNameMap['div']
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U) => Node, list?: Reactive<Array<U>> }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U) => Node, list?: Reactive<Array<U>> }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
export function div(cls?: Class, children?: NodeChildren): HTMLElementTagNameMap['div'] & { update?: (recursive: boolean) => void }
export function div<U extends any>(cls?: Class, children?: { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): HTMLElementTagNameMap['div'] & { array: DOMList<U>, update?: (recursive: boolean) => void }
export function div<U extends any>(cls?: Class, children?: NodeChildren | { render: (data: U) => Node, list?: Array<U>, redraw?: boolean }): HTMLElementTagNameMap['div'] & { array?: DOMList<U>, update?: (recursive: boolean) => void }
{
//@ts-expect-error wtf is wrong here ???
//@ts-expect-error
return dom("div", { class: cls }, children);
}
export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span'] & { update?: (recursive: boolean) => void }
export function span(cls?: Class, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span'] & { update?: (recursive: boolean) => void }
{
return dom("span", { class: cls, text: text });
}
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Array<Reactive<SVGElement>>): SVGElementTagNameMap[K]
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: SVGElement[]): SVGElementTagNameMap[K]
{
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
if(children)
{
for(const c of children)
{
const child = typeof c === 'function' ? c() : c;
child && element.appendChild(child);
}
}
if(children && children.length > 0)
for(const c of children) if(c !== undefined) element.appendChild(c);
if(properties?.attributes)
{
for(const [k, v] of Object.entries(properties.attributes))
{
reactivity(properties.attributes[k], (attribute) => {
if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10));
else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute);
});
}
}
if(typeof v === 'string') element.setAttribute(k, v);
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
if(properties?.text)
{
reactivity(properties.text, (text) => {
if(typeof text === 'string')
element.textContent = text;
else if(typeof text === 'number')
element.textContent = text.toString();
else
element.appendChild(text as Text);
})
}
if(properties?.text && typeof properties.text === 'string')
element.textContent = properties.text;
if(properties?.class)
{
reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes)));
}
if(properties?.style)
{
reactivity(properties.style, (style) => {
if(typeof style === 'string') element.setAttribute('style', style);
else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v);
})
}
styling(element, properties ?? {});
return element;
}
export function text(data: Reactive<string | number>): Text
{
const text = document.createTextNode('');
reactivity(data, (txt) => text.textContent = txt.toString());
requireReactive(data, (txt) => text.textContent = txt.toString());
return text;
}
export function styling(element: SVGElement | RedrawableHTML, properties: {
class?: Class;
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
}): SVGElement | RedrawableHTML
{
if(properties?.class)
{
element.setAttribute('class', mergeClasses(properties.class));
}
if(properties?.style)
{
if(typeof properties.style === 'string')
{
element.setAttribute('style', properties.style);
}
else
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v);
}
return element;
}
export interface IconProperties
{
@ -215,8 +338,9 @@ export function icon(name: string, properties?: IconProperties)
return element;
}
export function mergeClasses(classes: Class): string
export function mergeClasses(cls: Class): string
{
const classes = typeof cls === 'function' ? cls() : cls;
if(typeof classes === 'string')
{
return classes.trim();

View File

@ -21,9 +21,8 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
if(properties?.text)
{
const text = typeof properties.text === 'function' ? properties.text() : properties.text;
children ??= [];
text && children?.push(text.toString());
children?.push(properties.text);
}
if(children)

View File

@ -10,7 +10,7 @@ import { tags } from '@lezer/highlight';
import { dom, type RedrawableHTML } from '#shared/dom.util';
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension';
import renderMarkdown from '#shared/markdown.util';
import { renderMarkdown } from '#shared/markdown.util';
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
import { tagTag, tag as tagExtension } from './grammar/tag.extension';
@ -99,7 +99,7 @@ class CalloutWidget extends WidgetType
this.content = content;
this.foldable = foldable;
this.contentMD = renderMarkdown(content, undefined, { tags: { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th } });
this.contentMD = renderMarkdown(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
}
override eq(other: CalloutWidget)
{

View File

@ -18,7 +18,7 @@ const config = characterConfig as CharacterConfig;
export class HomebrewBuilder
{
private _container: RedrawableHTML;
private _tabs: RedrawableHTML;
private _tabs: RedrawableHTML & { refresh: () => void };
private _config: CharacterConfig;
private _featureEditor: FeaturePanel;

View File

@ -1,7 +1,6 @@
import * as FloatingUI from "@floating-ui/dom";
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type RedrawableHTML } from "./dom.util";
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type Reactive, type RedrawableHTML } from "./dom.util";
import { button } from "./components.util";
import type { Reactive } from "./reactive";
export interface FloatingProperties
{

View File

@ -1,10 +1,9 @@
import type { CharacterConfig, i18nID } from "~/types/character";
import characterConfig from '#shared/character-config.json';
import type { Localized } from "~/types/general";
const config = characterConfig as CharacterConfig;
export function getText(id?: i18nID, lang?: keyof Localized): string
export function getText(id?: i18nID, lang?: string): string
{
return id ? (config.texts.hasOwnProperty(id) ? (config.texts[id] as Localized)[lang ?? "default"] ?? '' : '') : '';
return id ? (config.texts.hasOwnProperty(id) ? config.texts[id][lang ?? "default"] : '') : '';
}

View File

@ -1,5 +1,5 @@
import type { Root, RootContent } from "hast";
import { dom, text, type Class, type Node } from "#shared/dom.util";
import { dom, styling, text, type Class, type Node, type RedrawableHTML } from "#shared/dom.util";
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout, type Prose } from "#shared/proses";
import { heading } from "hast-util-heading";
import { headingRank } from "hast-util-heading-rank";
@ -64,7 +64,9 @@ export function markdownReference(content: string, filter?: string, properties?:
}
}
const el = dom('div', properties, data.children.map(e => renderContent(e, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags))));
const el = renderMarkdown(data, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags));
if(properties) styling(el, properties);
return el;
}));

View File

@ -252,14 +252,14 @@ export class SnapFinder {
findEdgeSnapPosition(node: string, x: number, y: number): { x: number, y: number, node: string, direction: Direction } | undefined
{
const near = [...this.spatialGrid.fetch(x, y)?.values().filter(e => e.id !== node).flatMap(e => this.snapPointCache.getSnapPoints(e.id)?.map(_e => ({ ..._e, node: e })) ?? []) ?? []].filter(e => e.type === TYPE.EDGE);
const near = [...this.spatialGrid.fetch(x, y)?.values().filter(e => e !== node).flatMap(e => this.snapPointCache.getSnapPoints(e)?.map(_e => ({ ..._e, node: e })) ?? []) ?? []].filter(e => e.type === TYPE.EDGE);
let nearestDistance = this.config.threshold, nearest = undefined;
for (const point of near) {
const distance = Math.hypot(point.pos.x - x, point.pos.y - y);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = { ...point.pos, node: point.node.id, direction: point.side! };
nearest = { ...point.pos, node: point.node, direction: point.side! };
}
}
@ -290,7 +290,7 @@ export class SnapFinder {
this.snapPointCache.invalidate(node);
this.snapPointCache.insert(node);
const neighbors = [...this.spatialGrid.getViewportNeighbors(node, this.canvas.viewport)].flatMap(e => this.snapPointCache.getSnapPoints(e.id)).filter(e => !!e);
const neighbors = [...this.spatialGrid.getViewportNeighbors(node, this.canvas.viewport)].flatMap(e => this.snapPointCache.getSnapPoints(e)).filter(e => !!e);
const bestSnap = this.findBestSnap(this.snapPointCache.getSnapPoints(node.id)!, neighbors, this.config.threshold, resizeHandle);
return this.applySnap(node, bestSnap.x, bestSnap.y, resizeHandle);
@ -354,9 +354,9 @@ export class SnapFinder {
if (resizeHandle)
{
result.x = offsetx ? node.x + offsetx * resizeHandle.x : this.snapToGrid(node.x);
result.width = offsetx ? node.width + offsetx * resizeHandle.width : this.snapToGrid(node.width);
result.w = offsetx ? node.width + offsetx * resizeHandle.width : this.snapToGrid(node.width);
result.y = offsety ? node.y + offsety * resizeHandle.y : this.snapToGrid(node.y);
result.height = offsety ? node.height - offsety * resizeHandle.height : this.snapToGrid(node.height);
result.h = offsety ? node.height - offsety * resizeHandle.height : this.snapToGrid(node.height);
}
else
{

View File

@ -1,304 +0,0 @@
export type Reactive<T> = T | (() => T);
export const isString = (val: unknown): val is string => typeof val === 'string';
const isIntegerKey = (key: unknown): boolean => isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key;
let defered = false, _deferSet = new Set<() => void>();
export const _defer = (fn: () => void) => {
if(!defered)
{
defered = true;
queueMicrotask(() => {
_deferSet.forEach(e => e());
_deferSet.clear();
defered = false;
});
}
_deferSet.add(fn);
}
let activeEffect: (() => void) | null = null, _isTracking = true;
const SYMBOLS = {
PROXY: Symbol('is a proxy'),
ITERATE: Symbol('iterating'),
RAW: Symbol('raw value'),
} as const;
function reactiveReadArray<T>(array: T[]): T[]
{
const _raw = raw(array)
if (_raw === array) return _raw;
track(_raw, SYMBOLS.ITERATE);
return _raw.map(wrapReactive);
}
function shallowReadArray<T>(arr: T[]): T[]
{
track((arr = raw(arr)), SYMBOLS.ITERATE);
return arr;
}
function iterator(self: unknown[], method: keyof Array<unknown>, wrapValue: (value: any) => unknown)
{
const arr = shallowReadArray(self);
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
_next: IterableIterator<unknown>['next']
};
if (arr !== self && !isShallow(self))
{
iter._next = iter.next;
iter.next = () => {
const result = iter._next();
if (!result.done) result.value = wrapValue(result.value);
return result;
}
}
return iter;
}
function wrapReactive(obj: any): any
{
return obj && typeof obj === 'object' ? reactive(obj as Proxy<object>) : obj;
}
const arrayProto = Array.prototype
function apply(self: unknown[], method: keyof Array<any>, fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown, wrappedRetFn?: (result: any) => unknown, args?: IArguments)
{
const arr = shallowReadArray(self);
const needsWrap = arr !== self;
const methodFn = arr[method] as Function;
if (methodFn !== arrayProto[method as any])
{
const result = methodFn.apply(self, args);
return needsWrap ? toReactive(result) : result;
}
let wrappedFn = fn;
if (arr !== self)
{
if (needsWrap)
{
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, wrapReactive(item), index, self);
};
}
else if (fn.length > 2)
{
wrappedFn = function (this: unknown, item, index) {
return fn.call(this, item, index, self);
};
}
}
const result = methodFn.call(arr, wrappedFn, thisArg);
return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;
}
function reduce(self: unknown[], method: keyof Array<any>, fn: (acc: unknown, item: unknown, index: number, array: unknown[]) => unknown, args: unknown[])
{
const arr = shallowReadArray(self);
let wrappedFn = fn;
if (arr !== self && fn.length > 3)
{
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) };
}
else
{
wrappedFn = function (this: unknown, acc, item, index) { return fn.call(this, acc, wrapReactive(item), index, self) ;}
}
return (arr[method] as any)(wrappedFn, ...args);
}
function searchProxy(self: unknown[], method: keyof Array<any>, args: unknown[])
{
const arr = raw(self) as any;
track(arr, SYMBOLS.ITERATE);
const res = arr[method](...args);
if ((res === -1 || res === false) && isProxy(args[0]))
{
args[0] = raw(args[0]);
return arr[method](...args);
}
return res;
}
function noTracking(self: unknown[], method: keyof Array<any>, args: unknown[] = [])
{
_isTracking = false;
const res = (raw(self) as any)[method].apply(self, args);
_isTracking = true;
return res;
}
const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ without getting an error
__proto__: null, // <-- Required to remove the object prototype removing the object default functions from the substitution
[Symbol.iterator]() { return iterator(this, Symbol.iterator, item => wrapReactive(item)) },
concat(...args: unknown[]) { return reactiveReadArray(this).concat(...args.map(x => (Array.isArray(x) ? reactiveReadArray(x) : x))) },
entries() { return iterator(this, 'entries', (value: [number, unknown]) => { value[1] = wrapReactive(value[1]); return value; }) },
every(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'every', fn, thisArg, undefined, arguments) },
filter(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply( this, 'filter', fn, thisArg, v => v.map((item: unknown) => wrapReactive(item)), arguments, ) },
find(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'find', fn, thisArg, item => wrapReactive(item), arguments, ) },
findIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findIndex', fn, thisArg, undefined, arguments) },
findLast(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply( this, 'findLast', fn, thisArg, item => wrapReactive(item), arguments) },
findLastIndex(fn: (item: unknown, index: number, array: unknown[]) => boolean, thisArg?: unknown) { return apply(this, 'findLastIndex', fn, thisArg, undefined, arguments) },
forEach(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'forEach', fn, thisArg, undefined, arguments) },
includes(...args: unknown[]) { return searchProxy(this, 'includes', args) },
indexOf(...args: unknown[]) { return searchProxy(this, 'indexOf', args) },
join(separator?: string) { return reactiveReadArray(this).join(separator) },
lastIndexOf(...args: unknown[]) { return searchProxy(this, 'lastIndexOf', args) },
map(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'map', fn, thisArg, undefined, arguments) },
pop() { return noTracking(this, 'pop') },
push(...args: unknown[]) { return noTracking(this, 'push', args) },
reduce(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduce', fn, args) },
reduceRight(fn: ( acc: unknown, item: unknown, index: number, array: unknown[], ) => unknown, ...args: unknown[]) { return reduce(this, 'reduceRight', fn, args) },
shift() { return noTracking(this, 'shift') },
some(fn: (item: unknown, index: number, array: unknown[]) => unknown, thisArg?: unknown) { return apply(this, 'some', fn, thisArg, undefined, arguments) },
splice(...args: unknown[]) { return noTracking(this, 'splice', args) },
toReversed() { return reactiveReadArray(this).toReversed() },
toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) },
toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) },
unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) },
values() { return iterator(this, 'values', item => wrapReactive(item)) }, /* */
};
// Store object to proxy correspondance
const _reactiveCache = new WeakMap();
// Store a Weak map of all the tracked object.
// For each object, we have a map of its properties, allowing us to effectively listen to absolutely everything on the object
// For a given property, we have a set of "effect" (function called on value update)
type Dependency = Set<() => void>;
const _tracker = new WeakMap<object, Map<string | symbol | null, Dependency>>();
function trigger(target: object, key?: string | symbol | null, value?: unknown)
{
const dependencies = _tracker.get(target);
if(!dependencies) return;
const run = (dep?: Dependency) => {
dep?.forEach(_defer);
};
const isArray = Array.isArray(target);
const arrayIndex = isIntegerKey(key);
//When the array length is modified, call not only the length and ITERATE dependencies but also the added/removed items dependencies
if(isArray && key === 'length')
{
// Run for 'length' key, SYMBOL.ITERATE and any index key after the new length (for reduction)
dependencies.forEach((v, k: any) => (k === 'length' || k === SYMBOLS.ITERATE || (isIntegerKey(k) && k >= (value as number))) && run(v));
}
else
{
key !== undefined && run(dependencies.get(key));
arrayIndex && run(dependencies.get(SYMBOLS.ITERATE));
}
}
function track(target: object, key: string | symbol | null)
{
if(!activeEffect || !_isTracking) return;
let dependencies = _tracker.get(target);
if(!dependencies)
{
dependencies = new Map();
_tracker.set(target, dependencies);
}
let set = dependencies.get(key);
if(!set)
{
set = new Set();
dependencies.set(key, set);
}
set.add(activeEffect);
//if(set) console.log('Tracking %o with key "%s"', target, key, set.size);
}
export type Proxy<T> = T & {
[SYMBOLS.PROXY]?: boolean;
[SYMBOLS.RAW]?: T;
};
export function isProxy(target: Proxy<any>): boolean
{
return target[SYMBOLS.PROXY];
}
export function reactive<T extends object>(obj: T | Proxy<T>): T | Proxy<T>
{
if((obj as Proxy<T>)[SYMBOLS.PROXY])
return obj;
if(_reactiveCache.has(obj))
return _reactiveCache.get(obj)!;
const prototype = Object.getPrototypeOf(obj);
const isArray = Array.isArray(obj);
const proxy = new Proxy<T>(obj, {
get: (target, key, receiver) => {
if(key === SYMBOLS.PROXY)
return true;
else if(key === SYMBOLS.RAW)
return obj;
if(key in arraySubstitute)
return arraySubstitute[key]!;
const value = Reflect.get(target, key, receiver);
track(target, key);
//If the value is an object, mark it as reactive dynamically
if(value && typeof value === 'object')
return reactive(value as Proxy<object>);
return value;
},
set: (target, key, value, receiver) => {
if(key === SYMBOLS.PROXY || key === SYMBOLS.RAW)
return false;
const result = Reflect.set(target, key, raw(value), receiver);
trigger(target, key, value);
return result;
},
deleteProperty: (target, key) => {
const has = key in target;
const result = Reflect.deleteProperty(target, key);
if(result && has) trigger(target, key);
return result;
},
has: (target, key) => {
const result = Reflect.has(target, key);
track(target, key);
return result;
},
ownKeys: (target) => {
const result = Reflect.ownKeys(target);
track(target, SYMBOLS.ITERATE);
return result;
}
}) as Proxy<T>;
_reactiveCache.set(obj, proxy);
return proxy;
}
export function raw<T>(obj: T): T
{
return typeof obj === 'object' ? ((obj as Proxy<T>)[SYMBOLS.RAW] as Proxy<T> | undefined) ?? obj : obj;
}
export function reactivity<T>(reactiveProperty: Reactive<T>, effect: (processed: T) => void)
{
// Function wrapping to keep the context safe and secured.
// Also useful to retrigger the tracking system if the reactive property provides new properties (via conditions for example)
const secureEffect = () => effect(typeof reactiveProperty === 'function' ? (reactiveProperty as () => T)() : reactiveProperty);
const secureContext = () => {
activeEffect = secureContext;
try {
return secureEffect();
} finally {
activeEffect = null;
}
};
return secureContext();
}