fix(panel): switch between multiple views correctly

update single select instance to avoid conflicting rerender listeners
This commit is contained in:
dragonwocky 2024-02-24 23:36:43 +11:00
parent 951b199b54
commit b910f82acf
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
3 changed files with 90 additions and 93 deletions

View File

@ -50,27 +50,27 @@ function View({ _get }) {
function Switcher({ _get, _set, minWidth, maxWidth }) { function Switcher({ _get, _set, minWidth, maxWidth }) {
const { html, useState } = globalThis.__enhancerApi, const { html, useState } = globalThis.__enhancerApi,
$switcher = html`<div $select = html`<${Select}
class="relative flex items-center grow popupMode="dropdown"
font-medium p-[8.5px] ml-[4px] select-none" class="w-full text-left"
></div>`, maxWidth=${maxWidth - 56}
setView = (view) => _set?.(view); minWidth=${minWidth - 56}
...${{ _get, _set }}
/>`;
useState(["panelViews"], ([panelViews = []]) => { useState(["panelViews"], ([panelViews = []]) => {
const values = panelViews.map(([{ title, $icon }]) => { const values = panelViews.map(([{ title, $icon }]) => {
// panel switcher internally uses the select island, // panel switcher internally uses the select island,
// which expects an option value rather than a title // which expects an option value rather than a title
return { value: title, $icon }; return { value: title, $icon };
}); });
$switcher.innerHTML = ""; $select.setValues(values);
$switcher.append(html`<${Select}
popupMode="dropdown"
class="w-full text-left"
maxWidth=${maxWidth - 56}
minWidth=${minWidth - 56}
...${{ _get, _set: setView, values }}
/>`);
}); });
return $switcher; return html`<div
class="relative flex items-center grow
font-medium p-[8.5px] ml-[4px] select-none"
>
${$select}
</div>`;
} }
function Panel({ function Panel({

View File

@ -7,46 +7,40 @@
import { Popup } from "./Popup.mjs"; import { Popup } from "./Popup.mjs";
function Option({ $icon = "", value = "", _get, _set }) { function Option({ $icon = "", value = "", _get, _set }) {
const { html, useState } = globalThis.__enhancerApi, const { html, useState } = globalThis.__enhancerApi;
$selected = html`<i class="ml-auto i-check size-[16px]"></i>`, return html`<div
$option = html`<div tabindex="0"
tabindex="0" role="option"
role="option" class="select-none cursor-pointer rounded-[3px]
class="select-none cursor-pointer rounded-[3px] flex items-center w-full h-[28px] px-[12px] leading-[1.2]
flex items-center w-full h-[28px] px-[12px] leading-[1.2] transition duration-[20ms] focus:bg-[color:var(--theme--bg-hover)]"
transition duration-[20ms] focus:bg-[color:var(--theme--bg-hover)]" onmouseover=${(event) => event.target.focus()}
onmouseover=${(event) => event.target.focus()} onclick=${() => _set?.(value)}
onclick=${() => _set?.(value)} onkeydown=${(event) => {
onkeydown=${(event) => { if (["Enter", " "].includes(event.key)) _set?.(value);
// if (["Enter", " "].includes(event.key)) _set?.(value); }}
}} >
<div
class="mr-[6px] inline-flex items-center gap-[6px]
text-[14px] text-ellipsis overflow-hidden"
> >
<div ${$icon}<span>${value}</span>
class="mr-[6px] inline-flex items-center gap-[6px] </div>
text-[14px] text-ellipsis overflow-hidden" </div>`;
>
${$icon}<span>${value}</span>
</div>
</div>`;
useState(["rerender"], async () => {
if ((await _get?.()) === value) {
$option.append($selected);
} else $selected.remove();
});
return $option;
} }
function Select({ function Select({
values,
_get, _get,
_set, _set,
_requireReload = true, _requireReload = true,
values = [],
popupMode = "left", popupMode = "left",
maxWidth = 256, maxWidth = 256,
minWidth = 48, minWidth = 48,
...props ...props
}) { }) {
const { html, extendProps, setState, useState } = globalThis.__enhancerApi, const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
$selected = html`<i class="ml-auto i-check size-[16px]"></i>`,
// dir="rtl" overflows to the left during transition // dir="rtl" overflows to the left during transition
$select = html`<div $select = html`<div
dir="rtl" dir="rtl"
@ -57,64 +51,66 @@ function Select({
cursor-pointer text-[14px] overflow-hidden pr-[28px] cursor-pointer text-[14px] overflow-hidden pr-[28px]
transition duration-[20ms] leading-[28px] pl-[8px] transition duration-[20ms] leading-[28px] pl-[8px]
hover:bg-[color:var(--theme--bg-hover)]" hover:bg-[color:var(--theme--bg-hover)]"
></div>`; ></div>`,
$popup = html`<div></div>`,
let xyz;
const options = values.map((opt) => {
if (["string", "number"].includes(typeof opt)) opt = { value: opt };
if (!(opt?.$icon instanceof Element)) {
if (typeof opt?.$icon === "string") {
opt.$icon = html`<i class="i-${opt.$icon} size-[16px]" />`;
} else delete opt.$icon;
}
return {
...opt,
$option: html`<${Option} ...${{ ...opt, _get, _set }} />`,
$value: html`<div
class="inline-flex text-nowrap items-center gap-[6px]"
>
<!-- swap icon/value order for correct display when dir="rtl" -->
<span>${opt.value}</span>${opt.$icon?.cloneNode(true) ?? ""}
</div>`,
};
}),
getSelected = async () => {
const value = (await _get?.()) ?? $select.innerText,
option = options.find((opt) => opt.value === value);
if (!option) {
console.log(1, options, options.length, options === xyz);
_set?.(options[0].value);
}
return option || options[0];
},
onKeydown = (event) => { onKeydown = (event) => {
// const intercept = () => { const intercept = () => {
// event.preventDefault(); event.preventDefault();
// event.stopPropagation(); event.stopPropagation();
// }; };
// if (event.key === "Escape") { if (event.key === "Escape") {
// intercept(setState({ rerender: true })); intercept(setState({ rerender: true }));
// } else if (!options.length) return; } else if (!options.length) return;
// // prettier-ignore // prettier-ignore
// const $next = options.find(({ $option }) => $option === event.target) const $next = options.find(({ $option }) => $option === event.target)
// ?.$option.nextElementSibling ?? options.at(0).$option, ?.$option.nextElementSibling ?? options.at(0).$option,
// $prev = options.find(({ $option }) => $option === event.target) $prev = options.find(({ $option }) => $option === event.target)
// ?.$option.previousElementSibling ?? options.at(-1).$option; ?.$option.previousElementSibling ?? options.at(-1).$option;
// // overflow to opposite end of list from dir of travel // overflow to opposite end of list from dir of travel
// if (event.key === "ArrowUp") intercept($prev.focus()); if (event.key === "ArrowUp") intercept($prev.focus());
// if (event.key === "ArrowDown") intercept($next.focus()); if (event.key === "ArrowDown") intercept($next.focus());
// // re-enable natural tab behaviour in notion interface // re-enable natural tab behaviour in notion interface
// if (event.key === "Tab") event.stopPropagation(); if (event.key === "Tab") event.stopPropagation();
}; };
xyz = options;
console.log(2, options, options.length, options === xyz); let options = [];
const valueToOption = (opt) => {
if (["string", "number"].includes(typeof opt)) opt = { value: opt };
if (!(opt?.$icon instanceof Element)) {
if (typeof opt?.$icon === "string") {
opt.$icon = html`<i class="i-${opt.$icon} size-[16px]" />`;
} else delete opt.$icon;
}
const $icon = opt.$icon?.cloneNode(true);
return {
...opt,
$option: html`<${Option} ...${{ ...opt, _get, _set }} />`,
$value: html`<div class="inline-flex text-nowrap items-center gap-[6px]">
<!-- swap icon/value order for correct display when dir="rtl" -->
<span>${opt.label || opt.value}</span>${$icon ?? ""}
</div>`,
};
};
$select.setValues = (values) => {
options = values.map(valueToOption);
$popup.innerHTML = "";
$popup.append(...options.map(({ $option }) => $option));
};
$select.setValues(values);
let _initialValue; let _initialValue;
const getSelected = async () => {
const value = (await _get?.()) ?? $select.innerText,
option = options.find((opt) => opt.value === value);
if (!option) _set?.(options[0].value);
return option || options[0];
};
useState(["rerender"], async () => { useState(["rerender"], async () => {
if (!options.length) return; if (!options.length) return;
const { value, $value } = await getSelected(); const { value, $value, $option } = await getSelected();
$select.innerHTML = ""; $select.innerHTML = "";
$select.append($value); $select.append($value);
$option.append($selected);
if (_requireReload) { if (_requireReload) {
_initialValue ??= value; _initialValue ??= value;
if (value !== _initialValue) setState({ databaseUpdated: true }); if (value !== _initialValue) setState({ databaseUpdated: true });
@ -122,7 +118,7 @@ function Select({
}); });
extendProps(props, { class: "notion-enhancer--menu-select relative" }); extendProps(props, { class: "notion-enhancer--menu-select relative" });
return html`<div ...${props}> return html`<div ...${props} setValues=${$select.setValues}>
${$select}<${Popup} ${$select}<${Popup}
tabindex="0" tabindex="0"
trigger=${$select} trigger=${$select}
@ -137,7 +133,7 @@ function Select({
$select.style.width = ""; $select.style.width = "";
$select.style.background = ""; $select.style.background = "";
}} }}
>${options.map(({ $option }) => $option)} >${$popup}
<//> <//>
<i <i
class="i-chevron-down pointer-events-none class="i-chevron-down pointer-events-none

View File

@ -42,6 +42,7 @@ export default async (api, db) => {
let $page, $scroller; let $page, $scroller;
const getHeadings = () => { const getHeadings = () => {
if (!$page) return [];
return [...$page.querySelectorAll(headings.join(", "))]; return [...$page.querySelectorAll(headings.join(", "))];
}, },
getHeadingLevel = ($heading) => { getHeadingLevel = ($heading) => {