mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 15:59:03 +00:00
139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
/**
|
|
* notion-enhancer
|
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
|
* (https://notion-enhancer.github.io/) under the MIT license
|
|
*/
|
|
|
|
import { Popup } from "./Popup.mjs";
|
|
|
|
function Option({ $icon = "", value = "", _get, _set }) {
|
|
const { html, useState } = globalThis.__enhancerApi,
|
|
$selected = html`<i class="ml-auto i-check w-[16px] h-[16px]"></i>`,
|
|
$option = html`<div
|
|
tabindex="0"
|
|
role="option"
|
|
class="select-none cursor-pointer rounded-[3px]
|
|
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
|
transition duration-[20ms] focus:bg-[color:var(--theme--bg-hover)]"
|
|
onmouseover=${(event) => event.target.focus()}
|
|
onclick=${() => _set?.(value)}
|
|
onkeydown=${(event) => {
|
|
if (["Enter", " "].includes(event.key)) _set?.(value);
|
|
}}
|
|
>
|
|
<div
|
|
class="mr-[6px] inline-flex items-center gap-[6px]
|
|
text-[14px] text-ellipsis overflow-hidden"
|
|
>
|
|
${$icon}<span>${value}</span>
|
|
</div>
|
|
</div>`;
|
|
useState(["rerender"], async () => {
|
|
if ((await _get?.()) === value) {
|
|
$option.append($selected);
|
|
} else $selected.remove();
|
|
});
|
|
return $option;
|
|
}
|
|
|
|
function Select({
|
|
values,
|
|
_get,
|
|
_set,
|
|
_requireReload = true,
|
|
popupMode = "left",
|
|
maxWidth = 256,
|
|
...props
|
|
}) {
|
|
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
|
// dir="rtl" overflows to the left during transition
|
|
$select = html`<div
|
|
dir="rtl"
|
|
role="button"
|
|
tabindex="0"
|
|
class="appearance-none bg-transparent rounded-[4px]
|
|
cursor-pointer text-[14px] leading-[28px] h-[28px]
|
|
max-w-[${maxWidth}px] pl-[8px] pr-[28px] transition
|
|
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
|
></div>`;
|
|
|
|
const options = values.map((opt) => {
|
|
if (typeof opt === "string") opt = { value: opt };
|
|
if (!(opt?.$icon instanceof Element)) {
|
|
if (opt?.icon && typeof opt.icon === "string") {
|
|
opt.$icon = html`<i class="i-${opt.icon} h-[16px] w-[16px]" />`;
|
|
} else delete opt.$icon;
|
|
}
|
|
opt.$option = html`<${Option} ...${{ ...opt, _get, _set }} />`;
|
|
opt.$selection = html`<div class="inline-flex items-center gap-[6px]">
|
|
<!-- swap icon/value order for correct display when dir="rtl" -->
|
|
<span>${opt.value}</span>${opt.$icon?.cloneNode(true) ?? ""}
|
|
</div>`;
|
|
return opt;
|
|
}),
|
|
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];
|
|
},
|
|
onKeydown = (event) => {
|
|
const intercept = () => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
};
|
|
if (event.key === "Escape") {
|
|
intercept(setState({ rerender: true }));
|
|
} else if (!options.length) return;
|
|
// prettier-ignore
|
|
const $next = options.find(({ $option }) => $option === event.target)
|
|
?.$option.nextElementSibling ?? options.at(0).$option,
|
|
$prev = options.find(({ $option }) => $option === event.target)
|
|
?.$option.previousElementSibling ?? options.at(-1).$option;
|
|
// overflow to opposite end of list from dir of travel
|
|
if (event.key === "ArrowUp") intercept($prev.focus());
|
|
if (event.key === "ArrowDown") intercept($next.focus());
|
|
// re-enable natural tab behaviour in notion interface
|
|
if (event.key === "Tab") event.stopPropagation();
|
|
};
|
|
|
|
let _initialValue;
|
|
useState(["rerender"], async () => {
|
|
if (!options.length) return;
|
|
const { value, $selection } = await getSelected();
|
|
$select.innerHTML = "";
|
|
$select.append($selection);
|
|
if (_requireReload) {
|
|
_initialValue ??= value;
|
|
if (value !== _initialValue) setState({ databaseUpdated: true });
|
|
}
|
|
});
|
|
|
|
extendProps(props, { class: "notion-enhancer--menu-select relative" });
|
|
return html`<div ...${props}>
|
|
${$select}<${Popup}
|
|
tabindex="0"
|
|
trigger=${$select}
|
|
mode=${popupMode}
|
|
onopen=${() => document.addEventListener("keydown", onKeydown, true)}
|
|
onbeforeclose=${() => {
|
|
document.removeEventListener("keydown", onKeydown, true);
|
|
$select.style.width = `${$select.offsetWidth}px`;
|
|
$select.style.background = "transparent";
|
|
}}
|
|
onclose=${() => {
|
|
$select.style.width = "";
|
|
$select.style.background = "";
|
|
}}
|
|
>${options.map(({ $option }) => $option)}
|
|
<//>
|
|
<i
|
|
class="i-chevron-down pointer-events-none
|
|
absolute right-[6px] top-[6px] w-[16px] h-[16px]
|
|
text-[color:var(--theme--fg-secondary)]"
|
|
></i>
|
|
</div>`;
|
|
}
|
|
|
|
export { Select };
|