Added Roles, User Management, fixed bugs, added FlexTable component (should be renamed to GridTable), other table components and test code should be removed down the line, added admin function to fix broken data structures.
This commit is contained in:
335
imports/ui/Table2.svelte
Normal file
335
imports/ui/Table2.svelte
Normal file
@@ -0,0 +1,335 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
/** @type {Array<Object>} */
|
||||
export let columns;
|
||||
|
||||
/** @type {Array<Object>} */
|
||||
export let rows;
|
||||
|
||||
/** @type {Array<Object>} */
|
||||
export let c_rows;
|
||||
|
||||
/** @type {Array<number>} */
|
||||
export let sortOrders = [1, -1];
|
||||
|
||||
// READ AND WRITE
|
||||
|
||||
/** @type {string} */
|
||||
export let sortBy = "";
|
||||
|
||||
/** @type {number} */
|
||||
export let sortOrder = sortOrders?.[0] || 1;
|
||||
|
||||
/** @type {Object} */
|
||||
export let filterSelections = {};
|
||||
|
||||
// expand
|
||||
/** @type {Array.<string|number>} */
|
||||
export let expanded = [];
|
||||
|
||||
// READ ONLY
|
||||
|
||||
/** @type {string} */
|
||||
export let expandRowKey = null;
|
||||
|
||||
/** @type {string} */
|
||||
export let expandSingle = false;
|
||||
|
||||
/** @type {string} */
|
||||
export let iconAsc = "▲";
|
||||
|
||||
/** @type {string} */
|
||||
export let iconDesc = "▼";
|
||||
|
||||
/** @type {string} */
|
||||
export let iconSortable = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let iconExpand = "▼";
|
||||
|
||||
/** @type {string} */
|
||||
export let iconExpanded = "▲";
|
||||
|
||||
/** @type {boolean} */
|
||||
export let showExpandIcon = false;
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameTable = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameThead = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameTbody = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameSelect = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameInput = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameRow = "";
|
||||
|
||||
/** @type {string} */
|
||||
export let classNameCell = "";
|
||||
|
||||
/** @type {string} class added to the expanded row*/
|
||||
export let classNameRowExpanded = "";
|
||||
|
||||
/** @type {string} class added to the expanded row*/
|
||||
export let classNameExpandedContent = "";
|
||||
|
||||
/** @type {string} class added to the cell that allows expanding/closing */
|
||||
export let classNameCellExpand = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let sortFunction = () => "";
|
||||
|
||||
// Validation
|
||||
if (!Array.isArray(expanded)) throw "'expanded' needs to be an array";
|
||||
|
||||
let showFilterHeader = columns.some(c => {
|
||||
// check if there are any filter or search headers
|
||||
return c.filterOptions !== undefined || c.searchValue !== undefined;
|
||||
});
|
||||
let filterValues = {};
|
||||
let columnByKey;
|
||||
$: {
|
||||
columnByKey = {};
|
||||
columns.forEach(col => {
|
||||
columnByKey[col.key] = col;
|
||||
});
|
||||
}
|
||||
|
||||
$: colspan = (showExpandIcon ? 1 : 0) + columns.length;
|
||||
|
||||
console.log(rows);
|
||||
|
||||
$: c_rows = rows
|
||||
.filter(r => {
|
||||
// get search and filter results/matches
|
||||
return Object.keys(filterSelections).every(f => {
|
||||
// check search (text input) matches
|
||||
let resSearch =
|
||||
filterSelections[f] === "" ||
|
||||
(columnByKey[f].searchValue &&
|
||||
(columnByKey[f].searchValue(r) + "")
|
||||
.toLocaleLowerCase()
|
||||
.indexOf((filterSelections[f] + "").toLocaleLowerCase()) >= 0);
|
||||
|
||||
// check filter (dropdown) matches
|
||||
let resFilter =
|
||||
resSearch ||
|
||||
filterSelections[f] === undefined ||
|
||||
// default to value() if filterValue() not provided in col
|
||||
filterSelections[f] ===
|
||||
(typeof columnByKey[f].filterValue === "function"
|
||||
? columnByKey[f].filterValue(r)
|
||||
: columnByKey[f].value(r));
|
||||
return resFilter;
|
||||
});
|
||||
})
|
||||
.map(r =>
|
||||
Object.assign({}, r, {
|
||||
// internal row property for sort order
|
||||
$sortOn: sortFunction(r),
|
||||
// internal row property for expanded rows
|
||||
$expanded:
|
||||
expandRowKey !== null && expanded.indexOf(r[expandRowKey]) >= 0,
|
||||
})
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!sortBy) return 0;
|
||||
else if (a.$sortOn > b.$sortOn) return sortOrder;
|
||||
else if (a.$sortOn < b.$sortOn) return -sortOrder;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const asStringArray = v =>
|
||||
[]
|
||||
.concat(v)
|
||||
.filter(v => typeof v === "string" && v !== "")
|
||||
.join(" ");
|
||||
|
||||
const calculateFilterValues = () => {
|
||||
filterValues = {};
|
||||
columns.forEach(c => {
|
||||
if (typeof c.filterOptions === "function") {
|
||||
filterValues[c.key] = c.filterOptions(rows);
|
||||
} else if (Array.isArray(c.filterOptions)) {
|
||||
// if array of strings is provided, use it for name and value
|
||||
filterValues[c.key] = c.filterOptions.map(val => ({
|
||||
name: val,
|
||||
value: val,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$: {
|
||||
let col = columnByKey[sortBy];
|
||||
if (
|
||||
col !== undefined &&
|
||||
col.sortable === true &&
|
||||
typeof col.value === "function"
|
||||
) {
|
||||
sortFunction = r => col.value(r);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
// if filters are enabled, watch rows and columns
|
||||
if (showFilterHeader && columns && rows) {
|
||||
calculateFilterValues();
|
||||
}
|
||||
}
|
||||
|
||||
const updateSortOrder = colKey => {
|
||||
return colKey === sortBy
|
||||
? sortOrders[
|
||||
(sortOrders.findIndex(o => o === sortOrder) + 1) % sortOrders.length
|
||||
]
|
||||
: sortOrders[0];
|
||||
};
|
||||
|
||||
const handleClickCol = (event, col) => {
|
||||
if (col.sortable) {
|
||||
sortOrder = updateSortOrder(col.key);
|
||||
sortBy = sortOrder ? col.key : undefined;
|
||||
}
|
||||
dispatch("clickCol", { event, col, key: col.key });
|
||||
};
|
||||
|
||||
const handleClickRow = (event, row) => {
|
||||
dispatch("clickRow", { event, row });
|
||||
};
|
||||
|
||||
const handleClickExpand = (event, row) => {
|
||||
row.$expanded = !row.$expanded;
|
||||
const keyVal = row[expandRowKey];
|
||||
if (expandSingle && row.$expanded) {
|
||||
expanded = [keyVal];
|
||||
} else if (expandSingle) {
|
||||
expanded = [];
|
||||
} else if (!row.$expanded) {
|
||||
expanded = expanded.filter(r => r != keyVal);
|
||||
} else {
|
||||
expanded = [...expanded, keyVal];
|
||||
}
|
||||
dispatch("clickExpand", { event, row });
|
||||
};
|
||||
|
||||
const handleClickCell = (event, row, key) => {
|
||||
dispatch("clickCell", { event, row, key });
|
||||
};
|
||||
</script>
|
||||
|
||||
<table class={asStringArray(classNameTable)}>
|
||||
<thead class={asStringArray(classNameThead)}>
|
||||
{#if showFilterHeader}
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
<th class={asStringArray([col.headerFilterClass])}>
|
||||
{#if col.searchValue !== undefined}
|
||||
<input
|
||||
bind:value={filterSelections[col.key]}
|
||||
class={asStringArray(classNameInput)}
|
||||
/>
|
||||
{:else if filterValues[col.key] !== undefined}
|
||||
<select
|
||||
bind:value={filterSelections[col.key]}
|
||||
class={asStringArray(classNameSelect)}
|
||||
>
|
||||
<option value={undefined} />
|
||||
{#each filterValues[col.key] as option}
|
||||
<option value={option.value}>{option.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
{#if showExpandIcon}
|
||||
<th />
|
||||
{/if}
|
||||
</tr>
|
||||
{/if}
|
||||
<slot name="header" {sortOrder} {sortBy}>
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
<th
|
||||
on:click={e => handleClickCol(e, col)}
|
||||
class={asStringArray([
|
||||
col.sortable ? "isSortable" : "",
|
||||
col.headerClass,
|
||||
])}
|
||||
>
|
||||
{col.title}
|
||||
{#if sortBy === col.key}
|
||||
{@html sortOrder === 1 ? iconAsc : iconDesc}
|
||||
{:else if col.sortable}
|
||||
{@html iconSortable}
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
{#if showExpandIcon}
|
||||
<th />
|
||||
{/if}
|
||||
</tr>
|
||||
</slot>
|
||||
</thead>
|
||||
|
||||
<tbody class={asStringArray(classNameTbody)}>
|
||||
{#each c_rows as row, n}
|
||||
<slot name="row" {row} {n}>
|
||||
<tr on:click={e => { handleClickRow(e, row); }} class={asStringArray([classNameRow, row.$expanded && classNameRowExpanded])}>
|
||||
{#each columns as col}
|
||||
<td on:click={e => {handleClickCell(e, row, col.key);}} class={asStringArray([col.class, classNameCell])}>
|
||||
{#if col.renderComponent}
|
||||
<svelte:component
|
||||
this={col.renderComponent.component || col.renderComponent}
|
||||
{...col.renderComponent.props || {}}
|
||||
{row}
|
||||
{col}
|
||||
/>
|
||||
{:else}
|
||||
{@html col.renderValue ? col.renderValue(row) : col.value(row)}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{#if showExpandIcon}
|
||||
<td on:click={e => handleClickExpand(e, row)} class={asStringArray(["isClickable", classNameCellExpand])}>
|
||||
{@html row.$expanded ? iconExpand : iconExpanded}
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{#if row.$expanded}
|
||||
<tr class={asStringArray(classNameExpandedContent)}>
|
||||
<td {colspan}>
|
||||
<slot name="expanded" {row} {n} />
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</slot>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
.isSortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.isClickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr th select {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user