Add combobox groups
This commit is contained in:
parent
920ce2e1b6
commit
4e5ea504ea
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,12 +1,13 @@
|
||||||
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, TrainingOption } from "~/types/character";
|
import type { Ability, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character";
|
||||||
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
|
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
|
||||||
import { MarkdownEditor } from "./editor.util";
|
import { MarkdownEditor } from "./editor.util";
|
||||||
import { button, combobox, fakeA, input, numberpicker, select } from "./proses";
|
import { button, combobox, fakeA, numberpicker, select, type Option } from "./proses";
|
||||||
import { popper, tooltip } from "./floating.util";
|
import { tooltip } from "./floating.util";
|
||||||
import { mainStatShortTexts, mainStatTexts } from "./character.util";
|
import { mainStatShortTexts, mainStatTexts } from "./character.util";
|
||||||
import config from "#shared/character-config.json";
|
import config from "#shared/character-config.json";
|
||||||
import { getID, ID_SIZE } from "./general.util";
|
import { getID, ID_SIZE } from "./general.util";
|
||||||
import renderMarkdown from "./markdown.util";
|
import renderMarkdown from "./markdown.util";
|
||||||
|
import { Tree } from "./tree";
|
||||||
|
|
||||||
export class FeatureEditor
|
export class FeatureEditor
|
||||||
{
|
{
|
||||||
|
|
@ -74,7 +75,7 @@ export class FeatureEditor
|
||||||
}
|
}
|
||||||
private _renderEffect(effect: FeatureItem): HTMLDivElement
|
private _renderEffect(effect: FeatureItem): HTMLDivElement
|
||||||
{
|
{
|
||||||
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-stretch', [
|
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [
|
||||||
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
|
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
|
||||||
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
|
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
|
||||||
this._table.replaceChild(this._edit(effect), content);
|
this._table.replaceChild(this._edit(effect), content);
|
||||||
|
|
@ -91,11 +92,11 @@ export class FeatureEditor
|
||||||
switch(effect.category)
|
switch(effect.category)
|
||||||
{
|
{
|
||||||
case 'value':
|
case 'value':
|
||||||
return choices.find(e => e.value.category === 'value' && e.value.property === effect.property)?.value;
|
return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property);
|
||||||
/* case 'choice':
|
/* case 'choice':
|
||||||
return choices.find(e => e.value.category === 'choice' && e.value. === effect.property); */
|
return choices.find(e => e.value.category === 'choice' && e.value. === effect.property); */
|
||||||
case 'list':
|
case 'list':
|
||||||
return choices.find(e => e.value.category === 'list' && e.value.list === effect.list)?.value;
|
return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const approve = () => {
|
const approve = () => {
|
||||||
|
|
@ -124,8 +125,8 @@ export class FeatureEditor
|
||||||
case 'value':
|
case 'value':
|
||||||
const summaryText = text(textFromEffect(buffer));
|
const summaryText = text(textFromEffect(buffer));
|
||||||
top = [
|
top = [
|
||||||
select([ { text: '+', value: 'add' }, ['speed', 'capacity'].includes(buffer.property) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' } }),
|
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-[80px]' } }),
|
||||||
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
|
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
|
||||||
button(icon('radix-icons:update'), () => {
|
button(icon('radix-icons:update'), () => {
|
||||||
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
|
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
|
||||||
const element = redraw();
|
const element = redraw();
|
||||||
|
|
@ -134,7 +135,7 @@ export class FeatureEditor
|
||||||
summaryText.textContent = textFromEffect(buffer);
|
summaryText.textContent = textFromEffect(buffer);
|
||||||
}, 'px-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
|
}, 'px-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
|
||||||
];
|
];
|
||||||
bottom = [summaryText];
|
bottom = [div('px-2 py-1', [summaryText])];
|
||||||
break;
|
break;
|
||||||
case 'list':
|
case 'list':
|
||||||
if(buffer.action === 'add')
|
if(buffer.action === 'add')
|
||||||
|
|
@ -149,16 +150,16 @@ export class FeatureEditor
|
||||||
editor.content = buffer.item;
|
editor.content = buffer.item;
|
||||||
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
|
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
|
||||||
|
|
||||||
bottom = [ div('px-4 py-1', [ editor.dom ]) ];
|
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1', [ editor.dom ]) ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-32' } }) ];
|
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-32' } }) ];
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
return div('border border-light-30 dark:border-dark-30 col-span-1 row-span-2', [ div('flex justify-between items-stretch', [
|
return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [
|
||||||
div('flex flex-row', [
|
div('flex flex-row', [
|
||||||
combobox(choices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[250px] -m-px h-[36px]' }, change: (e) => {
|
combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px h-[36px]' }, change: (e) => {
|
||||||
buffer = { id: buffer.id, ...e } as FeatureItem;
|
buffer = { id: buffer.id, ...e } as FeatureItem;
|
||||||
const element = redraw();
|
const element = redraw();
|
||||||
this._table.replaceChild(element, content);
|
this._table.replaceChild(element, content);
|
||||||
|
|
@ -167,7 +168,7 @@ export class FeatureEditor
|
||||||
...top,
|
...top,
|
||||||
]),
|
]),
|
||||||
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), reject, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), reject, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
||||||
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto', bottom) ]);
|
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto items-center', bottom) ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = redraw();
|
let content = redraw();
|
||||||
|
|
@ -179,7 +180,7 @@ export class FeatureEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const choices: Array<{ text: string, value: Partial<FeatureItem> }> = [
|
const featureChoices: Option<Partial<FeatureItem>>[] = [
|
||||||
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, },
|
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, },
|
||||||
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, },
|
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, },
|
||||||
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, },
|
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, },
|
||||||
|
|
@ -190,12 +191,42 @@ const choices: Array<{ text: string, value: Partial<FeatureItem> }> = [
|
||||||
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, },
|
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, },
|
||||||
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, },
|
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, },
|
||||||
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
|
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
|
||||||
|
{ text: 'Défense', value: [
|
||||||
|
{ text: 'Défense max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 0 } }
|
||||||
|
] },
|
||||||
|
{ text: 'Maitrise', value: [
|
||||||
|
{ text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 0 } }
|
||||||
|
] },
|
||||||
|
{ text: 'Compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 0 } })) },
|
||||||
|
{ text: 'Modifier', value: [
|
||||||
|
{ text: 'Modifier de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 0 } },
|
||||||
|
{ text: 'Modifier de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 0 } }
|
||||||
|
] },
|
||||||
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
|
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
|
||||||
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
|
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
|
||||||
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
|
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
|
||||||
{ text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, },
|
{ text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, },
|
||||||
{ text: 'Choix', value: { category: 'choice', options: [] }, },
|
{ text: 'Choix', value: { category: 'choice', options: [] }, },
|
||||||
];
|
];
|
||||||
|
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureItem>[];
|
||||||
function textFromEffect(effect: FeatureItem)
|
function textFromEffect(effect: FeatureItem)
|
||||||
{
|
{
|
||||||
if(effect.category === 'value')
|
if(effect.category === 'value')
|
||||||
|
|
|
||||||
126
shared/proses.ts
126
shared/proses.ts
|
|
@ -6,6 +6,7 @@ import { Canvas } from "#shared/canvas.util";
|
||||||
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
||||||
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
||||||
import { clamp, unifySlug } from "#shared/general.util";
|
import { clamp, unifySlug } from "#shared/general.util";
|
||||||
|
import { Tree } from "./tree";
|
||||||
|
|
||||||
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
||||||
export type Prose = { class: string } | { custom: CustomProse };
|
export type Prose = { class: string } | { custom: CustomProse };
|
||||||
|
|
@ -231,40 +232,23 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
|
||||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
||||||
}
|
}
|
||||||
export function select<T extends NonNullable<any>>(options: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
export type Option<T> = { text: string, value: T | Option<T>[] } | undefined;
|
||||||
|
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T> | undefined> };
|
||||||
|
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||||
{
|
{
|
||||||
const textFromValue = (value?: T): string => {
|
|
||||||
if(!value)
|
|
||||||
return '';
|
|
||||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
|
||||||
if(!found)
|
|
||||||
return '';
|
|
||||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
|
||||||
};
|
|
||||||
let close: Function | undefined;
|
let close: Function | undefined;
|
||||||
|
|
||||||
let disabled = settings?.disabled ?? false;
|
let disabled = settings?.disabled ?? false;
|
||||||
const textValue = text(textFromValue(settings?.defaultValue));
|
const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? '');
|
||||||
const optionElements = options.map(e => {
|
const optionElements = options.map(e => {
|
||||||
if(e === undefined)
|
if(e === undefined)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return dom('div', { listeners: { click: () => {
|
return dom('div', { listeners: { click: () => {
|
||||||
let text, value;
|
textValue.textContent = e.text;
|
||||||
if(typeof e === 'string')
|
settings?.change && settings?.change(e.value);
|
||||||
{
|
|
||||||
text = value = e;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
text = e.text;
|
|
||||||
value = e.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
textValue.textContent = text;
|
|
||||||
settings?.change && settings?.change(value);
|
|
||||||
close && close();
|
close && close();
|
||||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]);
|
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
|
||||||
});
|
});
|
||||||
const select = dom('div', { listeners: { click: () => {
|
const select = dom('div', { listeners: { click: () => {
|
||||||
if(disabled)
|
if(disabled)
|
||||||
|
|
@ -283,70 +267,86 @@ export function select<T extends NonNullable<any>>(options: Array<string | undef
|
||||||
})
|
})
|
||||||
return select;
|
return select;
|
||||||
}
|
}
|
||||||
export function combobox<T extends NonNullable<any>>(options: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||||
{
|
{
|
||||||
const textFromValue = (value?: T): string => {
|
|
||||||
if(!value)
|
|
||||||
return '';
|
|
||||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
|
||||||
if(!found)
|
|
||||||
return '';
|
|
||||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
|
||||||
};
|
|
||||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||||
let selected = true;
|
let selected = true, tree: StoredOption<T>[] = [];
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
if(disabled || (context && context.container.parentElement))
|
if(disabled || (context && context.container.parentElement))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const box = container.getBoundingClientRect();
|
const box = container.getBoundingClientRect();
|
||||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => { if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red') } });
|
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: hide });
|
||||||
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
||||||
};
|
};
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
if(!context || !context.container.parentElement)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.close();
|
|
||||||
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
||||||
};
|
tree = [];
|
||||||
|
|
||||||
let disabled = settings?.disabled ?? false;
|
context && context.container.parentElement && context.close();
|
||||||
const optionElements = options.map((e, i) => {
|
};
|
||||||
if(e === undefined)
|
const progress = (option: StoredOption<T>) => {
|
||||||
|
if(!context || !context.container.parentElement || option.container === undefined)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return { item: e, dom: dom('div', { listeners: { click: () => {
|
const redrawn = render(option.item)?.container;
|
||||||
let text, value;
|
if(redrawn)
|
||||||
if(typeof e === 'string')
|
|
||||||
{
|
{
|
||||||
text = value = e;
|
context.container.replaceChildren(redrawn);
|
||||||
|
tree.push(option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const back = () => {
|
||||||
|
tree.pop();
|
||||||
|
const last = tree.slice(-1)[0];
|
||||||
|
|
||||||
|
last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
|
||||||
|
};
|
||||||
|
const render = (option: Option<T>): StoredOption<T> | undefined => {
|
||||||
|
if(option === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(Array.isArray(option.value))
|
||||||
|
{
|
||||||
|
const children = option.value.map(render);
|
||||||
|
|
||||||
|
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored) }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
|
||||||
|
return stored;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
text = e.text;
|
return { item: option, dom: dom('div', { listeners: { click: () => {
|
||||||
value = e.value;
|
select.value = option.text;
|
||||||
}
|
settings?.change && settings?.change(option.value as T);
|
||||||
|
|
||||||
select.value = text;
|
|
||||||
settings?.change && settings?.change(value);
|
|
||||||
selected = true;
|
selected = true;
|
||||||
hide();
|
hide();
|
||||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]) };
|
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(option.text) ]) };
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
const filter = (value: string, option?: StoredOption<T>): HTMLElement[] => {
|
||||||
|
if(option && option.children !== undefined)
|
||||||
|
{
|
||||||
|
return option.children.flatMap(e => filter(value, e));
|
||||||
|
}
|
||||||
|
else if(option && option.item)
|
||||||
|
{
|
||||||
|
return option.item.text.toLowerCase().normalize().includes(value) ? [ option.dom ] : [];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let disabled = settings?.disabled ?? false;
|
||||||
|
const optionElements = options.map(render);
|
||||||
const select = dom('input', { listeners: { focus: show, input: () => {
|
const select = dom('input', { listeners: { focus: show, input: () => {
|
||||||
context && context?.container.replaceChildren(...optionElements.filter(e => {
|
context && select.value ? context.container.replaceChildren(...optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e))) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
|
||||||
if(e === undefined)
|
|
||||||
return false;
|
|
||||||
if(typeof e.item === 'string')
|
|
||||||
return (e.item as string).toLowerCase().includes(select.value.toLowerCase());
|
|
||||||
return e.item.text.toLowerCase().includes(select.value.toLowerCase());
|
|
||||||
}).map(e => e!.dom));
|
|
||||||
selected = false;
|
selected = false;
|
||||||
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
||||||
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
||||||
select.value = textFromValue(settings?.defaultValue);
|
settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });
|
||||||
|
|
||||||
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);
|
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);
|
||||||
|
|
||||||
|
|
|
||||||
2723
shared/test.ts
2723
shared/test.ts
File diff suppressed because it is too large
Load Diff
|
|
@ -111,6 +111,22 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
|
||||||
|
|
||||||
return recursive(1, this._data);
|
return recursive(1, this._data);
|
||||||
}
|
}
|
||||||
|
static each<T extends Record<string, any>>(tree: Array<T | undefined>, children: keyof T, callback: (item: T, depth: number, parent?: T) => void)
|
||||||
|
{
|
||||||
|
const recursive = (depth: number, data?: Array<T | undefined>, parent?: T) => data?.forEach(e => { if(!e) return; callback(e, depth, parent); !Array.isArray(e[children]) ? undefined : recursive(depth + 1, e[children] as T[] | undefined, e) });
|
||||||
|
|
||||||
|
recursive(1, tree);
|
||||||
|
}
|
||||||
|
static accumulate<T extends Record<string, any>>(tree: Array<T | undefined>, children: keyof T, callback: (item: T, depth: number, parent?: T) => any): any[]
|
||||||
|
{
|
||||||
|
const recursive = (depth: number, data?: Array<T | undefined>, parent?: T): any[] => data?.flatMap(e => e && [callback(e, depth, parent), ...!Array.isArray(e[children]) ? [] : recursive(depth + 1, e[children] as T[] | undefined, e)]) ?? [];
|
||||||
|
|
||||||
|
return recursive(1, tree);
|
||||||
|
}
|
||||||
|
static flatten<T extends Record<string, any>>(tree: T[], children: keyof T): T[]
|
||||||
|
{
|
||||||
|
return Tree.accumulate(tree, children, (item) => item);
|
||||||
|
}
|
||||||
get data()
|
get data()
|
||||||
{
|
{
|
||||||
return this._data;
|
return this._data;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue