feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
This commit is contained in:
2026-08-15 22:27:52 +02:00
parent e074f04bdf
commit d4d99a7473
18854 changed files with 2615729 additions and 173735 deletions

21
web/node_modules/@vincjo/datatables/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 vincjo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
web/node_modules/@vincjo/datatables/README.md generated vendored Normal file
View File

@@ -0,0 +1,70 @@
<div align="center">
<img align="center" src="./static/logo.svg" alt="logo" width="172"/>
<p align="center">
<h1 align="center" style="font-size:32px;margin:0;border:none;">svelte simple datatables</h1>
<p style="color:#eee">A powerful toolkit for building datatable components.</p>
<img src="https://img.shields.io/badge/svelte-runes-v2?color=%23c2185b" alt="any text"/>
<img src="https://img.shields.io/github/license/vincjo/datatables?color=c2185b" alt="last commit"/>
</p>
</div>
# Docs
Streamline your data workflow with a robust API providing advanced features while reducing code complexity.
:globe_with_meridians: **[vincjo.fr/datatables](https://vincjo.fr/datatables)**
# Install
```apache
npm i -D @vincjo/datatables
```
# Smooth transition from v1 to v2
In order to make the migration process a little easier, v1 is embed in “legacy” namespace so you will have the opportunity to upgrade your components progressively by simply modifying imports.
```diff
- @vincjo/datatables
+ @vincjo/datatables/legacy
- @vincjo/datatables/remote
+ @vincjo/datatables/legacy/remote
```
<br>
# Sample code
```svelte
<script lang="ts">
import { TableHandler } from '@vincjo/datatables'
import { someData } from './data'
const table = new TableHandler(someData, { rowsPerPage: 50 })
</script>
<table>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
{#each table.rows as row}
<tr>
<td>{row.first_name}</td>
<td>{row.last_name}</td>
</tr>
{/each}
</tbody>
</table>
```

1
web/node_modules/@vincjo/datatables/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export { TableHandler, RecordFilter, Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, check, type Field, type Check, type Internationalization, type Row, type TableParams, type SortParams, type TableHandlerInterface, type AdvancedFilterBuilder, type CalculationBuilder, type CSVBuilder, type FilterBuilder, type RecordFilterBuilder, type SearchBuilder, type SortBuilder, } from './src/client/index.js';

7
web/node_modules/@vincjo/datatables/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
export {
// class:
TableHandler, RecordFilter,
// components:
Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount,
// utils:
check, } from './src/client/index.js';

View File

@@ -0,0 +1,12 @@
import DataHandler from './local/DataHandler';
import Datatable from './local/Datatable.svelte';
import Th from './local/Th.svelte';
import ThFilter from './local/ThFilter.svelte';
import Pagination from './local/Pagination.svelte';
import RowCount from './local/RowCount.svelte';
import RowsPerPage from './local/RowsPerPage.svelte';
import Search from './local/Search.svelte';
import { check } from './local/Comparator';
import type { Internationalization, Row, Field, Comparator, Filter, Sort, Selectable, Order, FilterBy, OrderBy } from './local';
export { DataHandler, check, Datatable, Th, ThFilter, Pagination, RowCount, RowsPerPage, Search };
export type { Internationalization, Row, Field, Comparator, Filter, Sort, Selectable, Order, OrderBy, FilterBy, };

View File

@@ -0,0 +1,11 @@
// Reexport your entry components here
import DataHandler from './local/DataHandler';
import Datatable from './local/Datatable.svelte';
import Th from './local/Th.svelte';
import ThFilter from './local/ThFilter.svelte';
import Pagination from './local/Pagination.svelte';
import RowCount from './local/RowCount.svelte';
import RowsPerPage from './local/RowsPerPage.svelte';
import Search from './local/Search.svelte';
import { check } from './local/Comparator';
export { DataHandler, check, Datatable, Th, ThFilter, Pagination, RowCount, RowsPerPage, Search };

View File

@@ -0,0 +1,25 @@
import type { Criterion } from './';
export declare const check: {
isLike: (entry: any, value: any) => boolean;
isNotLike: (entry: any, value: any) => boolean;
startsWith: (entry: any, value: any) => boolean;
endsWith: (entry: any, value: any) => boolean;
isEqualTo: (entry: any, value: any) => boolean;
isNotEqualTo: (entry: any, value: any) => boolean;
isGreaterThan: (entry: number, value: number) => boolean;
isGreaterThanOrEqualTo: (entry: number, value: number) => boolean;
isLessThan: (entry: number, value: number) => boolean;
isLessThanOrEqualTo: (entry: number, value: number) => boolean;
isBetween: (entry: number, value: [min: number, max: number]) => boolean;
isStrictlyBetween: (entry: number, value: [min: number, max: number]) => boolean;
isTrue: (entry: boolean, _: any) => boolean;
isFalse: (entry: boolean, _: any) => boolean;
isNull: (entry: null, _: any) => boolean;
isNotNull: (entry: any, _: any) => boolean;
whereIn: (entry: any, values?: Criterion[]) => boolean;
/**
* @deprecated use "isLike" instead
* @since 1.12.7 2023-09-27
*/
contains: (entry: any, value: any) => boolean;
};

View File

@@ -0,0 +1,94 @@
export const check = {
isLike: (entry, value) => {
return stringify(entry).indexOf(stringify(value)) > -1;
},
isNotLike: (entry, value) => {
return stringify(entry).indexOf(stringify(value)) === -1;
},
startsWith: (entry, value) => {
return stringify(entry).startsWith(stringify(value));
},
endsWith: (entry, value) => {
return stringify(entry).endsWith(stringify(value));
},
isEqualTo: (entry, value) => {
return stringify(entry) === stringify(value);
},
isNotEqualTo: (entry, value) => {
return stringify(entry) !== stringify(value);
},
isGreaterThan: (entry, value) => {
if (isNull(entry))
return false;
return entry > value;
},
isGreaterThanOrEqualTo: (entry, value) => {
if (isNull(entry))
return false;
return entry >= value;
},
isLessThan: (entry, value) => {
if (isNull(entry))
return false;
return entry < value;
},
isLessThanOrEqualTo: (entry, value) => {
if (isNull(entry))
return false;
return entry <= value;
},
isBetween: (entry, value) => {
if (isNull(entry))
return false;
const [min, max] = value;
return entry >= min && entry <= max;
},
isStrictlyBetween: (entry, value) => {
if (isNull(entry))
return false;
const [min, max] = value;
return entry > min && entry < max;
},
isTrue: (entry, _) => {
return entry === true;
},
isFalse: (entry, _) => {
return entry === false;
},
isNull: (entry, _) => {
return entry === null || entry === undefined;
},
isNotNull: (entry, _) => {
return entry === null || entry === undefined ? false : true;
},
whereIn: (entry, values = []) => {
if (isNull(entry))
return false;
if (values.length === 0)
return false;
for (const { value, comparator } of values) {
if (comparator(entry, value)) {
return true;
}
}
return false;
},
/**
* @deprecated use "isLike" instead
* @since 1.12.7 2023-09-27
*/
contains: (entry, value) => {
return check.isLike(entry, value);
},
};
/* utils */
function stringify(value = null) {
return String(value)
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
}
function isNull(entry) {
if (entry === null || entry === undefined)
return true;
}

View File

@@ -0,0 +1,40 @@
import { type Writable, type Readable } from 'svelte/store';
import type { Filter, Sort } from './';
import type { Params } from './DataHandler';
import EventHandler from './handlers/EventHandler';
export default class Context<Row> {
event: EventHandler;
rowsPerPage: Writable<number | null>;
pageNumber: Writable<number>;
search: Writable<{
value?: string;
scope?: (keyof Row)[];
}>;
filters: Writable<Filter<Row>[]>;
filterCount: Readable<number>;
rawRows: Writable<Row[]>;
filteredRows: Readable<Row[]>;
pagedRows: Readable<Row[]>;
rowCount: Readable<{
total: number;
start: number;
end: number;
}>;
pages: Readable<number[]>;
pagesWithEllipsis: Readable<number[]>;
pageCount: Readable<number>;
sort: Writable<(Sort<Row>)>;
selected: Writable<(Row | Row[keyof Row])[]>;
selectScope: Writable<'all' | 'currentPage'>;
isAllSelected: Readable<boolean>;
constructor(data: Row[], params: Params);
private createFilterCount;
private createFilteredRows;
private match;
private createPagedRows;
private createRowCount;
private createPages;
private createPagesWithEllipsis;
private createPageCount;
private createIsAllSelected;
}

View File

@@ -0,0 +1,171 @@
import { writable, derived } from 'svelte/store';
import { isNull, parseField } from './utils';
import { check } from './Comparator';
import EventHandler from './handlers/EventHandler';
export default class Context {
event;
rowsPerPage;
pageNumber;
search;
filters;
filterCount;
rawRows;
filteredRows;
pagedRows;
rowCount;
pages;
pagesWithEllipsis;
pageCount;
sort;
selected;
selectScope;
isAllSelected;
constructor(data, params) {
this.event = new EventHandler();
this.rowsPerPage = writable(params.rowsPerPage);
this.pageNumber = writable(1);
this.search = writable({});
this.filters = writable([]);
this.filterCount = this.createFilterCount();
this.rawRows = writable(data);
this.filteredRows = this.createFilteredRows();
this.pagedRows = this.createPagedRows();
this.rowCount = this.createRowCount();
this.pages = this.createPages();
this.pagesWithEllipsis = this.createPagesWithEllipsis();
this.pageCount = this.createPageCount();
this.sort = writable({});
this.selected = writable([]);
this.selectScope = writable('all');
this.isAllSelected = this.createIsAllSelected();
}
createFilterCount() {
return derived(this.filters, ($filters) => $filters.length);
}
createFilteredRows() {
return derived([this.rawRows, this.search, this.filters], ([$rawRows, $search, $filters]) => {
if ($search.value) {
$rawRows = $rawRows.filter((row) => {
const fields = $search.scope ?? Object.keys(row);
const scope = fields.map((field) => {
const { callback } = parseField(field);
return callback;
});
return scope.some((callback) => {
return this.match(callback(row), $search.value);
});
});
this.pageNumber.set(1);
this.selected.set([]);
this.event.trigger('change');
}
if ($filters.length > 0) {
$filters.forEach((filter) => {
return ($rawRows = $rawRows.filter((row) => {
const entry = filter.callback(row);
return this.match(entry, filter.value, filter.comparator);
}));
});
this.pageNumber.set(1);
this.selected.set([]);
this.event.trigger('change');
}
return $rawRows;
});
}
match(entry, value, compare = null) {
if (isNull(value)) {
return true;
}
if (!entry && compare) {
return compare(entry, value);
}
if (!entry)
return check.isLike(entry, value);
else if (typeof entry === 'object') {
return Object.keys(entry).some((k) => {
return this.match(entry[k], value, compare);
});
}
if (!compare)
return check.isLike(entry, value);
return compare(entry, value);
}
createPagedRows() {
return derived([this.filteredRows, this.rowsPerPage, this.pageNumber], ([$filteredRows, $rowsPerPage, $pageNumber]) => {
if (!$rowsPerPage) {
return $filteredRows;
}
return $filteredRows.slice(($pageNumber - 1) * $rowsPerPage, $pageNumber * $rowsPerPage);
});
}
createRowCount() {
return derived([this.filteredRows, this.pageNumber, this.rowsPerPage], ([$filteredRows, $pageNumber, $rowsPerPage]) => {
const total = $filteredRows.length;
if (!$rowsPerPage) {
return { total: total, start: 1, end: total };
}
return {
total: total,
start: $pageNumber * $rowsPerPage - $rowsPerPage + 1,
end: Math.min($pageNumber * $rowsPerPage, $filteredRows.length)
};
});
}
createPages() {
return derived([this.rowsPerPage, this.filteredRows], ([$rowsPerPage, $filteredRows]) => {
if (!$rowsPerPage) {
return [1];
}
const pages = Array.from(Array(Math.ceil($filteredRows.length / $rowsPerPage)));
return pages.map((_, i) => i + 1);
});
}
createPagesWithEllipsis() {
return derived([this.pages, this.pageNumber], ([$pages, $pageNumber]) => {
if ($pages.length <= 7) {
return $pages;
}
const ellipse = null;
const firstPage = 1;
const lastPage = $pages.length;
if ($pageNumber <= 4) {
return [
...$pages.slice(0, 5),
ellipse,
lastPage
];
}
else if ($pageNumber < $pages.length - 3) {
return [
firstPage,
ellipse,
...$pages.slice($pageNumber - 2, $pageNumber + 1),
ellipse,
lastPage
];
}
else {
return [
firstPage,
ellipse,
...$pages.slice($pages.length - 5, $pages.length)
];
}
});
}
createPageCount() {
return derived(this.pages, ($pages) => {
return $pages.length;
});
}
createIsAllSelected() {
return derived([this.selected, this.pagedRows, this.filteredRows, this.selectScope], ([$selected, $pagedRows, $filteredRows, $selectScope]) => {
const rowCount = $selectScope === 'currentPage' ? $pagedRows.length : $filteredRows.length;
if (rowCount === $selected.length && rowCount !== 0) {
return true;
}
return false;
});
}
}

View File

@@ -0,0 +1,93 @@
import FilterHelper from './helpers/FilterHelper';
import AdvancedFilterHelper from './helpers/AdvancedFilterHelper';
import CalculationHelper from './helpers/CalculationHelper';
import type { Readable, Writable } from 'svelte/store';
import type { Internationalization, Row, Field, Comparator } from './';
export type Params = {
rowsPerPage?: number;
i18n?: Internationalization;
};
export default class DataHandler<T extends Row = any> {
private context;
private sortHandler;
private selectHandler;
private pageHandler;
private searchHandler;
private filterHandler;
i18n: Internationalization;
constructor(data?: T[], params?: Params);
setRows(data: T[]): void;
getRows(): Readable<T[]>;
getAllRows(): Readable<T[]>;
getRowCount(): Readable<{
total: number;
start: number;
end: number;
}>;
getRowsPerPage(): Writable<number | null>;
getPages(param?: {
ellipsis: boolean;
}): Readable<number[]>;
getPageCount(): Readable<number>;
getPageNumber(): Readable<number>;
setPage(value: number | 'previous' | 'next'): void;
search(value: string, scope?: Field<T>[]): void;
clearSearch(): void;
sort(orderBy: Field<T>, identifier?: string): void;
sortAsc(orderBy: Field<T>, identifier?: string): void;
sortDesc(orderBy: Field<T>, identifier?: string): void;
getSort(): Writable<{
identifier?: string;
direction?: 'asc' | 'desc';
}>;
applySort(params?: {
orderBy: Field<T>;
direction?: 'asc' | 'desc';
}): void;
defineSort(orderBy: Field<T>, direction?: 'asc' | 'desc'): void;
clearSort(): void;
filter(value: string | number | null | undefined | boolean | number[], filterBy: Field<T>, comparator?: Comparator<T>): void;
getFilters(): Readable<{
value: unknown;
filterBy: Field<T>;
check: string;
}[]>;
createFilter(filterBy: Field<T>, comparator?: Comparator<T>): FilterHelper<T>;
createAdvancedFilter(filterBy: Field<T>): AdvancedFilterHelper<T>;
getFilterCount(): Readable<number>;
clearFilters(): void;
select(value: T | T[keyof T]): void;
getSelected(): Writable<(T | T[keyof T])[]>;
selectAll(params?: {
selectBy?: keyof T;
scope?: 'all' | 'currentPage';
}): void;
isAllSelected(): Readable<boolean>;
on(event: 'change' | 'clearFilters' | 'clearSearch', callback: () => void): void;
createCalculation(field: Field<T>, param?: {
precision: number;
}): CalculationHelper<T>;
translate(i18n: Internationalization): Internationalization;
/**
* @deprecated use setRows() instead
* @since v0.9.99 2023-01-16
*/
update(data: any[]): void;
/**
* @deprecated use applySort() instead
* @since v1.11.0 2023-07-11
*/
applySorting(params?: {
orderBy: Field<T>;
direction?: 'asc' | 'desc';
}): void;
/**
* @deprecated use getSort() instead
* @since v1.11.0 2023-07-11
*/
getSorted(): Writable<{
identifier?: string;
direction?: "asc" | "desc";
}>;
getTriggerChange(): Writable<number>;
}

View File

@@ -0,0 +1,170 @@
import Context from './Context';
import SortHandler from './handlers/SortHandler';
import SelectHandler from './handlers/SelectHandler';
import PageHandler from './handlers/PageHandler';
import SearchHandler from './handlers/SearchHandler';
import FilterHandler from './handlers/FilterHandler';
import FilterHelper from './helpers/FilterHelper';
import AdvancedFilterHelper from './helpers/AdvancedFilterHelper';
import CalculationHelper from './helpers/CalculationHelper';
export default class DataHandler {
context;
sortHandler;
selectHandler;
pageHandler;
searchHandler;
filterHandler;
i18n;
constructor(data = [], params = { rowsPerPage: null }) {
this.i18n = this.translate(params.i18n);
this.context = new Context(data, params);
this.sortHandler = new SortHandler(this.context);
this.selectHandler = new SelectHandler(this.context);
this.pageHandler = new PageHandler(this.context);
this.searchHandler = new SearchHandler(this.context);
this.filterHandler = new FilterHandler(this.context);
}
setRows(data) {
this.context.rawRows.set(data);
this.context.event.trigger('change');
this.applySort();
}
getRows() {
return this.context.pagedRows;
}
getAllRows() {
return this.context.filteredRows;
}
getRowCount() {
return this.context.rowCount;
}
getRowsPerPage() {
return this.context.rowsPerPage;
}
getPages(param = { ellipsis: false }) {
if (param.ellipsis) {
return this.context.pagesWithEllipsis;
}
return this.context.pages;
}
getPageCount() {
return this.context.pageCount;
}
getPageNumber() {
return this.context.pageNumber;
}
setPage(value) {
switch (value) {
case 'previous': return this.pageHandler.previous();
case 'next': return this.pageHandler.next();
default: return this.pageHandler.goto(value);
}
}
search(value, scope = null) {
this.searchHandler.set(value, scope);
}
clearSearch() {
this.searchHandler.clear();
}
sort(orderBy, identifier) {
this.setPage(1);
this.sortHandler.set(orderBy, identifier);
}
sortAsc(orderBy, identifier) {
this.setPage(1);
this.sortHandler.asc(orderBy, identifier);
}
sortDesc(orderBy, identifier) {
this.setPage(1);
this.sortHandler.desc(orderBy, identifier);
}
getSort() {
return this.context.sort;
}
applySort(params = null) {
this.sortHandler.apply(params);
}
defineSort(orderBy, direction) {
this.sortHandler.define(orderBy, direction);
}
clearSort() {
this.sortHandler.clear();
}
filter(value, filterBy, comparator = null) {
this.filterHandler.set(value, filterBy, comparator);
}
getFilters() {
return this.filterHandler.get();
}
createFilter(filterBy, comparator) {
return new FilterHelper(this.filterHandler, filterBy, comparator);
}
createAdvancedFilter(filterBy) {
return new AdvancedFilterHelper(this.filterHandler, filterBy);
}
getFilterCount() {
return this.context.filterCount;
}
clearFilters() {
this.filterHandler.clear();
}
select(value) {
this.selectHandler.set(value);
}
getSelected() {
return this.context.selected;
}
selectAll(params = {}) {
this.context.selectScope.set(params.scope === 'currentPage' ? 'currentPage' : 'all');
this.selectHandler.all(params.selectBy ?? null);
}
isAllSelected() {
return this.context.isAllSelected;
}
on(event, callback) {
this.context.event.add(event, callback);
}
createCalculation(field, param = null) {
return new CalculationHelper(this.context, field, { precision: param?.precision ?? 2 });
}
translate(i18n) {
return {
...{
search: 'Search...',
show: 'Show',
entries: 'entries',
filter: 'Filter',
rowCount: 'Showing {start} to {end} of {total} entries',
noRows: 'No entries found',
previous: 'Previous',
next: 'Next'
},
...i18n
};
}
/**
* @deprecated use setRows() instead
* @since v0.9.99 2023-01-16
*/
update(data) {
console.log('%c%s', 'color:#e65100;background:#fff3e0;font-size:12px;border-radius:4px;padding:4px;text-align:center;', `DataHandler.update(data) method is deprecated. Please use DataHandler.setRows(data) instead`);
this.context.rawRows.set(data);
}
/**
* @deprecated use applySort() instead
* @since v1.11.0 2023-07-11
*/
applySorting(params = null) {
this.applySort(params);
}
/**
* @deprecated use getSort() instead
* @since v1.11.0 2023-07-11
*/
getSorted() {
return this.getSort();
}
getTriggerChange() {
return this.context.event.triggerChange;
}
}

View File

@@ -0,0 +1,93 @@
<script lang="ts">import { Search, RowsPerPage, RowCount, Pagination } from './';
export let handler;
export let search = true;
export let rowsPerPage = true;
export let rowCount = true;
export let pagination = true;
let element;
let clientWidth = 1000;
const height = (search || rowsPerPage ? 48 : 8) + (rowCount || pagination ? 48 : 8);
handler.on('change', () => {
if (element)
element.scrollTop = 0;
});
</script>
<section bind:clientWidth class={$$props.class ?? ''}>
<header class:container={search || rowsPerPage}>
{#if search}
<Search {handler} />
{/if}
{#if rowsPerPage}
<RowsPerPage {handler} small={clientWidth < 600} />
{/if}
</header>
<article bind:this={element} style="height:calc(100% - {height}px)">
<slot />
</article>
<footer class:container={rowCount || pagination}>
{#if rowCount}
<RowCount {handler} small={clientWidth < 600} />
{/if}
{#if pagination}
<Pagination {handler} small={clientWidth < 600} />
{/if}
</footer>
</section>
<style>
section {
height: 100%;
}
section :global(table) {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
section :global(thead) {
position: sticky;
inset-block-start: 0;
z-index: 1;
}
header,
footer {
min-height: 8px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
header.container,
footer.container {
height: 48px;
}
footer {
border-top: 1px solid #e0e0e0;
}
article {
position: relative;
/* height:calc(100% - 96px); */
overflow: auto;
scrollbar-width: thin;
}
article::-webkit-scrollbar {
width: 6px;
height: 6px;
}
article::-webkit-scrollbar-track {
background: #f5f5f5;
}
article::-webkit-scrollbar-thumb {
background: #c2c2c2;
}
article::-webkit-scrollbar-thumb:hover {
background: #9e9e9e;
}
</style>

View File

@@ -0,0 +1,35 @@
import { type DataHandler, type Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
search?: boolean;
rowsPerPage?: boolean;
rowCount?: boolean;
pagination?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {
default: {};
};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']> & {
children?: any;
}>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
$$slots?: ReturnType<__sveltets_Render<T>['slots']>;
children?: any;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Datatable: $$IsomorphicComponent;
type Datatable<T extends Row> = InstanceType<typeof Datatable<T>>;
export default Datatable;

View File

@@ -0,0 +1,110 @@
<script lang="ts">export let handler;
export let small = false;
const pageNumber = handler.getPageNumber();
const pageCount = handler.getPageCount();
const pages = handler.getPages({ ellipsis: true });
</script>
<section class={$$props.class ?? ''}>
{#if small}
<button type="button"
class="small"
class:disabled={$pageNumber === 1}
on:click={() => handler.setPage(1)}
>
&#10092;&#10092;
</button>
<button type="button"
class:disabled={$pageNumber === 1}
on:click={() => handler.setPage('previous')}
>
&#10094;
</button>
<button type="button"
class:disabled={$pageNumber === $pageCount}
on:click={() => handler.setPage('next')}
>
&#10095;
</button>
<button type="button"
class="small"
class:disabled={$pageNumber === $pageCount}
on:click={() => handler.setPage($pageCount)}
>
&#10093;&#10093;
</button>
{:else}
<button type="button"
class:disabled={$pageNumber === 1}
on:click={() => handler.setPage('previous')}
>
{@html handler.i18n.previous}
</button>
{#each $pages as page}
<button type="button"
class:active={$pageNumber === page}
class:ellipse={page === null}
on:click={() => handler.setPage(page)}
>
{page ?? '...'}
</button>
{/each}
<button type="button"
class:disabled={$pageNumber === $pageCount}
on:click={() => handler.setPage('next')}
>
{@html handler.i18n.next}
</button>
{/if}
</section>
<style>
section {
display: flex;
}
button {
background: inherit;
height: 32px;
width: 32px;
color: #616161;
cursor: pointer;
font-size: 13px;
margin: 0;
padding: 0;
transition: all, 0.2s;
line-height: 32px;
border: 1px solid #e0e0e0;
border-right: none;
border-radius: 0;
outline: none;
}
button:first-child {
border-radius: 4px 0 0 4px;
}
button:last-child {
border-right: 1px solid #e0e0e0;
border-radius: 0 4px 4px 0;
}
button:first-child:not(.small),
button:last-child:not(.small) {
min-width: 72px;
}
button:not(.active):hover {
background: #eee;
}
button.ellipse:hover {
background: inherit;
cursor: default;
}
button.active {
background: #eee;
font-weight: bold;
cursor: default;
}
button.disabled:hover {
background: inherit;
cursor: default;
}
</style>

View File

@@ -0,0 +1,26 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Pagination: $$IsomorphicComponent;
type Pagination<T extends Row> = InstanceType<typeof Pagination<T>>;
export default Pagination;

View File

@@ -0,0 +1,31 @@
<script lang="ts">export let handler;
export let small = false;
const rowCount = handler.getRowCount();
</script>
<aside class={$$props.class ?? ''}>
{#if small}
{#if $rowCount.total > 0}
<b>{$rowCount.start}</b>-
<b>{$rowCount.end}</b>/
<b>{$rowCount.total}</b>
{:else}
{handler.i18n.noRows}
{/if}
{:else if $rowCount.total > 0}
{@html handler.i18n.rowCount
.replace('{start}', `<b>${$rowCount.start}</b>`)
.replace('{end}', `<b>${$rowCount.end}</b>`)
.replace('{total}', `<b>${$rowCount.total}</b>`)}
{:else}
{handler.i18n.noRows}
{/if}
</aside>
<style>
aside {
color: #616161;
line-height: 32px;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,26 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const RowCount: $$IsomorphicComponent;
type RowCount<T extends Row> = InstanceType<typeof RowCount<T>>;
export default RowCount;

View File

@@ -0,0 +1,34 @@
<script lang="ts">export let handler;
export let small = false;
const rowsPerPage = handler.getRowsPerPage();
const options = [5, 10, 20, 50, 100];
</script>
<aside class={$$props.class ?? ''}>
{#if !small}
<span>{handler.i18n.show}</span>
{/if}
<select bind:value={$rowsPerPage} on:change={() => handler.setPage(1)}>
{#each options as option}
<option value={option}>
{option}
</option>
{/each}
</select>
{#if !small}
<span>{handler.i18n.entries}</span>
{/if}
</aside>
<style>
aside {
display: flex;
justify-content: flex-start;
align-items: center;
height: 32px;
color: #757575;
}
select {
margin: 0 4px;
}
</style>

View File

@@ -0,0 +1,26 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const RowsPerPage: $$IsomorphicComponent;
type RowsPerPage<T extends Row> = InstanceType<typeof RowsPerPage<T>>;
export default RowsPerPage;

View File

@@ -0,0 +1,36 @@
<script lang="ts">export let handler;
let value = '';
handler.on('clearSearch', () => value = '');
</script>
<input
class={$$props.class ?? ''}
bind:value
placeholder={handler.i18n.search}
spellcheck="false"
on:input={() => handler.search(value)}
/>
<style>
input {
border: 1px solid #e0e0e0;
border-radius: 4px;
outline: none;
padding: 0 8px;
line-height: 24px;
margin: 0;
height: 24px;
background: transparent;
width: 40%;
max-width: 176px;
min-width: 96px;
transition: all, 0.1s;
}
input:focus {
border: 1px solid #bdbdbd;
}
input::placeholder {
color: #9e9e9e;
line-height: 24px;
}
</style>

View File

@@ -0,0 +1,25 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Search: $$IsomorphicComponent;
type Search<T extends Row> = InstanceType<typeof Search<T>>;
export default Search;

View File

@@ -0,0 +1,78 @@
<script lang="ts">export let handler;
export let orderBy;
export let identifier = orderBy?.toString();
export let align = 'left';
export let rowSpan = 1;
const sort = handler.getSort();
</script>
<th
on:click={() => handler.sort(orderBy, identifier)}
class:sortable={orderBy}
class:active={$sort.identifier === identifier}
class={$$props.class ?? ''}
rowspan={rowSpan}
>
<div
class="flex"
style:justify-content={align === 'left' ? 'flex-start' : align === 'right' ? 'flex-end' : 'center'}
>
<strong>
<slot />
</strong>
<span class:asc={$sort.direction === 'asc'} class:desc={$sort.direction === 'desc'}></span>
</div>
</th>
<style>
th {
background: inherit;
padding: 8px 20px;
white-space: nowrap;
font-size: 13px;
user-select: none;
border-bottom: 1px solid #e0e0e0;
}
th.sortable {
cursor: pointer;
}
th strong {
white-space: pre-wrap;
font-size: 13.5px;
line-height: 16px;
}
th.sortable div.flex {
padding: 0;
display: flex;
align-items: center;
height: 100%;
}
th.sortable span {
padding-left: 8px;
}
th.sortable span:before,
th.sortable span:after {
border: 4px solid transparent;
content: '';
display: block;
height: 0;
width: 0;
}
th.sortable span:before {
border-bottom-color: #e0e0e0;
margin-top: 2px;
}
th.sortable span:after {
border-top-color: #e0e0e0;
margin-top: 2px;
}
th.active.sortable span.asc:before {
border-bottom-color: #9e9e9e;
}
th.active.sortable span.desc:after {
border-top-color: #9e9e9e;
}
th:not(.sortable) span {
visibility: hidden;
}
</style>

View File

@@ -0,0 +1,35 @@
import type { DataHandler, Field, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
orderBy: Field<T>;
identifier?: string;
align?: "left" | "right" | "center";
rowSpan?: number;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {
default: {};
};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']> & {
children?: any;
}>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
$$slots?: ReturnType<__sveltets_Render<T>['slots']>;
children?: any;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Th: $$IsomorphicComponent;
type Th<T extends Row> = InstanceType<typeof Th<T>>;
export default Th;

View File

@@ -0,0 +1,45 @@
<script lang="ts">export let handler;
export let filterBy;
export let align = 'left';
export let comparator = null;
let value = '';
handler.on('clearFilters', () => value = '');
</script>
<th class={$$props.class ?? ''}>
<input
style:text-align={align}
type="text"
placeholder={handler.i18n.filter}
spellcheck="false"
bind:value
on:input={() => handler.filter(value, filterBy, comparator)}
/>
</th>
<style>
th {
border-bottom: 1px solid #e0e0e0;
}
input {
width: 100%;
height: 24px;
border: none;
text-align: left;
padding: 0 20px;
background: inherit;
outline: none;
border-radius: 0;
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
}
input::placeholder {
color: #bdbdbd;
font-style: italic;
font-size: 13px;
}
input:focus {
outline: none;
border: none;
}
</style>

View File

@@ -0,0 +1,28 @@
import type { DataHandler, Field, Comparator, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
filterBy: Field<T>;
align?: "left" | "right" | "center";
comparator?: Comparator<T>;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const ThFilter: $$IsomorphicComponent;
type ThFilter<T extends Row> = InstanceType<typeof ThFilter<T>>;
export default ThFilter;

View File

@@ -0,0 +1,6 @@
export default class EventHandler {
private events;
triggerChange: import("svelte/store").Writable<number>;
add(event: keyof EventHandler['events'], callback: () => void): void;
trigger(event: keyof EventHandler['events']): void;
}

View File

@@ -0,0 +1,21 @@
import { writable } from 'svelte/store';
export default class EventHandler {
events = {
change: [],
clearFilters: [],
clearSearch: []
};
triggerChange = writable(0); // legacy
add(event, callback) {
this.events[event].push(callback);
}
trigger(event) {
for (const callback of this.events[event]) {
callback();
}
/* legacy: support for triggerChange store */
if (event === 'change') {
this.triggerChange.update((store) => { return store + 1; });
}
}
}

View File

@@ -0,0 +1,19 @@
import type { Filter, Field, Comparator, EventHandler, Criterion } from '..';
import type Context from '../Context';
import { type Writable, type Readable } from 'svelte/store';
type Value = string | number | null | undefined | boolean | number[] | Criterion[];
export default class FilterHandler<Row> {
protected filters: Writable<Filter<Row>[]>;
protected event: EventHandler;
private collection;
constructor(context: Context<Row>);
set(value: Value, filterBy: Field<Row>, comparator?: Comparator<Row>, name?: string): void;
clear(): void;
get(): Readable<{
value: unknown;
filterBy: Field<Row>;
check: string;
}[]>;
private createCollection;
}
export {};

View File

@@ -0,0 +1,59 @@
import { isNotNull } from '../utils';
import { derived } from 'svelte/store';
import { parseField } from '../utils';
// type Collection<Row> = {
// value: unknown
// filterBy: Field<Row>
// set: (value: unknown, comparator: Comparator<Row>) => void,
// clear: () => void
// }
export default class FilterHandler {
filters;
event;
collection;
constructor(context) {
this.filters = context.filters;
this.event = context.event;
}
set(value, filterBy, comparator = null, name) {
const { callback, identifier, key } = parseField(filterBy, name);
const filter = { value, identifier, callback, comparator, key };
this.filters.update((store) => {
store = store.filter((item) => item.identifier !== identifier);
if (isNotNull(value)) {
store.push(filter);
}
return store;
});
}
clear() {
this.filters.set([]);
this.event.trigger('change');
this.event.trigger('clearFilters');
}
get() {
if (this.collection) {
return this.collection;
}
this.collection = this.createCollection();
return this.collection;
}
createCollection() {
return derived(this.filters, ($filters) => {
return $filters.map(({ value, callback, key, comparator }) => {
const filterBy = key ?? callback;
return {
value,
filterBy,
check: comparator ? comparator.name : 'isLike'
// set: (value: Value, comparator: Comparator<Row> = check.isLike) => {
// this.set(value, filterBy, comparator)
// },
// clear: () => {
// this.set(undefined, filterBy)
// }
};
});
});
}
}

View File

@@ -0,0 +1,11 @@
import type Context from '../Context';
export default class PageHandler<Row> {
private pageNumber;
private rowCount;
private rowsPerPage;
private event;
constructor(context: Context<Row>);
goto(number: number): void;
previous(): void;
next(): void;
}

View File

@@ -0,0 +1,34 @@
import { get } from 'svelte/store';
export default class PageHandler {
pageNumber;
rowCount;
rowsPerPage;
event;
constructor(context) {
this.pageNumber = context.pageNumber;
this.rowCount = context.rowCount;
this.rowsPerPage = context.rowsPerPage;
this.event = context.event;
}
goto(number) {
this.pageNumber.update((store) => {
const rowsPerPage = get(this.rowsPerPage);
if (rowsPerPage) {
const total = get(this.rowCount).total;
if (number >= 1 && number <= Math.ceil(total / rowsPerPage)) {
store = number;
this.event.trigger('change');
}
}
return store;
});
}
previous() {
const number = get(this.pageNumber) - 1;
this.goto(number);
}
next() {
const number = get(this.pageNumber) + 1;
this.goto(number);
}
}

View File

@@ -0,0 +1,9 @@
import type Context from '../Context';
import type { Field } from '..';
export default class SearchHandler<Row> {
private search;
private event;
constructor(context: Context<Row>);
set(value: string, scope?: Field<Row>[]): void;
clear(): void;
}

View File

@@ -0,0 +1,22 @@
export default class SearchHandler {
search;
event;
constructor(context) {
this.search = context.search;
this.event = context.event;
}
set(value, scope = null) {
this.search.update((store) => {
store = {
value: value ?? '',
scope: scope ?? null,
};
return store;
});
}
clear() {
this.search.set({ value: null, scope: null });
this.event.trigger('change');
this.event.trigger('clearSearch');
}
}

View File

@@ -0,0 +1,13 @@
import type Context from '../Context';
export default class SelectHandler<Row> {
private filteredRows;
private pagedRows;
private selected;
private scope;
private isAllSelected;
private event;
constructor(context: Context<Row>);
set(value: Row[keyof Row] | Row): void;
all(selectBy?: keyof Row): void;
clear(): void;
}

View File

@@ -0,0 +1,46 @@
import { get } from 'svelte/store';
export default class SelectHandler {
filteredRows;
pagedRows;
selected;
scope;
isAllSelected;
event;
constructor(context) {
this.filteredRows = context.filteredRows;
this.pagedRows = context.pagedRows;
this.selected = context.selected;
this.scope = context.selectScope;
this.isAllSelected = context.isAllSelected;
this.event = context.event;
}
set(value) {
const selected = get(this.selected);
if (selected.includes(value)) {
this.selected.set(selected.filter((item) => item !== value));
}
else {
this.selected.set([value, ...selected]);
}
}
all(selectBy = null) {
const isAllSelected = get(this.isAllSelected);
if (isAllSelected) {
return this.clear();
}
const scope = get(this.scope);
const rows = scope === 'currentPage' ? get(this.pagedRows) : get(this.filteredRows);
if (scope === 'currentPage') {
this.event.add('change', () => this.clear());
}
if (selectBy) {
this.selected.set(rows.map((row) => row[selectBy]));
}
else {
this.selected.set(rows);
}
}
clear() {
this.selected.set([]);
}
}

View File

@@ -0,0 +1,20 @@
import type Context from '../Context';
import type { Field } from '..';
export default class SortHandler<Row> {
private rawRows;
private event;
private sort;
private backup;
constructor(context: Context<Row>);
set(orderBy?: Field<Row>, uid?: string): void;
asc(orderBy: Field<Row>, uid?: string): void;
desc(orderBy: Field<Row>, uid?: string): void;
apply(params?: {
orderBy: Field<Row>;
direction?: 'asc' | 'desc';
}): void;
clear(): void;
define(orderBy: Field<Row>, direction?: 'asc' | 'desc'): void;
private restore;
private log;
}

View File

@@ -0,0 +1,128 @@
import { get } from 'svelte/store';
import { parseField } from '../utils';
export default class SortHandler {
rawRows;
event;
sort;
backup;
constructor(context) {
this.rawRows = context.rawRows;
this.event = context.event;
this.sort = context.sort;
this.backup = [];
}
set(orderBy = null, uid) {
if (!orderBy)
return;
const sort = get(this.sort);
const { identifier } = parseField(orderBy, uid);
if (sort.identifier !== identifier) {
this.sort.update((store) => (store.direction = null));
}
if (sort.direction === null || sort.direction === 'desc') {
this.asc(orderBy, uid);
}
else if (sort.direction === 'asc') {
this.desc(orderBy, uid);
}
}
asc(orderBy, uid) {
if (!orderBy)
return;
const { identifier, callback, key } = parseField(orderBy, uid);
this.sort.set({ identifier, callback, direction: 'asc', key });
this.rawRows.update((store) => {
store.sort((x, y) => {
const [a, b] = [callback(x), callback(y)];
if (a === b)
return 0;
if (a === null)
return 1;
if (b === null)
return -1;
if (typeof a === 'boolean')
return a === false ? 1 : -1;
if (typeof a === 'string')
return a.localeCompare(b);
if (typeof a === 'number')
return a - b;
if (typeof a === 'object')
return JSON.stringify(a).localeCompare(JSON.stringify(b));
else
return String(a).localeCompare(String(b));
});
return store;
});
this.log({ identifier, callback, direction: 'asc' });
this.event.trigger('change');
}
desc(orderBy, uid) {
if (!orderBy)
return;
const { identifier, callback, key } = parseField(orderBy, uid);
this.sort.set({ identifier, callback, direction: 'desc', key });
this.rawRows.update((store) => {
store.sort((x, y) => {
const [a, b] = [callback(x), callback(y)];
if (a === b)
return 0;
if (a === null)
return 1;
if (b === null)
return -1;
if (typeof b === 'boolean')
return b === false ? 1 : -1;
if (typeof b === 'string')
return b.localeCompare(a);
if (typeof b === 'number')
return b - a;
if (typeof b === 'object')
return JSON.stringify(b).localeCompare(JSON.stringify(a));
else
return String(b).localeCompare(String(a));
});
return store;
});
this.log({ identifier, callback, direction: 'desc' });
this.event.trigger('change');
}
apply(params = null) {
if (params) {
switch (params.direction) {
case 'asc': return this.asc(params.orderBy);
case 'desc': return this.desc(params.orderBy);
default: return this.set(params.orderBy);
}
}
else {
this.restore();
}
}
clear() {
this.backup = [];
this.sort.set({});
}
define(orderBy, direction = 'asc') {
if (!orderBy)
return;
const { identifier, callback, key } = parseField(orderBy);
this.sort.set({ identifier, callback, direction, key });
}
restore() {
for (const sort of this.backup) {
const { key, callback, direction } = sort;
const param = key ?? callback;
this[direction](param);
}
}
log(sort) {
this.backup = this.backup.filter(item => item.identifier !== sort.identifier);
if (this.backup.length >= 3) {
const [_, slot2, slot3] = this.backup;
this.backup = [slot2, slot3, sort];
}
else {
this.backup = [...this.backup, sort];
}
}
}

View File

@@ -0,0 +1,15 @@
import type { Field, Comparator } from '..';
import type FilterHandler from '../handlers/FilterHandler';
import { type Writable } from 'svelte/store';
type Value = string | number | [min: number, max: number];
export default class AdvancedFilterHandler<Row> {
private filterHandler;
private criteria;
private filterBy;
private selected;
constructor(filterHandler: FilterHandler<Row>, filterBy: Field<Row>);
set(value: Value, comparator?: Comparator<any>): void;
getSelected(): Writable<Value[]>;
clear(): void;
}
export {};

View File

@@ -0,0 +1,35 @@
import { writable } from 'svelte/store';
import { check } from '../Comparator';
export default class AdvancedFilterHandler {
filterHandler;
criteria;
filterBy;
selected;
constructor(filterHandler, filterBy) {
this.filterHandler = filterHandler;
this.filterBy = filterBy;
this.criteria = [];
this.selected = writable([]);
}
set(value, comparator = check.isLike) {
if (this.criteria.find(criterion => criterion.value === value)) {
this.criteria = this.criteria.filter(criterion => criterion.value !== value);
}
else {
this.criteria = [{ value, comparator }, ...this.criteria];
}
if (this.criteria.length === 0) {
return this.clear();
}
this.filterHandler.set(this.criteria, this.filterBy, check.whereIn);
this.selected.set(this.criteria.map(criterion => criterion.value));
}
getSelected() {
return this.selected;
}
clear() {
this.criteria = [];
this.selected.set([]);
this.filterHandler.set(undefined, this.filterBy, check.whereIn);
}
}

View File

@@ -0,0 +1,21 @@
import type { Field } from '..';
import type Context from '../Context';
import { type Readable } from 'svelte/store';
export default class CalcultationHandler<Row> {
private rawRows;
private filteredRows;
private callback;
private precision;
constructor(context: Context<Row>, field: Field<Row>, param: {
precision: number;
});
distinct(callback?: (values: any[]) => any[]): {
value: string;
count: unknown;
}[];
avg(callback?: (values: number[]) => any[]): Readable<number>;
sum(callback?: (values: number[]) => any[]): Readable<number>;
bounds(callback?: (values: number[]) => any[]): [min: number, max: number];
setPrecision(value: number): void;
private round;
}

View File

@@ -0,0 +1,59 @@
import { get, derived } from 'svelte/store';
import { parseField } from '../utils';
export default class CalcultationHandler {
rawRows;
filteredRows;
callback;
precision;
constructor(context, field, param) {
this.rawRows = context.rawRows;
this.filteredRows = context.filteredRows;
this.callback = parseField(field).callback;
this.precision = param.precision;
}
distinct(callback = null) {
const rawRows = get(this.rawRows);
const values = rawRows.map(row => this.callback(row));
const array = callback ? callback(values) : values;
const result = array.reduce((acc, curr) => {
acc[curr] = (acc[curr] ?? 0) + 1;
return acc;
}, {});
return Object.entries(result).map(([value, count]) => ({ value, count }));
}
avg(callback = null) {
return derived(this.filteredRows, $filteredRows => {
if ($filteredRows.length === 0)
return 0;
const values = $filteredRows.map(row => this.callback(row)).filter(Boolean);
const array = callback ? callback(values) : values;
return this.round(array.reduce((acc, curr) => acc + curr, 0) / array.length);
});
}
sum(callback = null) {
return derived(this.filteredRows, $filteredRows => {
const values = $filteredRows.map(row => this.callback(row));
const array = callback ? callback(values) : values;
return this.round(array.reduce((acc, curr) => acc + curr, 0));
});
}
bounds(callback = null) {
const rawRows = get(this.rawRows);
const values = rawRows.map(row => this.callback(row));
const numbers = callback ? callback(values) : values;
return [
Math.min(...numbers.filter(Boolean)),
Math.max(...numbers.filter(Boolean))
];
}
setPrecision(value) {
this.precision = value;
}
round(value) {
if (this.precision === 0) {
return Math.round(value);
}
const denominator = Math.pow(10, this.precision);
return Math.round((value + Number.EPSILON) * denominator) / denominator;
}
}

View File

@@ -0,0 +1,15 @@
import type { Field, Comparator } from '..';
import type FilterHandler from '../handlers/FilterHandler';
type Value = string | number | boolean;
export default class FilterHelper<Row> {
private filterHandler;
private filterBy;
private uid;
private comparator;
private callback;
constructor(filterHandler: FilterHandler<Row>, filterBy: Field<Row>, comparator?: Comparator<Row>);
set(value: Value, comparator?: Comparator<any>): void;
clear(): void;
on(event: 'clear', callback: () => void): void;
}
export {};

View File

@@ -0,0 +1,28 @@
import { check } from '../Comparator';
export default class FilterHelper {
filterHandler;
filterBy;
uid;
comparator;
callback;
constructor(filterHandler, filterBy, comparator) {
this.filterHandler = filterHandler;
this.filterBy = filterBy;
this.uid = 'f_' + (Math.random()).toString(28).substring(2);
this.comparator = comparator ?? check.isLike;
this.callback = () => null;
}
set(value, comparator) {
if (comparator) {
this.comparator = comparator;
}
this.filterHandler.set(value, this.filterBy, this.comparator, this.uid);
}
clear() {
this.callback();
this.filterHandler.set(undefined, this.filterBy);
}
on(event, callback) {
this.callback = callback;
}
}

View File

@@ -0,0 +1,67 @@
import DataHandler from './DataHandler';
import Datatable from './Datatable.svelte';
import Th from './Th.svelte';
import ThFilter from './ThFilter.svelte';
import Pagination from './Pagination.svelte';
import RowCount from './RowCount.svelte';
import RowsPerPage from './RowsPerPage.svelte';
import Search from './Search.svelte';
import { check } from './Comparator';
export { DataHandler, check, Datatable, Th, ThFilter, Pagination, RowCount, RowsPerPage, Search };
export type { default as EventHandler } from './handlers/EventHandler';
export type Internationalization = {
search?: string;
show?: string;
entries?: string;
filter?: string;
rowCount?: string;
noRows?: string;
previous?: string;
next?: string;
};
export type Row = {
[key: string]: any;
};
export type Field<Row> = keyof Row | ((row: Row) => Row[keyof Row]);
export type Comparator<Row> = (entry: Row[keyof Row], value: any) => boolean;
export type Criterion = {
value: string | number | [min: number, max: number];
comparator: Comparator<Row>;
};
export type Filter<Row> = {
callback: (row: Row) => Row[keyof Row];
identifier: string;
value?: string | number | boolean | symbol | Criterion[] | number[];
comparator?: Comparator<Row>;
key?: string;
};
export type Sort<Row> = {
callback?: (row: Row) => Row[keyof Row];
identifier?: string;
direction?: 'asc' | 'desc';
key?: string;
};
/**
* @deprecated use (Row[keyof Row] | Row) instead
*
* import type { Row } from '@vincjo/datatables'
*/
export type Selectable<Row> = Row[keyof Row] | Row;
/**
* @deprecated use type Field<Row> instead
*
* import type { Field } from '@vincjo/datatables'
*/
export type FilterBy<Row> = Field<Row>;
/**
* @deprecated use type Field<Row> instead
*
* import type { Field } from '@vincjo/datatables'
*/
export type OrderBy<Row> = Field<Row>;
/**
* @deprecated use type Sort<Row> instead
*
* import type { Sort } from '@vincjo/datatables'
*/
export type Order<Row> = Sort<Row>;

View File

@@ -0,0 +1,11 @@
// Reexport your entry components here
import DataHandler from './DataHandler';
import Datatable from './Datatable.svelte';
import Th from './Th.svelte';
import ThFilter from './ThFilter.svelte';
import Pagination from './Pagination.svelte';
import RowCount from './RowCount.svelte';
import RowsPerPage from './RowsPerPage.svelte';
import Search from './Search.svelte';
import { check } from './Comparator';
export { DataHandler, check, Datatable, Th, ThFilter, Pagination, RowCount, RowsPerPage, Search };

View File

@@ -0,0 +1,8 @@
import type { Row, Field } from './';
export declare const isNull: (value: any) => boolean;
export declare const isNotNull: (value: any) => boolean;
export declare const parseField: (field: Field<any>, uid?: string) => {
callback: (row: Row) => any;
identifier: string;
key: string;
};

View File

@@ -0,0 +1,24 @@
export const isNull = (value) => {
if (value === null || value === undefined || value === '')
return true;
return false;
};
export const isNotNull = (value) => { return !isNull(value); };
export const parseField = (field, uid) => {
const identifier = uid ?? field.toString();
if (typeof field === 'string') {
return {
callback: (row) => row[field],
identifier,
key: field
};
}
else if (typeof field === 'function') {
return {
callback: field,
identifier,
key: undefined
};
}
throw new Error(`Invalid field argument: ${String(field)}`);
};

View File

@@ -0,0 +1,37 @@
import { type Writable, type Readable } from 'svelte/store';
import type { State, Sort, Filter } from './';
import type { Params } from './DataHandler';
import EventHandler from './handlers/EventHandler';
export default class Context<Row> {
totalRows: Writable<number | undefined>;
rowsPerPage: Writable<number>;
pageNumber: Writable<number>;
event: EventHandler;
search: Writable<string>;
filters: Writable<Filter<Row>[]>;
rows: Writable<Row[]>;
rowCount: Readable<{
total: number;
start: number;
end: number;
}>;
pages: Readable<number[]>;
pagesWithEllipsis: Readable<number[]>;
pageCount: Readable<number>;
sort: Writable<Sort<Row>>;
selected: Writable<(Row | Row[keyof Row])[]>;
isAllSelected: Readable<boolean>;
selectedCount: Readable<{
count: number;
total: number;
}>;
selectBy: keyof Row | undefined;
constructor(data: Row[], params: Params);
getState(): State;
private createPages;
private createPagesWithEllipsis;
private createPageCount;
private createRowCount;
private createIsAllSelected;
private createSelectedCount;
}

View File

@@ -0,0 +1,140 @@
import { writable, get, derived } from 'svelte/store';
import EventHandler from './handlers/EventHandler';
export default class Context {
totalRows;
rowsPerPage;
pageNumber;
event;
search;
filters;
rows;
rowCount;
pages;
pagesWithEllipsis;
pageCount;
sort;
selected;
isAllSelected;
selectedCount;
selectBy;
constructor(data, params) {
this.totalRows = writable(params.totalRows);
this.rowsPerPage = writable(params.rowsPerPage);
this.pageNumber = writable(1);
this.event = new EventHandler();
this.search = writable('');
this.filters = writable([]);
this.rows = writable(data);
this.rowCount = this.createRowCount();
this.pages = this.createPages();
this.pagesWithEllipsis = this.createPagesWithEllipsis();
this.pageCount = this.createPageCount();
this.sort = writable(undefined);
this.selected = writable([]);
this.isAllSelected = this.createIsAllSelected();
this.selectedCount = this.createSelectedCount();
this.selectBy = params.selectBy ?? undefined;
}
getState() {
const pageNumber = get(this.pageNumber);
const rowsPerPage = get(this.rowsPerPage);
const sort = get(this.sort);
const filters = get(this.filters);
return {
pageNumber,
rowsPerPage,
offset: rowsPerPage * (pageNumber - 1),
search: get(this.search),
sorted: sort ?? undefined, // deprecated
sort: sort ?? undefined,
filters: filters.length > 0 ? filters : undefined,
setTotalRows: (value) => this.totalRows.set(value)
};
}
createPages() {
return derived([this.rowsPerPage, this.totalRows], ([$rowsPerPage, $totalRows]) => {
if (!$rowsPerPage || !$totalRows) {
return undefined;
}
const pages = Array.from(Array(Math.ceil($totalRows / $rowsPerPage)));
return pages.map((_, i) => {
return i + 1;
});
});
}
createPagesWithEllipsis() {
return derived([this.pages, this.pageNumber], ([$pages, $pageNumber]) => {
if (!$pages) {
return undefined;
}
if ($pages.length <= 7) {
return $pages;
}
const ellipse = null;
const firstPage = 1;
const lastPage = $pages.length;
if ($pageNumber <= 4) {
return [
...$pages.slice(0, 5),
ellipse,
lastPage
];
}
else if ($pageNumber < $pages.length - 3) {
return [
firstPage,
ellipse,
...$pages.slice($pageNumber - 2, $pageNumber + 1),
ellipse,
lastPage
];
}
else {
return [
firstPage,
ellipse,
...$pages.slice($pages.length - 5, $pages.length)
];
}
});
}
createPageCount() {
return derived(this.pages, ($pages) => {
if (!$pages)
return undefined;
return $pages.length;
});
}
createRowCount() {
return derived([this.totalRows, this.pageNumber, this.rowsPerPage], ([$totalRows, $pageNumber, $rowsPerPage]) => {
if (!$rowsPerPage || !$totalRows) {
return undefined;
}
return {
total: $totalRows,
start: $pageNumber * $rowsPerPage - $rowsPerPage + 1,
end: Math.min($pageNumber * $rowsPerPage, $totalRows)
};
});
}
createIsAllSelected() {
return derived([this.selected, this.rows], ([$selected, $rows]) => {
if ($rows.length === 0) {
return false;
}
if (this.selectBy) {
const ids = $rows.map(row => row[this.selectBy]);
return ids.every(id => $selected.includes(id));
}
return $rows.every(row => $selected.includes(row));
});
}
createSelectedCount() {
return derived([this.selected, this.totalRows], ([$selected, $totalRows]) => {
return {
count: $selected.length,
total: $totalRows
};
});
}
}

View File

@@ -0,0 +1,77 @@
import type { Writable, Readable } from 'svelte/store';
import type { Internationalization, Row, State, Sort } from './';
export type Params = {
rowsPerPage?: number;
totalRows?: number;
selectBy?: keyof Row;
i18n?: Internationalization;
};
export default class DataHandler<T extends Row = any> {
private context;
private triggerHandler;
private sortHandler;
private selectHandler;
private pageHandler;
private searchHandler;
private filterHandler;
i18n: Internationalization;
constructor(data?: T[], params?: Params);
onChange(callback: (state: State) => Promise<T[]>): void;
invalidate(): void;
setRows(data: T[]): void;
setTotalRows(value: number): void;
getRows(): Writable<T[]>;
select(value: T[keyof T] | T): void;
getSelected(): Writable<(T | T[keyof T])[]>;
selectAll(): void;
isAllSelected(): Readable<boolean>;
getSelectedCount(): Readable<{
count: number;
total: number;
}>;
clearSelection(): void;
getRowsPerPage(): Writable<number | null>;
sort(orderBy: keyof T): void;
applySort(params?: {
orderBy: keyof T;
direction?: 'asc' | 'desc';
}): void;
sortAsc(orderBy: keyof T): void;
sortDesc(orderBy: keyof T): void;
getSort(): Writable<Sort<T>>;
search(value: string): void;
clearSearch(): void;
filter(value: string | number, filterBy: keyof T): void;
clearFilters(): void;
getPages(params?: {
ellipsis: boolean;
}): Readable<number[]>;
getPageCount(): Readable<number>;
getPageNumber(): Writable<number>;
setPage(value: number | 'previous' | 'next'): void;
getRowCount(): Readable<{
total: number;
start: number;
end: number;
}>;
on(event: 'change', callback: () => void): void;
translate(i18n: Internationalization): Internationalization;
/**
*
* @depracted use on('change', callback) instead
*/
getTriggerChange(): Writable<number>;
/**
*
* @deprecated use applySort() instead
*/
applySorting(params?: {
orderBy: keyof T;
direction?: 'asc' | 'desc';
}): void;
/**
*
* @deprecated use getSort() instead
*/
getSorted(): Writable<Sort<T>>;
}

View File

@@ -0,0 +1,157 @@
import Context from './Context';
import TriggerHandler from './handlers/TriggerHandler';
import SortHandler from './handlers/SortHandler';
import SelectHandler from './handlers/SelectHandler';
import PageHandler from './handlers/PageHandler';
import SearchHandler from './handlers/SearchHandler';
import FilterHandler from './handlers/FilterHandler';
export default class DataHandler {
context;
triggerHandler;
sortHandler;
selectHandler;
pageHandler;
searchHandler;
filterHandler;
i18n;
constructor(data = [], params = { rowsPerPage: 5 }) {
this.i18n = this.translate(params.i18n);
this.context = new Context(data, params);
this.triggerHandler = new TriggerHandler(this.context);
this.sortHandler = new SortHandler(this.context);
this.selectHandler = new SelectHandler(this.context);
this.pageHandler = new PageHandler(this.context);
this.searchHandler = new SearchHandler(this.context);
this.filterHandler = new FilterHandler(this.context);
}
onChange(callback) {
this.triggerHandler.set(callback);
}
invalidate() {
this.triggerHandler.invalidate();
}
setRows(data) {
this.context.rows.set(data);
}
setTotalRows(value) {
this.context.totalRows.set(value);
}
getRows() {
return this.context.rows;
}
select(value) {
this.selectHandler.set(value);
}
getSelected() {
return this.context.selected;
}
selectAll() {
this.selectHandler.all();
}
isAllSelected() {
return this.context.isAllSelected;
}
getSelectedCount() {
return this.context.selectedCount;
}
clearSelection() {
this.selectHandler.clear();
}
getRowsPerPage() {
return this.context.rowsPerPage;
}
sort(orderBy) {
this.setPage(1);
this.sortHandler.set(orderBy);
}
applySort(params = null) {
this.sortHandler.apply(params);
}
sortAsc(orderBy) {
this.setPage(1);
this.sortHandler.asc(orderBy);
}
sortDesc(orderBy) {
this.setPage(1);
this.sortHandler.desc(orderBy);
}
getSort() {
return this.context.sort;
}
search(value) {
this.setPage(1);
this.context.search.set(value);
}
clearSearch() {
this.searchHandler.remove();
}
filter(value, filterBy) {
this.setPage(1);
return this.filterHandler.set(value, filterBy);
}
clearFilters() {
this.filterHandler.remove();
}
getPages(params = { ellipsis: false }) {
if (params.ellipsis) {
return this.context.pagesWithEllipsis;
}
return this.context.pages;
}
getPageCount() {
return this.context.pageCount;
}
getPageNumber() {
return this.context.pageNumber;
}
setPage(value) {
switch (value) {
case 'previous': return this.pageHandler.previous();
case 'next': return this.pageHandler.next();
default: return this.pageHandler.goto(value);
}
}
getRowCount() {
return this.context.rowCount;
}
on(event, callback) {
this.context.event.add(event, callback);
}
translate(i18n) {
return {
...{
search: 'Search...',
show: 'Show',
entries: 'entries',
filter: 'Filter',
rowCount: 'Showing {start} to {end} of {total} entries',
noRows: 'No entries found',
previous: 'Previous',
next: 'Next',
selectedCount: '{count} of {total} row(s).'
},
...i18n
};
}
/**
*
* @depracted use on('change', callback) instead
*/
getTriggerChange() {
return this.context.event.triggerChange;
}
/**
*
* @deprecated use applySort() instead
*/
applySorting(params = null) {
this.applySort(params);
}
/**
*
* @deprecated use getSort() instead
*/
getSorted() {
return this.getSort();
}
}

View File

@@ -0,0 +1,98 @@
<script lang="ts">import { Search, RowsPerPage, RowCount, SelectedCount, Pagination } from './';
export let handler;
export let search = true;
export let rowsPerPage = true;
export let rowCount = true;
export let selectedCount = false;
export let pagination = true;
let element;
let clientWidth = 1000;
const height = (search || rowsPerPage ? 48 : 8) + (rowCount || selectedCount || pagination ? 48 : 8);
handler.on('change', () => {
if (element)
element.scrollTop = 0;
});
</script>
<section bind:clientWidth class={$$props.class ?? ''}>
<header class:container={search || rowsPerPage}>
{#if search}
<Search {handler} />
{:else}
<div></div>
{/if}
{#if rowsPerPage}
<RowsPerPage {handler} small={clientWidth < 600} />
{/if}
</header>
<article bind:this={element} style="height:calc(100% - {height}px)">
<slot />
</article>
<footer class:container={rowCount || pagination}>
{#if selectedCount}
<SelectedCount {handler}/>
{:else if rowCount}
<RowCount {handler} small={clientWidth < 600} />
{/if}
{#if pagination}
<Pagination {handler} small={clientWidth < 600} />
{/if}
</footer>
</section>
<style>
section {
height: 100%;
}
section :global(table) {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
section :global(thead) {
position: sticky;
inset-block-start: 0;
z-index: 1;
}
header,
footer {
min-height: 8px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
header.container,
footer.container {
height: 48px;
}
footer {
border-top: 1px solid #e0e0e0;
}
article {
position: relative;
/* height:calc(100% - 96px); */
overflow: auto;
scrollbar-width: thin;
}
article::-webkit-scrollbar {
width: 6px;
height: 6px;
}
article::-webkit-scrollbar-track {
background: #f5f5f5;
}
article::-webkit-scrollbar-thumb {
background: #c2c2c2;
}
article::-webkit-scrollbar-thumb:hover {
background: #9e9e9e;
}
</style>

View File

@@ -0,0 +1,36 @@
import { type DataHandler, type Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
search?: boolean;
rowsPerPage?: boolean;
rowCount?: boolean;
selectedCount?: boolean;
pagination?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {
default: {};
};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']> & {
children?: any;
}>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
$$slots?: ReturnType<__sveltets_Render<T>['slots']>;
children?: any;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Datatable: $$IsomorphicComponent;
type Datatable<T extends Row> = InstanceType<typeof Datatable<T>>;
export default Datatable;

View File

@@ -0,0 +1,146 @@
<script lang="ts">export let handler;
export let small = false;
const pageNumber = handler.getPageNumber();
const pageCount = handler.getPageCount();
const pages = handler.getPages({ ellipsis: true });
const setPage = (value) => {
handler.setPage(value);
handler.invalidate();
};
</script>
<section class={$$props.class ?? ''}>
{#if $pages === undefined}
<button type="button"
class="small"
class:disabled={$pageNumber === 1}
on:click={() => setPage('previous')}
>
&#10094;
</button>
<button type="button" class="page">page <b>{$pageNumber}</b></button>
<button type="button"
class="small"
on:click={() => setPage('next')}
>
&#10095;
</button>
{:else}
{#if small}
<button type="button"
class="small"
class:disabled={$pageNumber === 1}
on:click={() => setPage(1)}
>
&#10092;&#10092;
</button>
<button type="button"
class:disabled={$pageNumber === 1}
on:click={() => setPage('previous')}
>
&#10094;
</button>
<button type="button"
class:disabled={$pageNumber === $pageCount}
on:click={() => setPage('next')}
>
&#10095;
</button>
<button type="button"
class="small"
class:disabled={$pageNumber === $pageCount}
on:click={() => setPage($pageCount)}
>
&#10093;&#10093;
</button>
{:else}
<button type="button"
class:disabled={$pageNumber === 1}
on:click={() => setPage('previous')}
>
{@html handler.i18n.previous}
</button>
{#each $pages as page}
<button type="button"
class:active={$pageNumber === page}
class:ellipse={page === null}
on:click={() => setPage(page)}
>
{page ?? '...'}
</button>
{/each}
<button type="button"
class:disabled={$pageNumber === $pageCount}
on:click={() => setPage('next')}
>
{@html handler.i18n.next}
</button>
{/if}
{/if}
</section>
<style>
section {
display: flex;
}
button {
background: inherit;
height: 32px;
width: 32px;
color: #616161;
cursor: pointer;
font-size: 13px;
margin: 0;
padding: 0;
transition: all, 0.2s;
line-height: 32px;
border: 1px solid #e0e0e0;
border-right: none;
border-radius: 0;
outline: none;
}
button:first-child {
border-radius: 4px 0 0 4px;
}
button:last-child {
border-right: 1px solid #e0e0e0;
border-radius: 0 4px 4px 0;
}
button:first-child:not(.small),
button:last-child:not(.small) {
min-width: 72px;
}
button:not(.active):hover {
background: #eee;
}
button.disabled:hover {
background: inherit;
cursor: default;
}
button.page {
width: 72px;
background: #fafafa;
}
button.page:hover {
background: #fafafa;
cursor: default;
}
button.ellipse:hover {
background: inherit;
cursor: default;
}
button.active {
background: #eee;
font-weight: bold;
cursor: default;
}
button.disabled {
color: #bdbdbd;
}
button.disabled:hover {
background: inherit;
cursor: default;
}
</style>

View File

@@ -0,0 +1,26 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Pagination: $$IsomorphicComponent;
type Pagination<T extends Row> = InstanceType<typeof Pagination<T>>;
export default Pagination;

View File

@@ -0,0 +1,12 @@
<div align="center">
<img align="center" src="../../../static/logo-remote.svg" alt="logo" width="172"/>
<p align="center">
<h1 align="center" style="font-size:32px;margin:0;border:none;">svelte simple datatables</h1>
<p style="color:#eee">For server-side pagination</p>
<img src="https://img.shields.io/npm/v/@vincjo/datatables?color=%23006990" alt="npm version"/>
<img src="https://img.shields.io/github/license/vincjo/datatables?color=006990" alt="last commit"/>
</p>
</div>
<p><strong><a href="https://vincjo.fr/datatables/remote/home" rel="nofollow">Home</a></strong></p>
<p><strong><a href="https://vincjo.fr/datatables/remote/examples" rel="nofollow">Examples</a></strong></p>

View File

@@ -0,0 +1,36 @@
<script lang="ts">export let handler;
export let small = false;
const rowCount = handler.getRowCount();
</script>
{#if $rowCount === undefined}
<div></div>
{:else}
<aside class={$$props.class ?? ''}>
{#if small}
{#if $rowCount.total > 0}
<b>{$rowCount.start}</b>-
<b>{$rowCount.end}</b>/
<b>{$rowCount.total}</b>
{:else}
{handler.i18n.noRows}
{/if}
{:else if $rowCount.total > 0}
{@html handler.i18n.rowCount
.replace('{start}', `<b>${$rowCount.start}</b>`)
.replace('{end}', `<b>${$rowCount.end}</b>`)
.replace('{total}', `<b>${$rowCount.total}</b>`)}
{:else}
{handler.i18n.noRows}
{/if}
</aside>
{/if}
<style>
aside {
color: #616161;
line-height: 32px;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,26 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const RowCount: $$IsomorphicComponent;
type RowCount<T extends Row> = InstanceType<typeof RowCount<T>>;
export default RowCount;

View File

@@ -0,0 +1,38 @@
<script lang="ts">export let handler;
export let small = false;
export let options = [5, 10, 20];
const rowsPerPage = handler.getRowsPerPage();
const setRowsPerPage = () => {
handler.setPage(1);
handler.invalidate();
};
</script>
<aside class={$$props.class ?? ''}>
{#if !small}
<span>{handler.i18n.show}</span>
{/if}
<select bind:value={$rowsPerPage} on:change={setRowsPerPage}>
{#each options as option}
<option value={option}>
{option}
</option>
{/each}
</select>
{#if !small}
<span>{handler.i18n.entries}</span>
{/if}
</aside>
<style>
aside {
display: flex;
justify-content: flex-start;
align-items: center;
height: 32px;
color: #757575;
}
select {
margin: 0 4px;
}
</style>

View File

@@ -0,0 +1,27 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
small?: boolean;
options?: number[];
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const RowsPerPage: $$IsomorphicComponent;
type RowsPerPage<T extends Row> = InstanceType<typeof RowsPerPage<T>>;
export default RowsPerPage;

View File

@@ -0,0 +1,43 @@
<script lang="ts">export let handler;
let value = '';
let timeout;
const search = () => {
handler.search(value);
clearTimeout(timeout);
timeout = setTimeout(() => {
handler.invalidate();
}, 400);
};
</script>
<input
class={$$props.class ?? ''}
bind:value
placeholder={handler.i18n.search}
spellcheck="false"
on:input={search}
/>
<style>
input {
border: 1px solid #e0e0e0;
border-radius: 4px;
outline: none;
padding: 0 8px;
line-height: 24px;
margin: 0;
height: 24px;
background: transparent;
width: 40%;
max-width: 176px;
min-width: 96px;
transition: all, 0.1s;
}
input:focus {
border: 1px solid #bdbdbd;
}
input::placeholder {
color: #9e9e9e;
line-height: 24px;
}
</style>

View File

@@ -0,0 +1,25 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Search: $$IsomorphicComponent;
type Search<T extends Row> = InstanceType<typeof Search<T>>;
export default Search;

View File

@@ -0,0 +1,32 @@
<script lang="ts">export let handler;
const selectedCount = handler.getSelectedCount();
</script>
<aside class={$$props.class ?? ''}>
<b>{$selectedCount.count}</b>
{#if $selectedCount.total}
of <b>{$selectedCount.total}</b>
{/if}
row(s) selected.
{#if $selectedCount.count > 0}
<button type="button" on:click={() => handler.clearSelection()}> Clear</button>
{/if}
</aside>
<style>
aside {
color: #616161;
line-height: 32px;
font-size: 14px;
}
button {
background: #fafafa;
padding: 2px 4px;
border-radius: 2px;
cursor: pointer;
border: 1px solid #e0e0e0;
}
</style>

View File

@@ -0,0 +1,25 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const SelectedCount: $$IsomorphicComponent;
type SelectedCount<T extends Row> = InstanceType<typeof SelectedCount<T>>;
export default SelectedCount;

View File

@@ -0,0 +1,81 @@
<script lang="ts">export let handler;
export let orderBy;
export let align = 'left';
export let rowSpan = 1;
const sort = handler.getSort();
const update = () => {
handler.sort(orderBy);
handler.invalidate();
};
</script>
<th
on:click={update}
class:sortable={orderBy}
class:active={$sort?.orderBy === orderBy}
class={$$props.class ?? ''}
rowspan={rowSpan}
>
<div
class="flex"
style:justify-content={align === 'left' ? 'flex-start' : align === 'right' ? 'flex-end' : 'center'}
>
<strong>
<slot />
</strong>
<span class:asc={$sort?.direction === 'asc'} class:desc={$sort?.direction === 'desc'}></span>
</div>
</th>
<style>
th {
background: inherit;
padding: 8px 20px;
white-space: nowrap;
font-size: 13px;
user-select: none;
border-bottom: 1px solid #e0e0e0;
}
th.sortable {
cursor: pointer;
}
th strong {
white-space: pre-wrap;
font-size: 13.5px;
line-height: 16px;
}
th.sortable div.flex {
padding: 0;
display: flex;
align-items: center;
height: 100%;
}
th.sortable span {
padding-left: 8px;
}
th.sortable span:before,
th.sortable span:after {
border: 4px solid transparent;
content: '';
display: block;
height: 0;
width: 0;
}
th.sortable span:before {
border-bottom-color: #e0e0e0;
margin-top: 2px;
}
th.sortable span:after {
border-top-color: #e0e0e0;
margin-top: 2px;
}
th.active.sortable span.asc:before {
border-bottom-color: #9e9e9e;
}
th.active.sortable span.desc:after {
border-top-color: #9e9e9e;
}
th:not(.sortable) span {
visibility: hidden;
}
</style>

View File

@@ -0,0 +1,34 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
orderBy: keyof T;
align?: "left" | "right" | "center";
rowSpan?: number;
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {
default: {};
};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']> & {
children?: any;
}>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
$$slots?: ReturnType<__sveltets_Render<T>['slots']>;
children?: any;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const Th: $$IsomorphicComponent;
type Th<T extends Row> = InstanceType<typeof Th<T>>;
export default Th;

View File

@@ -0,0 +1,51 @@
<script lang="ts">export let handler;
export let filterBy;
export let align = 'left';
let value = '';
let timeout;
const filter = () => {
handler.filter(value, filterBy);
clearTimeout(timeout);
timeout = setTimeout(() => {
handler.invalidate();
}, 400);
};
</script>
<th class={$$props.class ?? ''}>
<input
style:text-align={align}
type="text"
placeholder={handler.i18n.filter}
spellcheck="false"
bind:value
on:input={filter}
/>
</th>
<style>
th {
border-bottom: 1px solid #e0e0e0;
}
input {
width: 100%;
height: 24px;
border: none;
text-align: left;
padding: 0 20px;
background: inherit;
outline: none;
border-radius: 0;
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
}
input::placeholder {
color: #bdbdbd;
font-style: italic;
font-size: 13px;
}
input:focus {
outline: none;
border: none;
}
</style>

View File

@@ -0,0 +1,27 @@
import type { DataHandler, Row } from './';
declare class __sveltets_Render<T extends Row> {
props(): {
[x: string]: any;
handler: DataHandler<T>;
filterBy: keyof T;
align?: "left" | "right" | "center";
};
events(): {} & {
[evt: string]: CustomEvent<any>;
};
slots(): {};
bindings(): string;
exports(): {};
}
interface $$IsomorphicComponent {
new <T extends Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
} & ReturnType<__sveltets_Render<T>['exports']>;
<T extends Row>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {
$$events?: ReturnType<__sveltets_Render<T>['events']>;
}): ReturnType<__sveltets_Render<T>['exports']>;
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
}
declare const ThFilter: $$IsomorphicComponent;
type ThFilter<T extends Row> = InstanceType<typeof ThFilter<T>>;
export default ThFilter;

View File

@@ -0,0 +1,6 @@
export default class EventHandler {
private events;
triggerChange: import("svelte/store").Writable<number>;
add(event: keyof EventHandler['events'], callback: () => void): void;
trigger(event: keyof EventHandler['events']): void;
}

View File

@@ -0,0 +1,21 @@
import { writable } from 'svelte/store';
export default class EventHandler {
events = {
change: [],
clearFilters: [],
clearSearch: []
};
triggerChange = writable(0); // legacy
add(event, callback) {
this.events[event].push(callback);
}
trigger(event) {
for (const callback of this.events[event]) {
callback();
}
/* legacy: support for triggerChange store */
if (event === 'change') {
this.triggerChange.update((store) => { return store + 1; });
}
}
}

View File

@@ -0,0 +1,9 @@
import type { Filter } from '..';
import type Context from '../Context';
import type { Writable } from 'svelte/store';
export default class FilterHandler<Row> {
filters: Writable<Filter<Row>[]>;
constructor(context: Context<Row>);
set(value: string | number, filterBy: keyof Row): void;
remove(): void;
}

View File

@@ -0,0 +1,21 @@
export default class FilterHandler {
filters;
constructor(context) {
this.filters = context.filters;
}
set(value, filterBy) {
const filter = { filterBy, value };
this.filters.update((store) => {
store = store.filter((item) => {
return (item.filterBy !== filterBy) && item.value;
});
if (value) {
store.push(filter);
}
return store;
});
}
remove() {
this.filters.set([]);
}
}

View File

@@ -0,0 +1,21 @@
import type Context from '../Context';
import { type Writable, type Readable } from 'svelte/store';
import type EventHandler from './EventHandler';
export default class PageHandler<Row> {
totalRows: Writable<number>;
pageNumber: Writable<number>;
rowCount: Readable<{
total: number;
start: number;
end: number;
}>;
rowsPerPage: Writable<number | null>;
event: EventHandler;
pages: Readable<number[]>;
selected: Writable<(Row | Row[keyof Row])[]>;
constructor(context: Context<Row>);
get(): Readable<number[]>;
goto(number: number): void;
previous(): void;
next(): void;
}

View File

@@ -0,0 +1,50 @@
import { get } from 'svelte/store';
export default class PageHandler {
totalRows;
pageNumber;
rowCount;
rowsPerPage;
event;
pages;
selected;
constructor(context) {
this.totalRows = context.totalRows;
this.pageNumber = context.pageNumber;
this.rowCount = context.rowCount;
this.rowsPerPage = context.rowsPerPage;
this.event = context.event;
this.pages = context.pages;
this.selected = context.selected;
}
get() {
return this.pages;
}
goto(number) {
const rowsPerPage = get(this.rowsPerPage);
const totalRows = get(this.totalRows);
this.pageNumber.update((store) => {
if (rowsPerPage && totalRows) {
if (number >= 1 && number <= Math.ceil(totalRows / rowsPerPage)) {
store = number;
this.event.trigger('change');
}
return store;
}
else {
if (number >= 1) {
store = number;
this.event.trigger('change');
}
return store;
}
});
}
previous() {
const number = get(this.pageNumber) - 1;
this.goto(number);
}
next() {
const number = get(this.pageNumber) + 1;
this.goto(number);
}
}

View File

@@ -0,0 +1,7 @@
import type Context from '../Context';
export default class SearchHandler<Row> {
private search;
constructor(context: Context<Row>);
set(value: string): void;
remove(): void;
}

View File

@@ -0,0 +1,12 @@
export default class SearchHandler {
search;
constructor(context) {
this.search = context.search;
}
set(value) {
this.search.set(value ?? null);
}
remove() {
this.search.set(null);
}
}

View File

@@ -0,0 +1,11 @@
import type Context from '../Context';
export default class SelectHandler<Row> {
private rows;
private selected;
private isAllSelected;
private selectBy;
constructor(context: Context<Row>);
set(value: Row | Row[keyof Row]): void;
all(): void;
clear(): void;
}

View File

@@ -0,0 +1,46 @@
import { get } from 'svelte/store';
export default class SelectHandler {
rows;
selected;
isAllSelected;
selectBy;
constructor(context) {
this.rows = context.rows;
this.selected = context.selected;
this.isAllSelected = context.isAllSelected;
this.selectBy = context.selectBy;
}
set(value) {
const selected = get(this.selected);
if (selected.includes(value)) {
this.selected.set(selected.filter((item) => item !== value));
}
else {
this.selected.set([value, ...selected]);
}
}
all() {
const rows = get(this.rows);
const isAllSelected = get(this.isAllSelected);
this.selected.update(store => {
if (this.selectBy) {
return store = store.filter(item => !rows.map((row) => row[this.selectBy]).includes(item));
}
return store = store.filter(item => !rows.includes(item));
});
if (!isAllSelected) {
this.selected.update(store => {
if (this.selectBy) {
store = [...rows.map((row) => row[this.selectBy]), ...store];
}
else {
store = [...rows, ...store];
}
return store;
});
}
}
clear() {
this.selected.set([]);
}
}

View File

@@ -0,0 +1,14 @@
import type Context from '../Context';
export default class SortHandler<Row> {
private event;
private hasMultipleSort;
private sort;
constructor(context: Context<Row>);
set(orderBy?: keyof Row): void;
asc(orderBy: keyof Row): void;
desc(orderBy: keyof Row): void;
apply(params?: {
orderBy: keyof Row;
direction?: 'asc' | 'desc';
}): any;
}

View File

@@ -0,0 +1,51 @@
import { get } from 'svelte/store';
export default class SortHandler {
event;
hasMultipleSort;
sort;
constructor(context) {
this.event = context.event;
this.hasMultipleSort = false;
this.sort = context.sort;
}
set(orderBy = null) {
if (!orderBy)
return;
const sort = get(this.sort);
if (!sort || sort.orderBy !== orderBy) {
this.asc(orderBy);
}
else if (sort.direction === 'asc') {
this.desc(sort.orderBy);
}
else if (sort.direction === 'desc') {
this.asc(orderBy);
}
}
asc(orderBy) {
if (!orderBy)
return;
this.sort.set({ orderBy, direction: 'asc' });
this.event.trigger('change');
}
desc(orderBy) {
if (!orderBy)
return;
this.sort.set({ orderBy, direction: 'desc' });
this.event.trigger('change');
}
apply(params = null) {
if (params) {
switch (params.direction) {
case 'asc': return this.asc(params.orderBy);
case 'desc': return this.desc(params.orderBy);
default: return this.set(params.orderBy);
}
}
const sort = get(this.sort);
if (sort) {
return this.apply({ orderBy: sort.orderBy, direction: sort.direction });
}
return;
}
}

View File

@@ -0,0 +1,9 @@
import type { State } from '..';
import type Context from '../Context';
export default class TriggerHandler<Row> {
private context;
private reload;
constructor(context: Context<Row>);
set(callback: (state: State) => Promise<Row[]>): void;
invalidate(): Promise<void>;
}

View File

@@ -0,0 +1,19 @@
export default class TriggerHandler {
context;
reload;
constructor(context) {
this.context = context;
}
set(callback) {
this.reload = callback;
}
async invalidate() {
if (!this.reload)
return;
const state = this.context.getState();
const data = await this.reload(state);
if (data) {
this.context.rows.set(data);
}
}
}

View File

@@ -0,0 +1,59 @@
import DataHandler from './DataHandler';
import Datatable from './Datatable.svelte';
import Search from './Search.svelte';
import RowsPerPage from './RowsPerPage.svelte';
import Th from './Th.svelte';
import ThFilter from './ThFilter.svelte';
import RowCount from './RowCount.svelte';
import SelectedCount from './SelectedCount.svelte';
import Pagination from './Pagination.svelte';
export { DataHandler, Datatable, Search, RowsPerPage, Th, ThFilter, RowCount, SelectedCount, Pagination };
export type Internationalization = {
search?: string;
show?: string;
entries?: string;
filter?: string;
rowCount?: string;
noRows?: string;
previous?: string;
next?: string;
selectedCount?: string;
};
export type Row = {
[key: string]: unknown;
};
export type Filter<Row> = {
filterBy: keyof Row;
value?: string | number | boolean;
};
export type Sort<Row> = {
orderBy?: keyof Row;
direction?: 'asc' | 'desc';
};
export type State = {
pageNumber: number;
rowsPerPage: number;
offset: number;
search: string | undefined;
sort: Sort<Row> | undefined;
filters: Filter<Row>[] | undefined;
setTotalRows: (value: number) => void;
/**
* @deprecated use 'sort' instead
*/
sorted: Sort<Row> | undefined;
};
/**
* @deprecated use (Row[keyof Row] | Row) instead
* @since v1.13.0 2023-11-14
*
* import type { Row } from '@vincjo/datatables'
*/
export type Selectable<Row> = Row[keyof Row] | Row;
/**
* @deprecated use type Sort<Row> instead
* @since v1.13.0 2023-11-14
*
* import type { Sort } from '@vincjo/datatables'
*/
export type Order<Row> = Sort<Row>;

View File

@@ -0,0 +1,11 @@
// Reexport your entry components here
import DataHandler from './DataHandler';
import Datatable from './Datatable.svelte';
import Search from './Search.svelte';
import RowsPerPage from './RowsPerPage.svelte';
import Th from './Th.svelte';
import ThFilter from './ThFilter.svelte';
import RowCount from './RowCount.svelte';
import SelectedCount from './SelectedCount.svelte';
import Pagination from './Pagination.svelte';
export { DataHandler, Datatable, Search, RowsPerPage, Th, ThFilter, RowCount, SelectedCount, Pagination };

View File

@@ -0,0 +1 @@
export { TableHandler, Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, type Row, type State, type Filter, type Sort, type Internationalization, type TableHandlerInterface } from '../src/server/index.js';

View File

@@ -0,0 +1,5 @@
export {
// class:
TableHandler,
// components:
Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount } from '../src/server/index.js';

View File

@@ -0,0 +1,39 @@
import type { Field, TableParams } from './';
import { EventDispatcher } from '../shared';
import { type Search, type Filter, type Query, type Sort } from './core';
export default abstract class AbstractTableHandler<Row> {
protected selectBy?: Field<Row>;
protected selectScope: "all" | "currentPage";
protected highlight: boolean;
protected event: EventDispatcher;
protected rawRows: Row[];
protected search: Search<Row>;
protected sort: Sort<Row>;
filters: Filter<Row>[];
queries: Query<Row>[];
rowsPerPage: number;
currentPage: number;
element: HTMLElement;
clientWidth: number;
filterCount: number;
allRows: readonly Row[];
rows: readonly Row[];
rowCount: {
total: number;
start: number;
end: number;
selected: number;
};
pages: readonly number[];
pageCount: number;
pagesWithEllipsis: readonly number[];
selected: unknown[];
isAllSelected: boolean;
constructor(data: Row[], params: TableParams<Row>);
private createAllRows;
private createRows;
private createRowCount;
private createPages;
private createPagesWithEllipsis;
private createIsAllSelected;
}

View File

@@ -0,0 +1,119 @@
import { EventDispatcher } from '../shared';
import { data, parse } from './core';
export default class AbstractTableHandler {
selectBy;
selectScope = $state('currentPage');
highlight;
event = new EventDispatcher();
rawRows = $state.raw([]);
search = $state({ value: null });
sort = $state({});
filters = $state([]);
queries = $state([]);
rowsPerPage = $state(10);
currentPage = $state(1);
element = $state(undefined);
clientWidth = $state(1000);
filterCount = $derived(this.filters.length);
allRows = $derived(this.createAllRows());
rows = $derived(this.createRows());
rowCount = $derived(this.createRowCount());
pages = $derived(this.createPages());
pageCount = $derived(this.pages.length);
pagesWithEllipsis = $derived(this.createPagesWithEllipsis());
selected = $state([]);
isAllSelected = $derived(this.createIsAllSelected());
constructor(data, params) {
this.rawRows = data;
this.rowsPerPage = params.rowsPerPage ?? null;
this.highlight = params.highlight ?? false;
this.selectBy = params.selectBy;
}
createAllRows() {
let allRows = $state.snapshot(this.rawRows);
if (this.search.value) {
allRows = data.search(allRows, this.search, this.highlight);
this.event.dispatch('change');
}
else if (this.filters.length > 0) {
for (const filter of this.filters) {
allRows = data.filter(allRows, filter, this.highlight);
}
this.event.dispatch('change');
}
else if (this.queries.length > 0) {
for (const query of this.queries) {
allRows = data.query(allRows, query);
}
this.event.dispatch('change');
}
return allRows;
}
createRows() {
if (!this.rowsPerPage)
return this.allRows;
return this.allRows.slice((this.currentPage - 1) * this.rowsPerPage, this.currentPage * this.rowsPerPage);
}
createRowCount() {
const total = this.allRows.length;
if (!this.rowsPerPage) {
return { total: total, start: 1, end: total, selected: this.selected.length };
}
return {
total: total,
start: this.currentPage * this.rowsPerPage - this.rowsPerPage + 1,
end: Math.min(this.currentPage * this.rowsPerPage, total),
selected: this.selected.length
};
}
createPages() {
if (!this.rowsPerPage) {
return [1];
}
const pages = Array.from(Array(Math.ceil(this.allRows.length / this.rowsPerPage)));
return pages.map((_, i) => i + 1);
}
createPagesWithEllipsis() {
if (this.pageCount <= 7) {
return this.pages;
}
const ellipse = null;
const firstPage = 1;
const lastPage = this.pageCount;
if (this.currentPage <= 4) {
return [
...this.pages.slice(0, 5),
ellipse,
lastPage
];
}
else if (this.currentPage < lastPage - 3) {
return [
firstPage,
ellipse,
...this.pages.slice(this.currentPage - 2, this.currentPage + 1),
ellipse,
lastPage
];
}
else {
return [
firstPage,
ellipse,
...this.pages.slice(lastPage - 5, lastPage)
];
}
}
createIsAllSelected() {
if (this.rowCount.total === 0 || !this.selectBy) {
return false;
}
const { callback } = parse(this.selectBy);
if (this.selectScope === 'all') {
const identifiers = this.allRows.map(callback);
return identifiers.every(id => this.selected.includes(id));
}
const identifiers = this.rows.map(callback);
return identifiers.every(id => this.selected.includes(id));
}
}

View File

@@ -0,0 +1,49 @@
import AbstractTableHandler from './AbstractTableHandler.svelte';
import type { Internationalization, Row, Field, Check, TableParams, ColumnView, TableHandlerInterface } from './';
import ViewBuilder from '../shared/builders/ViewBuilder.svelte';
import SearchBuilder from './builders/SearchBuilder.svelte';
import FilterBuilder from './builders/FilterBuilder.svelte';
import QueryBuilder from './builders/QueryBuilder.svelte';
import AdvancedFilterBuilder from './builders/AdvancedFilterBuilder.svelte';
import CalculationBuilder from './builders/CalculationBuilder.svelte';
import SortBuilder from './builders/SortBuilder.svelte';
import CSVBuilder from './builders/CSVBuilder.svelte';
import RecordFilterBuilder from './builders/RecordFilterBuilder.svelte';
export default class TableHandler<T extends Row = any> extends AbstractTableHandler<T> implements TableHandlerInterface<T> {
i18n: Internationalization;
private view;
private sortHandler;
private filterHandler;
private queryHandler;
private selectHandler;
private pageHandler;
private searchHandler;
constructor(data?: T[], params?: TableParams<T>);
setRows(data: T[]): void;
setRowsPerPage(value: number): void;
setPage(value: number | 'previous' | 'next' | 'last'): void;
createSearch(scope?: Field<T>[]): SearchBuilder<T>;
clearSearch(): void;
createRecordFilter(records?: Row[]): RecordFilterBuilder;
createSort(field: Field<T>, params?: {
locales: Intl.LocalesArgument;
options: Intl.CollatorOptions;
}): SortBuilder<T>;
clearSort(): void;
clearFilters(): void;
createAdvancedFilter(field: Field<T>, check?: Check): AdvancedFilterBuilder<T>;
createFilter(field: Field<T>, check?: Check): FilterBuilder<T>;
createQuery(): QueryBuilder<T>;
select(value: unknown): void;
selectAll(params?: {
scope?: 'all' | 'currentPage';
}): void;
getSelectedRows(): T[];
clearSelection(): void;
on(event: 'change' | 'clearFilters' | 'clearSearch', callback: () => void): void;
createCalculation(field: Field<T>): CalculationBuilder<T>;
createCSV(): CSVBuilder<T>;
createView(columns: ColumnView[]): ViewBuilder<T>;
getView(): ViewBuilder<T>;
private translate;
}

View File

@@ -0,0 +1,135 @@
import { untrack } from 'svelte';
import AbstractTableHandler from './AbstractTableHandler.svelte';
import SortHandler from './handlers/SortHandler.svelte';
import FilterHandler from './handlers/FilterHandler.svelte';
import QueryHandler from './handlers/QueryHandler.svelte';
import SelectHandler from './handlers/SelectHandler.svelte';
import PageHandler from './handlers/PageHandler.svelte';
import SearchHandler from './handlers/SearchHandler.svelte';
import ViewBuilder from '../shared/builders/ViewBuilder.svelte';
import SearchBuilder from './builders/SearchBuilder.svelte';
import FilterBuilder from './builders/FilterBuilder.svelte';
import QueryBuilder from './builders/QueryBuilder.svelte';
import AdvancedFilterBuilder from './builders/AdvancedFilterBuilder.svelte';
import CalculationBuilder from './builders/CalculationBuilder.svelte';
import SortBuilder from './builders/SortBuilder.svelte';
import CSVBuilder from './builders/CSVBuilder.svelte';
import RecordFilterBuilder from './builders/RecordFilterBuilder.svelte';
export default class TableHandler extends AbstractTableHandler {
i18n;
view;
sortHandler;
filterHandler;
queryHandler;
selectHandler;
pageHandler;
searchHandler;
constructor(data = [], params = { rowsPerPage: null }) {
super(data, params);
this.translate(params.i18n);
this.sortHandler = new SortHandler(this);
this.filterHandler = new FilterHandler(this);
this.queryHandler = new QueryHandler(this);
this.selectHandler = new SelectHandler(this);
this.pageHandler = new PageHandler(this);
this.searchHandler = new SearchHandler(this);
}
setRows(data) {
const scrollTop = this.element?.scrollTop ?? 0;
this.rawRows = data;
untrack(() => {
this.event.dispatch('change');
this.sortHandler.restore();
if (this.element) {
setTimeout(() => this.element.scrollTop = scrollTop, 2);
}
});
}
setRowsPerPage(value) {
this.rowsPerPage = value;
this.setPage(1);
}
setPage(value) {
switch (value) {
case 'previous': return this.pageHandler.previous();
case 'next': return this.pageHandler.next();
case 'last': return this.pageHandler.last();
default: return this.pageHandler.goto(value);
}
}
createSearch(scope) {
return new SearchBuilder(this.searchHandler, scope);
}
clearSearch() {
this.searchHandler.clear();
this.event.dispatch('clearSearch');
this.setPage(1);
}
createRecordFilter(records) {
return new RecordFilterBuilder(records);
}
createSort(field, params) {
return new SortBuilder(this.sortHandler, field, params);
}
clearSort() {
this.sortHandler.clear();
}
clearFilters() {
this.filters = [];
this.event.dispatch('clearFilters');
this.setPage(1);
}
createAdvancedFilter(field, check) {
return new AdvancedFilterBuilder(this.filterHandler, field, check);
}
createFilter(field, check) {
return new FilterBuilder(this.filterHandler, field, check);
}
createQuery() {
return new QueryBuilder(this.queryHandler);
}
select(value) {
this.selectHandler.set(value);
}
selectAll(params = {}) {
this.selectScope = (params.scope === 'all') ? 'all' : 'currentPage';
this.selectHandler.all(this.selectScope);
}
getSelectedRows() {
return this.selectHandler.getRows();
}
clearSelection() {
this.selectHandler.clear();
}
on(event, callback) {
this.event.add(event, callback);
}
createCalculation(field) {
return new CalculationBuilder(this, field);
}
createCSV() {
return new CSVBuilder(this);
}
createView(columns) {
this.view = new ViewBuilder(this, columns);
return this.view;
}
getView() {
return this.view;
}
translate(i18n) {
this.i18n = {
...{
search: 'Search...',
show: 'Show',
entries: 'entries',
filter: 'Filter',
rowCount: 'Showing {start} to {end} of {total} entries',
noRows: 'No entries found',
previous: 'Previous',
next: 'Next'
},
...i18n
};
}
}

View File

@@ -0,0 +1,16 @@
import type { Field, Check } from '..';
import type FilterHandler from '../handlers/FilterHandler.svelte';
export default class AdvancedFilterBuilder<Row> {
criteria: unknown[];
private id;
private filterHandler;
private collection;
private field;
private check;
private isRecursive;
constructor(filterHandler: FilterHandler<Row>, field: Field<Row>, check?: Check);
set(value: unknown, check?: Check): void;
isNotRecursive(): this;
clear(): void;
private cleanup;
}

View File

@@ -0,0 +1,42 @@
import { check as comparator } from '../core';
export default class AdvancedFilterBuilder {
criteria = $state([]);
id = Math.random().toString(36).substring(2, 15);
filterHandler;
collection;
field;
check;
isRecursive = true;
constructor(filterHandler, field, check) {
this.filterHandler = filterHandler;
this.field = field;
this.collection = [];
this.check = check ?? comparator.isEqualTo;
this.cleanup();
}
set(value, check) {
if (this.collection.find(criterion => criterion.value === value)) {
this.collection = this.collection.filter(criterion => criterion.value !== value);
}
else {
this.collection = [{ value, check: check ?? this.check }, ...this.collection];
}
if (this.collection.length === 0) {
return this.clear();
}
this.filterHandler.set(this.collection, this.field, comparator.whereIn, this.id, this.isRecursive);
this.criteria = this.collection.map(criterion => criterion.value);
}
isNotRecursive() {
this.isRecursive = false;
return this;
}
clear() {
this.collection = [];
this.criteria = [];
this.filterHandler.unset(this.id);
}
cleanup() {
this.filterHandler['table'].on('clearFilters', () => this.clear());
}
}

View File

@@ -0,0 +1,9 @@
import type { TableHandler } from '..';
export default class CSVBuilder<Row> {
private table;
constructor(table: TableHandler<Row>);
download(filename: string): void;
get(): string;
private getRows;
private getHeader;
}

View File

@@ -0,0 +1,40 @@
export default class CSVBuilder {
table;
constructor(table) {
this.table = table;
}
download(filename) {
const csv = this.get();
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csv));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
get() {
const rows = this.getRows();
rows.unshift(this.getHeader().join(','));
return rows.join('\r\n');
}
getRows() {
return this.table.allRows.map(row => {
const entries = Object.entries(row).map(([_, value]) => {
if (value === null)
return '';
if (typeof value === 'number') {
return value;
}
return `"${value}"`;
});
return entries.join(',');
});
}
getHeader() {
const [row] = this.table.allRows;
return Object.entries(row).map(([key, _]) => {
return `"${key}"`;
});
}
}

View File

@@ -0,0 +1,26 @@
import type { Field, TableHandler } from '..';
type Sort = [key: 'value' | 'count', direction: 'asc' | 'desc'];
export default class CalcultationBuilder<Row> {
private callback;
private precision;
private table;
constructor(table: TableHandler<Row>, field: Field<Row>);
distinct(param?: {
sort: Sort;
}): {
value: string;
count: number;
}[];
avg(param?: {
precision: number;
}): number;
sum(param?: {
precision: number;
}): number;
median(param?: {
precision: number;
}): number;
bounds(): number[];
private round;
}
export {};

View File

@@ -0,0 +1,69 @@
import { sort, parse } from '../core';
export default class CalcultationBuilder {
callback;
precision;
table;
constructor(table, field) {
this.table = table;
this.callback = parse(field).callback;
}
distinct(param) {
const values = this.table.allRows.map(row => this.callback(row));
const aggregate = values.reduce((acc, curr) => {
if (Array.isArray(curr)) {
for (const item of curr) {
acc[item] = (acc[item] ?? 0) + 1;
}
return acc;
}
acc[curr] = (acc[curr] ?? 0) + 1;
return acc;
}, {});
const result = Object.entries(aggregate).map(([value, count]) => ({ value, count }));
if (param?.sort) {
const [field, direction] = param.sort;
if (field === 'count') {
result.sort((x, y) => sort.asc(x.value, y.value));
}
result.sort((a, b) => sort[direction](a[field], b[field]));
}
return result;
}
avg(param) {
this.precision = param?.precision ?? 2;
if (this.table.allRows.length === 0)
return 0;
const values = this.table.allRows.map(row => this.callback(row)).filter(Boolean);
return this.round(values.reduce((acc, curr) => acc + curr, 0) / values.length);
}
sum(param) {
this.precision = param?.precision ?? 2;
const values = this.table.allRows.map(row => this.callback(row));
return this.round(values.reduce((acc, curr) => acc + curr, 0));
}
median(param) {
this.precision = param?.precision ?? 2;
const values = [...this.table.allRows.map(row => this.callback(row))]
.sort((a, b) => a - b)
.filter(Boolean);
if (values.length === 0)
return null;
const half = Math.floor(values.length / 2);
return (values.length % 2
? values[half]
: this.round((values[half - 1] + values[half]) / 2));
}
bounds() {
const values = this.table.allRows.map(row => this.callback(row));
const numbers = values.filter(Boolean);
if (numbers.length === 0)
return [null, null];
return [
Math.min(...numbers),
Math.max(...numbers)
];
}
round(value) {
return Number(value.toFixed(this.precision));
}
}

View File

@@ -0,0 +1,17 @@
import type { Field, Check } from '..';
import type FilterHandler from '../handlers/FilterHandler.svelte';
import type { FilterInterface } from '../../shared';
export default class FilterBuilder<Row> implements FilterInterface {
value: unknown;
private id;
private filterHandler;
private field;
private check;
private isRecursive;
constructor(filterHandler: FilterHandler<Row>, field: Field<Row>, check?: Check);
set(check?: Check): void;
init(value?: unknown): this;
isNotRecursive(): this;
clear(): void;
private cleanup;
}

View File

@@ -0,0 +1,34 @@
import { check as comparator } from '../core';
export default class FilterBuilder {
value = $state('');
id = Math.random().toString(36).substring(2, 15);
filterHandler;
field;
check;
isRecursive = true;
constructor(filterHandler, field, check) {
this.filterHandler = filterHandler;
this.field = field;
this.check = check ?? comparator.isLike;
this.cleanup();
}
set(check) {
this.filterHandler.set(this.value, this.field, check ?? this.check, this.id, this.isRecursive);
}
init(value) {
this.value = value;
this.set();
return this;
}
isNotRecursive() {
this.isRecursive = false;
return this;
}
clear() {
this.value = '';
this.filterHandler.unset(this.id);
}
cleanup() {
this.filterHandler['table'].on('clearFilters', () => this.clear());
}
}

View File

@@ -0,0 +1,14 @@
import type QueryHandler from '../handlers/QueryHandler.svelte';
export default class QueryBuilder<Row> {
value: unknown;
private id;
private queryHandler;
private path;
private check;
constructor(queryHandler: QueryHandler<Row>);
from(path: string[]): this;
where(filter: (row: any, value?: unknown) => boolean): this;
set(value?: unknown): void;
clear(): void;
private cleanup;
}

Some files were not shown because too many files have changed in this diff Show More