Reworking reactivity with a proxy/reflect mecanic
This commit is contained in:
parent
4cd478b47a
commit
323cb0ba7f
|
|
@ -1,11 +1,11 @@
|
|||
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 { z } from "zod/v4";
|
||||
import { keyof, 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";
|
||||
import { div, dom, icon, span, text, type DOMList, type RedrawableHTML } from "#shared/dom.util";
|
||||
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||
import { clamp } from "#shared/general.util";
|
||||
import { clamp, deepEquals } from "#shared/general.util";
|
||||
import markdown from "#shared/markdown.util";
|
||||
import { getText } from "#shared/i18n";
|
||||
import type { User } from "~/types/auth";
|
||||
|
|
@ -51,97 +51,91 @@ export const defaultCharacter: Character = {
|
|||
owner: -1,
|
||||
visibility: "private",
|
||||
};
|
||||
const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => {
|
||||
const compiled = {
|
||||
id: character.id,
|
||||
owner: character.owner,
|
||||
username: character.username,
|
||||
name: character.name,
|
||||
health: 0,
|
||||
mana: 0,
|
||||
race: character.people!,
|
||||
modifier: MAIN_STATS.reduce((p, v) => { p[v] = 0; return p; }, {} as Record<MainStat, number>),
|
||||
level: character.level,
|
||||
variables: character.variables,
|
||||
action: 0,
|
||||
reaction: 0,
|
||||
exhaust: 0,
|
||||
itempower: 0,
|
||||
features: {
|
||||
action: [],
|
||||
reaction: [],
|
||||
freeaction: [],
|
||||
passive: [],
|
||||
},
|
||||
abilities: {
|
||||
athletics: 0,
|
||||
acrobatics: 0,
|
||||
intimidation: 0,
|
||||
sleightofhand: 0,
|
||||
stealth: 0,
|
||||
survival: 0,
|
||||
investigation: 0,
|
||||
history: 0,
|
||||
religion: 0,
|
||||
arcana: 0,
|
||||
understanding: 0,
|
||||
perception: 0,
|
||||
performance: 0,
|
||||
medecine: 0,
|
||||
persuasion: 0,
|
||||
animalhandling: 0,
|
||||
deception: 0
|
||||
},
|
||||
spellslots: 0,
|
||||
artslots: 0,
|
||||
spellranks: {
|
||||
instinct: 0 as 0 | 1 | 2 | 3,
|
||||
knowledge: 0 as 0 | 1 | 2 | 3,
|
||||
precision: 0 as 0 | 1 | 2 | 3,
|
||||
arts: 0 as 0 | 1 | 2 | 3,
|
||||
},
|
||||
speed: false as number | false,
|
||||
defense: {
|
||||
hardcap: Infinity,
|
||||
static: 6,
|
||||
activeparry: 0,
|
||||
activedodge: 0,
|
||||
passiveparry: 0,
|
||||
passivedodge: 0,
|
||||
get passive() { return clamp(compiled.defense.static + compiled.defense.passivedodge + compiled.defense.passiveparry, 0, compiled.defense.hardcap) },
|
||||
get parry() { return clamp(compiled.defense.static + compiled.defense.passivedodge + compiled.defense.activeparry, 0, compiled.defense.hardcap) },
|
||||
get dodge() { return clamp(compiled.defense.static + compiled.defense.activedodge + compiled.defense.passiveparry, 0, compiled.defense.hardcap) },
|
||||
},
|
||||
mastery: {
|
||||
strength: 0,
|
||||
dexterity: 0,
|
||||
shield: 0,
|
||||
armor: 0,
|
||||
multiattack: 1,
|
||||
magicpower: 0,
|
||||
magicspeed: 0,
|
||||
magicelement: 0,
|
||||
magicinstinct: 0,
|
||||
},
|
||||
bonus: {
|
||||
abilities: {},
|
||||
defense: {},
|
||||
},
|
||||
resistance: {},
|
||||
initiative: 0,
|
||||
capacity: 0,
|
||||
lists: {
|
||||
action: [],
|
||||
freeaction: [],
|
||||
reaction: [],
|
||||
passive: [],
|
||||
spells: [],
|
||||
},
|
||||
aspect: "",
|
||||
notes: Object.assign({ public: '', private: '' }, character.notes),
|
||||
}
|
||||
return compiled;
|
||||
};
|
||||
const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (character: Character) => ({
|
||||
id: character.id,
|
||||
owner: character.owner,
|
||||
username: character.username,
|
||||
name: character.name,
|
||||
health: 0,
|
||||
mana: 0,
|
||||
race: character.people!,
|
||||
modifier: MAIN_STATS.reduce((p, v) => { p[v] = 0; return p; }, {} as Record<MainStat, number>),
|
||||
level: character.level,
|
||||
variables: character.variables,
|
||||
action: 0,
|
||||
reaction: 0,
|
||||
exhaust: 0,
|
||||
itempower: 0,
|
||||
features: {
|
||||
action: [],
|
||||
reaction: [],
|
||||
freeaction: [],
|
||||
passive: [],
|
||||
},
|
||||
abilities: {
|
||||
athletics: 0,
|
||||
acrobatics: 0,
|
||||
intimidation: 0,
|
||||
sleightofhand: 0,
|
||||
stealth: 0,
|
||||
survival: 0,
|
||||
investigation: 0,
|
||||
history: 0,
|
||||
religion: 0,
|
||||
arcana: 0,
|
||||
understanding: 0,
|
||||
perception: 0,
|
||||
performance: 0,
|
||||
medecine: 0,
|
||||
persuasion: 0,
|
||||
animalhandling: 0,
|
||||
deception: 0
|
||||
},
|
||||
spellslots: 0,
|
||||
artslots: 0,
|
||||
spellranks: {
|
||||
instinct: 0 as 0 | 1 | 2 | 3,
|
||||
knowledge: 0 as 0 | 1 | 2 | 3,
|
||||
precision: 0 as 0 | 1 | 2 | 3,
|
||||
arts: 0 as 0 | 1 | 2 | 3,
|
||||
},
|
||||
speed: false as number | false,
|
||||
defense: {
|
||||
hardcap: Infinity,
|
||||
static: 6,
|
||||
activeparry: 0,
|
||||
activedodge: 0,
|
||||
passiveparry: 0,
|
||||
passivedodge: 0,
|
||||
},
|
||||
mastery: {
|
||||
strength: 0,
|
||||
dexterity: 0,
|
||||
shield: 0,
|
||||
armor: 0,
|
||||
multiattack: 1,
|
||||
magicpower: 0,
|
||||
magicspeed: 0,
|
||||
magicelement: 0,
|
||||
magicinstinct: 0,
|
||||
},
|
||||
bonus: {
|
||||
abilities: {},
|
||||
defense: {},
|
||||
},
|
||||
resistance: {},
|
||||
initiative: 0,
|
||||
capacity: 0,
|
||||
lists: {
|
||||
action: [],
|
||||
freeaction: [],
|
||||
reaction: [],
|
||||
passive: [],
|
||||
spells: [],
|
||||
},
|
||||
aspect: "",
|
||||
notes: Object.assign({ public: '', private: '' }, character.notes),
|
||||
});
|
||||
export const mainStatTexts: Record<MainStat, string> = {
|
||||
"strength": "Force",
|
||||
"dexterity": "Dextérité",
|
||||
|
|
@ -923,11 +917,11 @@ class LevelPicker 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(2), 'h-[35px] px-[15px]'),
|
||||
]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))];
|
||||
|
|
@ -1305,7 +1299,7 @@ export class CharacterSheet
|
|||
private user: ComputedRef<User | null>;
|
||||
private character?: CharacterCompiler;
|
||||
container: RedrawableHTML = div('flex flex-1 h-full w-full items-start justify-center');
|
||||
private tabs?: RedrawableHTML & { refresh: () => void };
|
||||
private tabs?: RedrawableHTML;
|
||||
private tab: string = 'abilities';
|
||||
|
||||
ws?: Socket;
|
||||
|
|
@ -1333,6 +1327,22 @@ 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];
|
||||
if(variable.action === 'set')
|
||||
this.character?.variable(variable.key, variable.value, false);
|
||||
else if(Array.isArray(prop))
|
||||
{
|
||||
if(variable.action === 'add')
|
||||
prop.push(variable.value);
|
||||
else if(variable.action === 'remove')
|
||||
{
|
||||
const idx = prop.findIndex(e => deepEquals(e, variable.value));
|
||||
if(idx !== -1) prop.splice(idx, 1);
|
||||
}
|
||||
this.character?.variable(variable.key, prop, false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
document.title = `d[any] - ${character.name}`;
|
||||
|
|
@ -1692,6 +1702,7 @@ export class CharacterSheet
|
|||
])
|
||||
]
|
||||
}
|
||||
//TODO: Update to handle reactivity
|
||||
spellPanel(character: CompiledCharacter)
|
||||
{
|
||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||
|
|
@ -1724,7 +1735,6 @@ export class CharacterSheet
|
|||
}
|
||||
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
|
||||
textAmount.textContent = character.variables.spells.length.toString();
|
||||
this.tabs?.refresh();
|
||||
}, "px-2 py-1 text-sm font-normal");
|
||||
toggleButton.disabled = state === 'given';
|
||||
return foldable(() => [
|
||||
|
|
@ -1761,9 +1771,9 @@ export class CharacterSheet
|
|||
}
|
||||
itemsTab(character: CompiledCharacter)
|
||||
{
|
||||
const power = character.variables.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 = character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
|
||||
const items = this.character!.character.variables.items;
|
||||
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);
|
||||
|
||||
return [
|
||||
div('flex flex-col gap-2', [
|
||||
|
|
@ -1815,7 +1825,7 @@ export class CharacterSheet
|
|||
]),
|
||||
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
||||
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => e.amount?.toString() ?? '-') ]),
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => e.amount ?? '-') ]),
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]),
|
||||
e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight,
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.charge ? `${item.charge}` : '-') ]),
|
||||
|
|
|
|||
|
|
@ -497,32 +497,24 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: Re
|
|||
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): RedrawableHTML
|
||||
{
|
||||
let focus = settings?.focused ?? tabs[0]?.id;
|
||||
const lazyTabs = tabs.map((e, i) => ({
|
||||
title: dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
|
||||
if(this.hasAttribute('data-focus'))
|
||||
return;
|
||||
const titles = tabs.map(e => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
|
||||
if(this.hasAttribute('data-focus'))
|
||||
return;
|
||||
|
||||
if(settings?.switch && settings.switch(e.id) === false)
|
||||
return;
|
||||
if(settings?.switch && settings.switch(e.id) === false)
|
||||
return;
|
||||
|
||||
lazyTabs.forEach(e => e.title.toggleAttribute('data-focus', false));
|
||||
this.toggleAttribute('data-focus', true);
|
||||
focus = e.id;
|
||||
|
||||
if(!lazyTabs[i]!.content)
|
||||
{
|
||||
lazyTabs[i]!.content = (typeof e.content === 'function' ? e.content() : e.content);
|
||||
lazyTabs[i]!.content && content.replaceChildren(...lazyTabs[i]!.content?.map(e => typeof e ==='function' ? e() : e)?.filter(e => !!e));
|
||||
}
|
||||
else content.update && content.update(true);
|
||||
}}}, e.title),
|
||||
content: undefined as undefined | NodeChildren,
|
||||
}));
|
||||
titles.forEach(e => e.toggleAttribute('data-focus', false));
|
||||
this.toggleAttribute('data-focus', true);
|
||||
focus = e.id;
|
||||
const lazyContent = typeof e.content === 'function' ? e.content() : e.content;
|
||||
lazyContent && content.replaceChildren(...lazyContent?.map(e => typeof e === 'function' ? e() : e)?.filter(e => !!e));
|
||||
}}}, e.title));
|
||||
const _content = tabs.find(e => e.id === focus)?.content;
|
||||
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
|
||||
|
||||
const container = div(['flex flex-col', settings?.class?.container], [
|
||||
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], lazyTabs.map(e => e.title)),
|
||||
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
|
||||
content
|
||||
]);
|
||||
return container as RedrawableHTML;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
||||
import { loading } from './components.util';
|
||||
import { deepEquals } from './general.util';
|
||||
|
||||
export type RedrawableHTML = HTMLElement & { update?: (recursive: boolean) => void }
|
||||
export type Node = HTMLElement & { update?: (recursive: boolean) => void } | SVGElement | Text | undefined;
|
||||
|
|
@ -19,7 +20,7 @@ export interface DOMList<T> extends Array<T>{
|
|||
export interface NodeProperties
|
||||
{
|
||||
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
|
||||
text?: Reactive<string | Text>;
|
||||
text?: Reactive<string | number | Text>;
|
||||
class?: Class;
|
||||
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
|
||||
listeners?: {
|
||||
|
|
@ -27,19 +28,103 @@ export interface NodeProperties
|
|||
};
|
||||
}
|
||||
|
||||
let defered = false, _set = new Set<() => void>();
|
||||
let defered = false, _deferSet = new Set<() => void>();
|
||||
const _defer = (fn: () => void) => {
|
||||
if(!defered)
|
||||
{
|
||||
defered = true;
|
||||
queueMicrotask(() => {
|
||||
_set.forEach(e => e());
|
||||
_set.clear();
|
||||
_deferSet.forEach(e => e());
|
||||
_deferSet.clear();
|
||||
defered = false;
|
||||
});
|
||||
}
|
||||
|
||||
_set.add(fn);
|
||||
_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();
|
||||
|
|
@ -128,11 +213,14 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||
|
||||
if(properties?.text && (setup || typeof properties.text === 'function'))
|
||||
{
|
||||
const text = typeof properties.text === 'function' ? properties.text() : properties.text;
|
||||
if(typeof text === 'string')
|
||||
element.textContent = text;
|
||||
else
|
||||
element.appendChild(text as Text);
|
||||
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?.listeners && setup)
|
||||
|
|
@ -164,7 +252,7 @@ export function div<U extends any>(cls?: Class, children?: NodeChildren | { rend
|
|||
//@ts-expect-error
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function span(cls?: Class, text?: Reactive<string | 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 });
|
||||
}
|
||||
|
|
@ -187,104 +275,11 @@ export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: N
|
|||
|
||||
return element;
|
||||
}
|
||||
export function text(data: string): Text;
|
||||
export function text(data: {}, _txt: Reactive<string>): Text;
|
||||
export function text(data: any, _txt?: Reactive<string>): Text
|
||||
export function text(data: Reactive<string | number>): Text
|
||||
{
|
||||
if(typeof data === 'string')
|
||||
return document.createTextNode(data);
|
||||
else if(_txt)
|
||||
{
|
||||
const cache = new Map<string, number>();
|
||||
let txtCache = (typeof _txt === 'function' ? _txt() : _txt);
|
||||
const setup = (property: string) => {
|
||||
const prop = property.split('.');
|
||||
let obj = data;
|
||||
|
||||
for(let i = 0; i < prop.length - 1; i++)
|
||||
{
|
||||
if(prop[i]! in obj) obj = obj[prop[i]!];
|
||||
else return 0;
|
||||
}
|
||||
|
||||
const last = prop.slice(-1)[0]!;
|
||||
if(last in obj)
|
||||
{
|
||||
const prototype = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), last);
|
||||
let clone = obj[last];
|
||||
delete obj[last];
|
||||
Object.defineProperty(obj, last, { ...prototype, get: () => prototype?.get ? prototype.get() : clone, set: (v) => { if(prototype?.set) { prototype.set(v); clone = obj[last]; } else if(!prototype?.get) { clone = v; } cache.set(property, v); replace(); }, enumerable: true, configurable: true, });
|
||||
cache.set(property, clone);
|
||||
|
||||
return obj[last];
|
||||
}
|
||||
else return 0;
|
||||
}
|
||||
const apply = (_setup: boolean) => txtCache.replace(/\{\{(.+?)\}\}/g, (_, txt: string) => {
|
||||
let i = 0, current = 0, property = '', nextOp = '';
|
||||
const _compute = () => {
|
||||
if(property.length > 0)
|
||||
{
|
||||
let value = 0;
|
||||
if(_setup)
|
||||
value = setup(property);
|
||||
else
|
||||
value = cache.get(property)!;
|
||||
|
||||
if(nextOp === '+')
|
||||
current += value;
|
||||
else if(nextOp === '-')
|
||||
current -= value;
|
||||
else if(nextOp === '*')
|
||||
current *= value;
|
||||
else if(nextOp === '/')
|
||||
current /= value;
|
||||
else if(nextOp === '%')
|
||||
current /= value;
|
||||
else if(nextOp === '')
|
||||
current = value;
|
||||
|
||||
nextOp = '';
|
||||
property = '';
|
||||
}
|
||||
}
|
||||
while(i < txt.length)
|
||||
{
|
||||
switch(txt.charAt(i))
|
||||
{
|
||||
case '+':
|
||||
case '-':
|
||||
case '*':
|
||||
case '/':
|
||||
case '%':
|
||||
_compute();
|
||||
|
||||
nextOp = txt.charAt(i).trim();
|
||||
break;
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
property += txt.charAt(i);
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
_compute();
|
||||
return current.toString();
|
||||
});
|
||||
const replace = () => {
|
||||
const txt = (typeof _txt === 'function' ? _txt() : _txt);
|
||||
if(txt !== txtCache)
|
||||
{
|
||||
txtCache = txt;
|
||||
node.textContent = apply(true);
|
||||
}
|
||||
else node.textContent = apply(false);
|
||||
};
|
||||
const node = document.createTextNode(apply(true));
|
||||
return node;
|
||||
}
|
||||
else return document.createTextNode('');
|
||||
const text = document.createTextNode('');
|
||||
requireReactive(data, (txt) => text.textContent = txt.toString());
|
||||
return text;
|
||||
}
|
||||
export function styling(element: SVGElement | RedrawableHTML, properties: {
|
||||
class?: Class;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,43 @@ export function padRight(text: string, pad: string, length: number): string
|
|||
{
|
||||
return pad.repeat(length - text.length).concat(text);
|
||||
}
|
||||
export function deepEquals(a: any, b: any): boolean
|
||||
{
|
||||
if(a === b) return true;
|
||||
|
||||
if (a && b && typeof a == 'object' && typeof b == 'object')
|
||||
{
|
||||
if (a.constructor !== b.constructor) return false;
|
||||
|
||||
let length, i, keys;
|
||||
if (Array.isArray(a))
|
||||
{
|
||||
length = a.length;
|
||||
if (length != b.length) return false;
|
||||
for (i = length; i-- !== 0;)
|
||||
if (!deepEquals(a[i], b[i])) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
|
||||
if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
|
||||
if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
|
||||
|
||||
keys = Object.keys(a) as Array<keyof typeof a>;
|
||||
length = keys.length;
|
||||
if (length !== Object.keys(b).length) return false;
|
||||
|
||||
for (i = length; i-- !== 0;)
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i]!)) return false;
|
||||
|
||||
for (i = length; i-- !== 0;)
|
||||
if(!deepEquals(a[keys[i]!], b[keys[i]!])) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return a !== a && b !== b;
|
||||
}
|
||||
export function format(date: Date, template: string): string
|
||||
{
|
||||
const months = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue