Rework reactivity for array listening
This commit is contained in:
parent
323cb0ba7f
commit
49691feeee
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ability, Alignment, ArmorConfig, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, ItemConfig, ItemState, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponType } from "~/types/character";
|
||||
import { keyof, z } from "zod/v4";
|
||||
import type { Ability, Alignment, ArmorConfig, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, FeatureItem, ItemConfig, ItemState, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponType } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import proses, { preview } from "#shared/proses";
|
||||
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||
|
|
@ -11,6 +11,7 @@ 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;
|
||||
|
||||
|
|
@ -295,8 +296,8 @@ export class CharacterCompiler
|
|||
|
||||
set character(value: Character)
|
||||
{
|
||||
this._character = value;
|
||||
this._result = defaultCompiledCharacter(value);
|
||||
this._character = reactive(value);
|
||||
this._result = reactive(defaultCompiledCharacter(value));
|
||||
this._buffer = {
|
||||
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||
|
|
@ -306,6 +307,11 @@ 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)
|
||||
{
|
||||
|
|
@ -354,30 +360,14 @@ 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()
|
||||
{
|
||||
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 });
|
||||
})
|
||||
}
|
||||
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 });
|
||||
})
|
||||
}
|
||||
saveNotes()
|
||||
{
|
||||
|
|
@ -1006,11 +996,11 @@ class TrainingPicker extends BuilderTab
|
|||
]),
|
||||
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
|
||||
dom("span", { text: "Vie" }),
|
||||
text(this._builder, '{{compiled.health}}'),
|
||||
text(() => this._builder.compiled.health),
|
||||
]),
|
||||
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
|
||||
dom("span", { text: "Mana" }),
|
||||
text(this._builder, '{{compiled.mana}}'),
|
||||
text(() => this._builder.compiled.mana),
|
||||
]),
|
||||
button(text('Suivant'), () => this._builder.display(3), 'h-[35px] px-[15px]'),
|
||||
]), dom('span')
|
||||
|
|
@ -1321,14 +1311,14 @@ export class CharacterSheet
|
|||
useRequestFetch()(`/api/character/${id}`).then(character => {
|
||||
if(character)
|
||||
{
|
||||
this.character!.character = character;
|
||||
this.character!.character = reactive(character);
|
||||
this.character!.values;
|
||||
'update' in this.container! && this.container!.update!(true);
|
||||
}
|
||||
});
|
||||
})
|
||||
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))
|
||||
|
|
@ -1341,7 +1331,7 @@ export class CharacterSheet
|
|||
if(idx !== -1) prop.splice(idx, 1);
|
||||
}
|
||||
this.character?.variable(variable.key, prop, false);
|
||||
}
|
||||
} */
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1384,19 +1374,15 @@ export class CharacterSheet
|
|||
privateNotes.content = this.character!.character.notes!.private!;
|
||||
|
||||
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: RedrawableHTML }) => {
|
||||
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();
|
||||
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();
|
||||
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) => {
|
||||
|
|
@ -1407,7 +1393,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) => {
|
||||
|
|
@ -1458,12 +1444,12 @@ export class CharacterSheet
|
|||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||
text("PV: "),
|
||||
health.readonly,
|
||||
text(character, `/ {{health}}`),
|
||||
text(() => character.health),
|
||||
]),
|
||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||
text("Mana: "),
|
||||
mana.readonly,
|
||||
text(character, `/ {{mana}}`),
|
||||
text(() => character.mana),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
|
@ -1478,31 +1464,31 @@ export class CharacterSheet
|
|||
div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
|
||||
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.strength}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.strength}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.dexterity}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.dexterity}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.constitution}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.constitution}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.intelligence}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.intelligence}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.curiosity}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.curiosity}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.charisma}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.charisma}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
|
||||
]),
|
||||
div("flex flex-col items-center px-2", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{modifier.psyche}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.psyche}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" })
|
||||
])
|
||||
]),
|
||||
|
|
@ -1511,11 +1497,11 @@ export class CharacterSheet
|
|||
|
||||
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
|
||||
div("flex flex-col px-2 items-center", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `+{{initiative}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.initiative}`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Initiative" })
|
||||
]),
|
||||
div("flex flex-col px-2 items-center", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, () => character.speed === false ? "Aucun déplacement" : `{{speed}} cases`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => character.speed === false ? "Aucun déplacement" : `${character.speed} cases`) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Course" })
|
||||
])
|
||||
]),
|
||||
|
|
@ -1525,15 +1511,15 @@ export class CharacterSheet
|
|||
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
|
||||
icon("game-icons:checked-shield", { width: 32, height: 32 }),
|
||||
div("flex flex-col px-2 items-center", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.passive}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => clamp(character.defense.static + character.defense.passiveparry + character.defense.passivedodge, 0, character.defense.hardcap)) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Passive" })
|
||||
]),
|
||||
div("flex flex-col px-2 items-center", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.parry}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => clamp(character.defense.static + character.defense.activeparry + character.defense.passivedodge, 0, character.defense.hardcap)) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Blocage" })
|
||||
]),
|
||||
div("flex flex-col px-2 items-center", [
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(character, `{{defense.dodge}}`) ]),
|
||||
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => clamp(character.defense.static + character.defense.passiveparry + character.defense.activedodge, 0, character.defense.hardcap)) ]),
|
||||
dom("span", { class: "text-sm 2xl:text-base", text: "Esquive" })
|
||||
])
|
||||
]),
|
||||
|
|
@ -1551,7 +1537,7 @@ export class CharacterSheet
|
|||
Object.keys(character.abilities).map((ability) =>
|
||||
div("flex flex-row px-1 justify-between items-center", [
|
||||
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
|
||||
span("font-bold text-base text-light-100 dark:text-dark-100", text(character.abilities, `+{{${ability}}}`)),
|
||||
span("font-bold text-base text-light-100 dark:text-dark-100", text(() => `+${character.abilities[ability as Ability] ?? 0}`)),
|
||||
])
|
||||
)
|
||||
),
|
||||
|
|
@ -1583,10 +1569,10 @@ export class CharacterSheet
|
|||
]) : undefined,
|
||||
|
||||
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
||||
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', text(character.spellranks, "{{precision}}")) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', text(character.spellranks, "{{knowledge}}")) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', text(character.spellranks, "{{instinct}}")) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', text(character.spellranks, "{{arts}}")) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', text(() => character.spellranks.precision)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', text(() => character.spellranks.knowledge)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', text(() => character.spellranks.instinct)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', text(() => character.spellranks.arts)) ]),
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
|
@ -1659,50 +1645,52 @@ export class CharacterSheet
|
|||
}
|
||||
spellTab(character: CompiledCharacter)
|
||||
{
|
||||
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
|
||||
const preference = reactive({
|
||||
sort: localStorage.getItem('character-sort') ?? 'rank',
|
||||
} as { sort: 'rank' | 'type' | 'element' });
|
||||
|
||||
const sort = () => {
|
||||
switch(sortPreference)
|
||||
const sort = (spells: string[]) => {
|
||||
localStorage.setItem('character-sort', preference.sort);
|
||||
|
||||
switch(preference.sort)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
};
|
||||
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: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; sort().render(); } }),
|
||||
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; } }),
|
||||
]),
|
||||
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.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
|
||||
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' : '') }),
|
||||
button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'),
|
||||
])
|
||||
]),
|
||||
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', `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]) }),
|
||||
])
|
||||
]
|
||||
}
|
||||
//TODO: Update to handle reactivity
|
||||
spellPanel(character: CompiledCharacter)
|
||||
{
|
||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||
|
|
@ -1710,61 +1698,48 @@ 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' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisé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' }, [ 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 }), () => {
|
||||
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', 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(() => [
|
||||
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', { list: availableSpells, render: (spell) => 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}` }),
|
||||
text("/"),
|
||||
dom("span", { text: spellTypeTexts[spell.type] }),
|
||||
text("/")
|
||||
] : []),
|
||||
dom("span", { text: `${spell.cost} mana` }),
|
||||
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: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
|
||||
]),
|
||||
toggleButton,
|
||||
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` })
|
||||
]),
|
||||
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
|
||||
}))
|
||||
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' } })
|
||||
})
|
||||
]);
|
||||
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
||||
|
|
@ -1772,17 +1747,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, redraw: true, render: e => {
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, render: e => {
|
||||
const item = config.items[e.id];
|
||||
|
||||
if(!item) return;
|
||||
|
|
@ -1801,25 +1776,19 @@ 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++; (items as DOMList<ItemState>)?.render(); }
|
||||
else if(items.find(_e => _e === e)) items.find(_e => _e === e)!.amount++;
|
||||
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))) ]),
|
||||
]),
|
||||
|
|
@ -1837,41 +1806,13 @@ export class CharacterSheet
|
|||
}
|
||||
itemsPanel(character: CompiledCharacter)
|
||||
{
|
||||
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 } } = {
|
||||
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = reactive({
|
||||
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" }),
|
||||
|
|
@ -1881,13 +1822,39 @@ 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; 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('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' }) ]),
|
||||
]),
|
||||
content,
|
||||
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' } })
|
||||
} }),
|
||||
]);
|
||||
applyFilters();
|
||||
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
||||
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||
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 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 { Placement } from "@floating-ui/dom";
|
||||
import { type Reactive } from '#shared/reactive';
|
||||
|
||||
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
||||
import { loading } from './components.util';
|
||||
import { deepEquals } from './general.util';
|
||||
import { _defer, reactivity, type Proxy, type Reactive } from './reactive';
|
||||
|
||||
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 = Reactive<string | Array<Class> | Record<string, boolean> | undefined>;
|
||||
export type Class = 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;
|
||||
};
|
||||
|
|
@ -21,288 +20,166 @@ export interface NodeProperties
|
|||
{
|
||||
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
|
||||
text?: Reactive<string | number | Text>;
|
||||
class?: Class;
|
||||
class?: Reactive<Class>;
|
||||
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
|
||||
listeners?: {
|
||||
[K in keyof HTMLElementEventMap]?: Listener<K>
|
||||
};
|
||||
}
|
||||
|
||||
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 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 }
|
||||
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> }
|
||||
{
|
||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U>, update: (recursive: boolean) => void };
|
||||
let setup = true, updating = false;
|
||||
|
||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
const _cache = new Map<U, Node>();
|
||||
|
||||
const update = (recursive: boolean) => {
|
||||
updating = true;
|
||||
|
||||
if(children !== undefined && (setup || recursive))
|
||||
if(children)
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
for(const c of children)
|
||||
{
|
||||
for(const c of children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
if(child !== undefined)
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
}
|
||||
else if(children.list !== undefined)
|
||||
{
|
||||
reactivity(children.list, (list) => {
|
||||
element.replaceChildren();
|
||||
list?.forEach(e => {
|
||||
let dom = _cache.get(e);
|
||||
if(!dom)
|
||||
{
|
||||
element.appendChild(child);
|
||||
recursive && 'update' in child && _defer(() => child.update!(true));
|
||||
dom = children.render(e);
|
||||
_cache.set(e, dom);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.attributes)
|
||||
{
|
||||
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);
|
||||
dom && element.appendChild(dom);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.listeners && setup)
|
||||
if(properties?.attributes)
|
||||
{
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
{
|
||||
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);
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
styling(element, properties ?? {});
|
||||
updating = false;
|
||||
};
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
update(false);
|
||||
setup = false;
|
||||
element.update = update;
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
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 }
|
||||
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> }
|
||||
{
|
||||
//@ts-expect-error
|
||||
//@ts-expect-error wtf is wrong here ???
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function span(cls?: Class, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span'] & { update?: (recursive: boolean) => void }
|
||||
export function span(cls?: Reactive<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?: SVGElement[]): SVGElementTagNameMap[K]
|
||||
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Array<Reactive<SVGElement>>): SVGElementTagNameMap[K]
|
||||
{
|
||||
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
|
||||
if(children && children.length > 0)
|
||||
for(const c of children) if(c !== undefined) element.appendChild(c);
|
||||
if(children)
|
||||
{
|
||||
for(const c of children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.attributes)
|
||||
{
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
if(typeof v === 'string') element.setAttribute(k, v);
|
||||
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
|
||||
{
|
||||
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(properties?.text && typeof properties.text === 'string')
|
||||
element.textContent = properties.text;
|
||||
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 ?? {});
|
||||
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);
|
||||
})
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
export function text(data: Reactive<string | number>): Text
|
||||
{
|
||||
const text = document.createTextNode('');
|
||||
requireReactive(data, (txt) => text.textContent = txt.toString());
|
||||
reactivity(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
|
||||
{
|
||||
|
|
@ -338,9 +215,8 @@ export function icon(name: string, properties?: IconProperties)
|
|||
return element;
|
||||
}
|
||||
|
||||
export function mergeClasses(cls: Class): string
|
||||
export function mergeClasses(classes: Class): string
|
||||
{
|
||||
const classes = typeof cls === 'function' ? cls() : cls;
|
||||
if(typeof classes === 'string')
|
||||
{
|
||||
return classes.trim();
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
|||
|
||||
if(properties?.text)
|
||||
{
|
||||
const text = typeof properties.text === 'function' ? properties.text() : properties.text;
|
||||
children ??= [];
|
||||
children?.push(properties.text);
|
||||
text && children?.push(text.toString());
|
||||
}
|
||||
|
||||
if(children)
|
||||
|
|
|
|||
|
|
@ -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(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
|
||||
this.contentMD = renderMarkdown(content, undefined, { tags: { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th } });
|
||||
}
|
||||
override eq(other: CalloutWidget)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const config = characterConfig as CharacterConfig;
|
|||
export class HomebrewBuilder
|
||||
{
|
||||
private _container: RedrawableHTML;
|
||||
private _tabs: RedrawableHTML & { refresh: () => void };
|
||||
private _tabs: RedrawableHTML;
|
||||
|
||||
private _config: CharacterConfig;
|
||||
private _featureEditor: FeaturePanel;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as FloatingUI from "@floating-ui/dom";
|
||||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type Reactive, type RedrawableHTML } from "./dom.util";
|
||||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type RedrawableHTML } from "./dom.util";
|
||||
import { button } from "./components.util";
|
||||
import type { Reactive } from "./reactive";
|
||||
|
||||
export interface FloatingProperties
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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?: string): string
|
||||
export function getText(id?: i18nID, lang?: keyof Localized): string
|
||||
{
|
||||
return id ? (config.texts.hasOwnProperty(id) ? config.texts[id][lang ?? "default"] : '') : '';
|
||||
return id ? (config.texts.hasOwnProperty(id) ? (config.texts[id] as Localized)[lang ?? "default"] ?? '' : '') : '';
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Root, RootContent } from "hast";
|
||||
import { dom, styling, text, type Class, type Node, type RedrawableHTML } from "#shared/dom.util";
|
||||
import { dom, text, type Class, type Node } 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,10 +64,8 @@ export function markdownReference(content: string, filter?: string, properties?:
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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))));
|
||||
|
||||
return el;
|
||||
}));
|
||||
return state;
|
||||
|
|
|
|||
|
|
@ -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 !== node).flatMap(e => this.snapPointCache.getSnapPoints(e)?.map(_e => ({ ..._e, node: e })) ?? []) ?? []].filter(e => e.type === TYPE.EDGE);
|
||||
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);
|
||||
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, direction: point.side! };
|
||||
nearest = { ...point.pos, node: point.node.id, 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)).filter(e => !!e);
|
||||
const neighbors = [...this.spatialGrid.getViewportNeighbors(node, this.canvas.viewport)].flatMap(e => this.snapPointCache.getSnapPoints(e.id)).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.w = offsetx ? node.width + offsetx * resizeHandle.width : this.snapToGrid(node.width);
|
||||
result.width = offsetx ? node.width + offsetx * resizeHandle.width : this.snapToGrid(node.width);
|
||||
result.y = offsety ? node.y + offsety * resizeHandle.y : this.snapToGrid(node.y);
|
||||
result.h = offsety ? node.height - offsety * resizeHandle.height : this.snapToGrid(node.height);
|
||||
result.height = offsety ? node.height - offsety * resizeHandle.height : this.snapToGrid(node.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,304 @@
|
|||
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();
|
||||
}
|
||||
Loading…
Reference in New Issue