You've already forked obsidian-visualiser
Various fixes to select, combobox and feature editor.
This commit is contained in:
@@ -235,14 +235,23 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
|
||||
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 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> };
|
||||
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T> | undefined>, index: number };
|
||||
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
|
||||
{
|
||||
let close: Function | undefined;
|
||||
let context: { close: Function };
|
||||
let focused: number | undefined;
|
||||
|
||||
options = options.filter(e => !!e);
|
||||
|
||||
const focus = (i?: number) => {
|
||||
focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
|
||||
i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
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, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
@@ -250,14 +259,40 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
|
||||
textValue.textContent = e.text;
|
||||
settings?.change && settings?.change(e.value);
|
||||
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.text) ]);
|
||||
}, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const handleKeys = (e: KeyboardEvent) => {
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(0);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(optionElements.length - 1);
|
||||
return;
|
||||
case 'enter':
|
||||
focused && optionElements[focused]?.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeys);
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
close = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ 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` } }).close;
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ 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: () => window.removeEventListener('keydown', handleKeys) });
|
||||
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 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] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(select, 'disabled', {
|
||||
@@ -273,12 +308,19 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
{
|
||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||
let selected = true, tree: StoredOption<T>[] = [];
|
||||
let focused: number | undefined;
|
||||
|
||||
const focus = (i?: number) => {
|
||||
focused !== undefined && (tree.slice(-1)[0]?.children ?? optionElements)[focused]?.dom.toggleAttribute('data-focused', false);
|
||||
i !== undefined && (tree.slice(-1)[0]?.children ?? optionElements)[i]?.dom.toggleAttribute('data-focused', true) && (tree.slice(-1)[0]?.children ?? optionElements)[i]?.dom.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
const show = () => {
|
||||
if(disabled || (context && context.container.parentElement))
|
||||
return;
|
||||
|
||||
const box = container.getBoundingClientRect();
|
||||
focus();
|
||||
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');
|
||||
};
|
||||
@@ -292,12 +334,9 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
if(!context || !context.container.parentElement || option.container === undefined)
|
||||
return;
|
||||
|
||||
const redrawn = render(option.item)?.container;
|
||||
if(redrawn)
|
||||
{
|
||||
context.container.replaceChildren(redrawn);
|
||||
tree.push(option);
|
||||
}
|
||||
context.container.replaceChildren(option.container);
|
||||
tree.push(option);
|
||||
focus();
|
||||
};
|
||||
const back = () => {
|
||||
tree.pop();
|
||||
@@ -305,7 +344,7 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
|
||||
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 => {
|
||||
const render = (option: Option<T>, i: number): StoredOption<T> | undefined => {
|
||||
if(option === undefined)
|
||||
return;
|
||||
|
||||
@@ -313,17 +352,17 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
{
|
||||
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 };
|
||||
const stored = { index: i, item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]: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
|
||||
{
|
||||
return { item: option, dom: dom('div', { listeners: { click: () => {
|
||||
return { index: i, item: option, dom: dom('div', { listeners: { click: () => {
|
||||
select.value = option.text;
|
||||
settings?.change && settings?.change(option.value as T);
|
||||
selected = true;
|
||||
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(option.text) ]) };
|
||||
}, mouseenter: () => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(option.text) ]) };
|
||||
}
|
||||
}
|
||||
const filter = (value: string, option?: StoredOption<T>): HTMLElement[] => {
|
||||
@@ -347,6 +386,31 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||
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));
|
||||
selected = false;
|
||||
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
||||
}, keydown: (e) => {
|
||||
|
||||
const opt = (tree.slice(-1)[0]?.item?.value as Option<T>[] ?? options.filter(e => !!e)).filter(e => !!e), elements = (tree.slice(-1)[0]?.children ?? optionElements);
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(clamp((focused ?? -1) + 1, 0, opt.length - 1));
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(clamp((focused ?? 1) - 1, 0, opt.length - 1));
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(0);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(opt.length - 1);
|
||||
return;
|
||||
case 'enter':
|
||||
focused && elements[focused]?.dom.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
} }, 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' });
|
||||
settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user