You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

324 lines
9.2 KiB
Svelte

<!-- To access props and events using reference -->
<svelte:options accessors />
<script>import { getContext, createEventDispatcher, onMount } from "svelte";
export let group = void 0;
export let name = void 0;
export let value = void 0;
export let checked = false;
export let children = [];
export let spacing = "space-x-4";
export let open = getContext("open");
export let selection = getContext("selection");
export let multiple = getContext("multiple");
export let disabled = getContext("disabled");
export let indeterminate = false;
export let padding = getContext("padding");
export let indent = getContext("indent");
export let hover = getContext("hover");
export let rounded = getContext("rounded");
export let caretOpen = getContext("caretOpen");
export let caretClosed = getContext("caretClosed");
export let hyphenOpacity = getContext("hyphenOpacity");
export let regionSummary = getContext("regionSummary");
export let regionSymbol = getContext("regionSymbol");
export let regionChildren = getContext("regionChildren");
export let hideLead = false;
export let hideChildren = false;
let treeItem;
let childrenDiv;
function onSummaryClick(event) {
if (disabled)
event.preventDefault();
}
$:
if (multiple)
updateCheckbox(group, indeterminate);
$:
if (multiple)
updateGroup(checked, indeterminate);
$:
if (!multiple)
updateRadio(group);
$:
if (!multiple)
updateRadioGroup(checked);
let initUpdate = true;
function updateCheckbox(group2, indeterminate2) {
if (!Array.isArray(group2))
return;
checked = group2.indexOf(value) >= 0;
dispatch("groupChange", { checked, indeterminate: indeterminate2 });
dispatch("childChange");
if (initUpdate) {
onParentChange();
initUpdate = false;
}
}
function updateGroup(checked2, indeterminate2) {
if (!Array.isArray(group))
return;
const index = group.indexOf(value);
if (checked2) {
if (index < 0) {
group.push(value);
group = group;
onParentChange();
}
} else {
if (index >= 0) {
group.splice(index, 1);
group = group;
onParentChange();
}
}
}
function updateRadio(group2) {
checked = group2 === value;
dispatch("groupChange", { checked, indeterminate: false });
if (group2)
dispatch("childChange");
}
function updateRadioGroup(checked2) {
if (checked2 && group !== value)
group = value;
else if (!checked2 && group === value)
group = "";
}
function onChildValueChange() {
if (multiple) {
if (!Array.isArray(group))
return;
const childrenValues = children.map((c) => c.value);
const childrenGroup = children[0].group;
const index = group.indexOf(value);
if (children.some((c) => c.indeterminate)) {
indeterminate = true;
if (index >= 0) {
group.splice(index, 1);
group = group;
}
} else if (childrenValues.every((c) => Array.isArray(childrenGroup) && childrenGroup.includes(c))) {
indeterminate = false;
if (index < 0) {
group.push(value);
group = group;
}
} else if (childrenValues.some((c) => Array.isArray(childrenGroup) && childrenGroup.includes(c))) {
indeterminate = true;
if (index >= 0) {
group.splice(index, 1);
group = group;
}
} else {
indeterminate = false;
if (index >= 0) {
group.splice(index, 1);
group = group;
}
}
} else {
if (group !== value && children.some((c) => c.checked)) {
group = value;
} else if (group === value && !children.some((c) => c.checked)) {
group = "";
}
}
dispatch("childChange");
}
export function onParentChange() {
if (!multiple || !children || children.length === 0)
return;
if (!Array.isArray(group))
return;
const index = group.indexOf(value);
const checkChild = (child) => {
if (!child || !Array.isArray(child.group))
return;
child.indeterminate = false;
if (child.group.indexOf(child.value) < 0) {
child.group.push(child.value);
child.group = child.group;
}
};
const uncheckChild = (child) => {
if (!child || !Array.isArray(child.group))
return;
child.indeterminate = false;
const childIndex = child.group.indexOf(child.value);
if (childIndex >= 0) {
child.group.splice(childIndex, 1);
child.group = child.group;
}
};
children.forEach((child) => {
if (!child)
return;
index >= 0 ? checkChild(child) : uncheckChild(child);
child.onParentChange();
});
}
$:
if (!multiple && group !== void 0) {
if (group !== value) {
children.forEach((child) => {
if (child)
child.group = "";
});
}
}
const dispatch = createEventDispatcher();
$:
dispatch("toggle", { open });
$:
children.forEach((child) => {
if (child)
child.$on("childChange", onChildValueChange);
});
function onKeyDown(event) {
function getRootTree() {
let currentElement = treeItem;
while (currentElement !== null) {
if (currentElement.classList.contains("tree"))
return currentElement;
currentElement = currentElement.parentElement;
}
return void 0;
}
let rootTree = void 0;
let lastVisibleElement = null;
switch (event.code) {
case "ArrowRight":
if (!open)
open = true;
else if ($$slots.children && !hideChildren) {
const child = childrenDiv.querySelector("details>summary");
if (child)
child.focus();
}
break;
case "ArrowLeft":
if (open)
open = false;
else {
const parent = treeItem.parentElement?.parentElement;
if (parent && parent.tagName === "DETAILS")
parent.querySelector("summary")?.focus();
}
break;
case "Home":
event.preventDefault();
rootTree = getRootTree();
if (rootTree)
rootTree?.querySelector("summary")?.focus();
break;
case "End":
event.preventDefault();
rootTree = getRootTree();
if (rootTree) {
const detailsElements = rootTree?.querySelectorAll("details");
if (!detailsElements)
return;
for (let i = detailsElements.length - 1; i >= 0; i--) {
const details = detailsElements[i];
if (details.parentElement?.classList?.contains("tree") || details.parentElement?.parentElement?.getAttribute("open") !== null) {
lastVisibleElement = details;
break;
} else if (details.parentElement?.parentElement?.tagName !== "details") {
lastVisibleElement = details.parentElement.parentElement;
break;
}
}
if (lastVisibleElement) {
const summary = lastVisibleElement.querySelector("summary");
if (summary)
summary.focus();
}
}
break;
}
}
const cBase = "";
const cSummary = "list-none [&::-webkit-details-marker]:hidden flex items-center cursor-pointer";
const cSymbol = "fill-current w-3 text-center transition-transform duration-[200ms]";
const cChildren = "";
const cDisabled = "opacity-50 !cursor-not-allowed";
$:
classesCaretState = open && $$slots.children && !hideChildren ? caretOpen : caretClosed;
$:
classesDisabled = disabled ? cDisabled : "";
$:
classesBase = `${cBase} ${$$props.class ?? ""}`;
$:
classesSummary = `${cSummary} ${classesDisabled} ${spacing} ${rounded} ${padding} ${hover} ${regionSummary}`;
$:
classesSymbol = `${cSymbol} ${classesCaret} ${regionSymbol}`;
$:
classesCaret = `${classesCaretState}`;
$:
classesHyphen = `${hyphenOpacity}`;
$:
classesChildren = `${cChildren} ${indent} ${regionChildren}`;
</script>
<details bind:this={treeItem} bind:open class="tree-item {classesBase}" data-testid="tree-item" aria-disabled={disabled}>
<summary
class="tree-item-summary {classesSummary}"
role="treeitem"
aria-selected={selection ? checked : undefined}
aria-expanded={$$slots.children ? open : undefined}
on:click={onSummaryClick}
on:click
on:keydown={onKeyDown}
on:keydown
on:keyup
>
<!-- Symbol -->
<div class="tree-summary-symbol {classesSymbol}">
{#if $$slots.children && !hideChildren}
<!-- SVG Caret -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path
d="M201.4 374.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 306.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"
/>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="w-3 {classesHyphen}">
<path d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z" />
</svg>
{/if}
</div>
<!-- Selection -->
{#if selection && name && group !== undefined}
{#if multiple}
<input
class="checkbox tree-item-checkbox"
type="checkbox"
{name}
{value}
bind:checked
bind:indeterminate
on:change={onParentChange}
/>
{:else}
<input class="radio tree-item-radio" type="radio" bind:group {name} {value} />
{/if}
{/if}
<!-- Slot: Lead -->
{#if $$slots.lead && !hideLead}
<div class="tree-item-lead">
<slot name="lead" />
</div>
{/if}
<!-- Slot: Content -->
<div class="tree-item-content">
<slot />
</div>
</summary>
<div bind:this={childrenDiv} class="tree-item-children {classesChildren}" role="group">
<slot name="children" />
</div>
</details>