212 lines
6.2 KiB
JavaScript
212 lines
6.2 KiB
JavaScript
import {
|
|
Match,
|
|
Show,
|
|
createEffect,
|
|
createMemo,
|
|
createSignal,
|
|
mergeProps,
|
|
} from "solid-js";
|
|
|
|
function List(props) {
|
|
props = mergeProps({ items: [], showHeader: true, selectable: true }, props);
|
|
const [listItems, setListItems] = createSignal([]);
|
|
const selectedItems = createMemo(() =>
|
|
listItems()
|
|
.filter((item) => item.selected())
|
|
.map((item) => item.item)
|
|
);
|
|
const allItemsSelected = createMemo(() =>
|
|
listItems().every((item) => item.selected())
|
|
);
|
|
createEffect(() => {
|
|
setListItems(
|
|
props.items.map((item) => {
|
|
const [selected, setSelected] = createSignal(false);
|
|
return {
|
|
selected,
|
|
setSelected,
|
|
item: item,
|
|
};
|
|
})
|
|
);
|
|
});
|
|
createEffect(() => {
|
|
if (!props.onListItemsSelect) return;
|
|
props.onListItemsSelect(selectedItems());
|
|
});
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
if (props.onLazyLoad) {
|
|
props.onLazyLoad();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
const [lazyLoadTriggerElement, setLazyLoadTriggerElement] = createSignal();
|
|
createEffect(() => {
|
|
observer.observe(lazyLoadTriggerElement());
|
|
});
|
|
|
|
const [scrollTop, setScrollTop] = createSignal(0);
|
|
const itemsMutationObserver = new MutationObserver(() => {
|
|
itemsContainerElement().scrollTop = scrollTop();
|
|
});
|
|
const [itemsContainerElement, setItemsContainerElement] = createSignal();
|
|
createEffect(() => {
|
|
if (!itemsContainerElement()) return;
|
|
itemsMutationObserver.observe(itemsContainerElement(), { childList: true });
|
|
});
|
|
|
|
function handleListItemClick(item) {
|
|
if (!props.onListItemClick) return;
|
|
props.onListItemClick(item);
|
|
}
|
|
|
|
function handleToggleAllItems() {
|
|
if (allItemsSelected()) {
|
|
listItems().forEach((item) => item.setSelected(false));
|
|
return;
|
|
}
|
|
listItems().forEach((item) => item.setSelected(true));
|
|
}
|
|
|
|
function renderHeaderRow(columns) {
|
|
columns = columns.filter((column) => !column.hidden);
|
|
if (!props.showHeader) return null;
|
|
return (
|
|
<div class="list-group-item d-flex p-0">
|
|
<div class="d-flex flex-fill border-bottom border-2 fw-bold overflow-y-scroll">
|
|
<Show when={props.selectable}>
|
|
<div
|
|
class="d-flex align-items-center justify-content-center border-end py-2 px-3"
|
|
onClick={() => handleToggleAllItems()}
|
|
>
|
|
<input
|
|
class="form-check-input m-0"
|
|
type="checkbox"
|
|
checked={allItemsSelected()}
|
|
id={""}
|
|
/>
|
|
</div>
|
|
</Show>
|
|
<For each={columns}>
|
|
{(column) => (
|
|
<div
|
|
class={"d-flex p-2" + (column.width ? "" : " flex-fill")}
|
|
style={column.width ? `width:${column.width}em` : ""}
|
|
>
|
|
<Show when={column.withIcons}>
|
|
<div class="pe-2" style="color: transparent">
|
|
<i class="bi-folder2" />
|
|
</div>
|
|
</Show>
|
|
<div class="text-truncate">
|
|
<span>{column.name}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderListItem(listItem, columns) {
|
|
columns = columns.filter((column) => !column.hidden);
|
|
let item = listItem.item;
|
|
return (
|
|
<a
|
|
role="button"
|
|
class="list-group-item list-group-item-action d-flex p-0 overflow-x-hidden border-0 border-bottom border-1"
|
|
style="max-width: 100%"
|
|
>
|
|
<Show when={props.selectable}>
|
|
<div
|
|
class="d-flex align-items-center justify-content-center border-end py-2 px-3"
|
|
onClick={() => listItem.setSelected(!listItem.selected())}
|
|
>
|
|
<input
|
|
class="form-check-input m-0"
|
|
type="checkbox"
|
|
checked={listItem.selected()}
|
|
id={""}
|
|
/>
|
|
</div>
|
|
</Show>
|
|
<div
|
|
class="d-flex flex-fill overflow-hidden"
|
|
onClick={() => handleListItemClick(item)}
|
|
>
|
|
<For each={columns}>
|
|
{(column) => (
|
|
<div
|
|
class={
|
|
"d-flex p-2 align-items-center" +
|
|
(column.width ? " flex-shrink-0" : " flex-fill")
|
|
}
|
|
style={
|
|
"overflow: hidden; " +
|
|
(column.width ? `width:${column.width}em` : "")
|
|
}
|
|
>
|
|
<Show when={column.withIcons}>
|
|
<Show when={!item[column.id].icon}>
|
|
<div class="pe-2" style="color: transparent">
|
|
<i class="bi-folder2" />
|
|
</div>
|
|
</Show>
|
|
<Show when={item[column.id].icon}>
|
|
<div class="pe-2">
|
|
<i class={item[column.id].icon} />
|
|
</div>
|
|
</Show>
|
|
</Show>
|
|
<Switch>
|
|
<Match when={item[column.id].html}>
|
|
{item[column.id].html}
|
|
</Match>
|
|
<Match when={item[column.id].text}>
|
|
<div class="text-truncate">
|
|
<span>{item[column.id]?.text}</span>
|
|
</div>
|
|
</Match>
|
|
</Switch>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={props.style}
|
|
class={
|
|
"list-group d-flex flex-column overflow-hidden " + (props.class || "")
|
|
}
|
|
>
|
|
{renderHeaderRow(props.columns)}
|
|
<div
|
|
class="list-group-item p-0 flex-fill overflow-y-scroll"
|
|
style="min-width: 0"
|
|
onScroll={(event) => setScrollTop(event.target.scrollTop)}
|
|
ref={setItemsContainerElement}
|
|
>
|
|
<For each={listItems()}>
|
|
{(listItem) => renderListItem(listItem, props.columns)}
|
|
</For>
|
|
<div
|
|
style="width: 100%; height: 3em;"
|
|
ref={setLazyLoadTriggerElement}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default List;
|