svelte-tably 1.0.0-next.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
2
+ props(): any;
3
+ events(): {};
4
+ slots(): {};
5
+ bindings(): "data" | "selected" | "panel";
6
+ exports(): {
7
+ toCSV: (opts?: {
8
+ /** Semi-colons as separator? */
9
+ semicolon?: boolean;
10
+ /** Only selected rows */
11
+ selected?: boolean;
12
+ }) => Promise<string>;
13
+ };
14
+ }
15
+ interface $$IsomorphicComponent {
16
+ new <T extends Record<PropertyKey, unknown>>(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']>> & {
17
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
18
+ } & ReturnType<__sveltets_Render<T>['exports']>;
19
+ <T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
20
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
21
+ }
22
+ /**
23
+ * This is a description, \
24
+ * on how to use this.
25
+ *
26
+ * @example
27
+ * <Component />
28
+ */
29
+ declare const Table: $$IsomorphicComponent;
30
+ type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
31
+ export default Table;
@@ -0,0 +1,14 @@
1
+ import type { TableProps, TableState } from './table.svelte.js';
2
+ export declare class Data<T extends Record<PropertyKey, unknown>> {
3
+ #private;
4
+ origin: T[];
5
+ sorted: T[];
6
+ filtered: T[];
7
+ sortby: string | undefined;
8
+ sortReverse: boolean;
9
+ current: T[];
10
+ sortBy(column: string): void;
11
+ sortAction(node: HTMLElement, column: string): void;
12
+ sortTable(): void;
13
+ constructor(table: TableState<T>, props: TableProps<T>);
14
+ }
@@ -0,0 +1,81 @@
1
+ import { on } from 'svelte/events';
2
+ import { untrack } from 'svelte';
3
+ export class Data {
4
+ #table = $state();
5
+ origin = $state([]);
6
+ sorted = $state([]);
7
+ filtered = $state([]);
8
+ sortby = $state();
9
+ sortReverse = $state(false);
10
+ current = $derived(this.#table?.options.reorderable ? this.origin : this.filtered);
11
+ sortBy(column) {
12
+ if (this.#table.options.reorderable)
13
+ return;
14
+ const { sort, value } = this.#table.columns[column].options;
15
+ if (!sort || !value)
16
+ return;
17
+ if (this.sortby === column) {
18
+ this.sortReverse = !this.sortReverse;
19
+ }
20
+ else {
21
+ this.sortReverse = false;
22
+ this.sortby = column;
23
+ }
24
+ }
25
+ sortAction(node, column) {
26
+ $effect(() => on(node, 'click', () => {
27
+ this.sortBy(column);
28
+ }));
29
+ }
30
+ sortTable() {
31
+ if (!this.sortby || this.#table.options.reorderable) {
32
+ this.sorted = [...this.origin];
33
+ return;
34
+ }
35
+ const column = this.#table.columns[this.sortby];
36
+ let { sort, value } = column?.options ?? {};
37
+ if (!sort || !value) {
38
+ this.sorted = [...this.origin];
39
+ return;
40
+ }
41
+ if (sort === true) {
42
+ sort = (a, b) => String(a).localeCompare(String(b));
43
+ }
44
+ if (this.sortReverse) {
45
+ this.sorted = this.origin.toSorted((a, b) => sort(value(b), value(a)));
46
+ }
47
+ else {
48
+ this.sorted = this.origin.toSorted((a, b) => sort(value(a), value(b)));
49
+ }
50
+ }
51
+ constructor(table, props) {
52
+ this.#table = table;
53
+ this.origin = props.data;
54
+ this.sorted = this.origin.toSorted();
55
+ this.filtered = this.sorted;
56
+ $effect(() => {
57
+ this.origin = props.data;
58
+ if (this.#table.options.reorderable)
59
+ return;
60
+ props.data;
61
+ props.data.length;
62
+ this.sortby;
63
+ this.sortReverse;
64
+ untrack(() => this.sortTable());
65
+ });
66
+ $effect(() => {
67
+ const table = this.#table;
68
+ if (props.reorderable)
69
+ return;
70
+ const filters = [...props.filters ?? []];
71
+ for (const key in table.columns) {
72
+ const filter = table.columns[key].options.filter;
73
+ const valueOf = table.columns[key].options.value;
74
+ if (filter && valueOf) {
75
+ filters.push((item) => filter(valueOf(item)));
76
+ }
77
+ }
78
+ this.filtered = filters.length === 0 ? this.sorted : this.sorted.filter((value) => filters.every((filter) => filter(value)));
79
+ });
80
+ }
81
+ }
@@ -0,0 +1,76 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { ColumnState } from '../column/column.svelte.js';
3
+ import { PanelState } from '../panel/panel.svelte.js';
4
+ import { Data } from './data.svelte.js';
5
+ export class TableState {
6
+ #props = {};
7
+ id = $state();
8
+ dataState = $state({});
9
+ data = $derived(this.dataState.current ?? []);
10
+ columns = $state({});
11
+ panels = $state({});
12
+ expandable = $state();
13
+ row = $state();
14
+ /** Currently selected items */
15
+ get selected() { return this.#props.selected ??= []; }
16
+ set selected(items) { this.#props.selected = items; }
17
+ /** Column positions based on column ids */
18
+ positions = $state({
19
+ fixed: [],
20
+ sticky: [],
21
+ scroll: [],
22
+ hidden: []
23
+ });
24
+ /** Primarily externally managed options */
25
+ options = $derived({
26
+ panel: this.#props.panel,
27
+ filters: this.#props.reorderable ? false : (this.#props.filters ?? []),
28
+ resizeable: this.#props.resizeable ?? true,
29
+ reorderable: this.#props.reorderable ?? false,
30
+ select: this.#props.select ?? false,
31
+ auto: this.#props.auto ?? false
32
+ });
33
+ add(state) {
34
+ if (state instanceof ColumnState) {
35
+ const key = state.id;
36
+ this.columns[key] = state;
37
+ const clean = () => {
38
+ delete this.columns[key];
39
+ this.positions.fixed = this.positions.fixed.filter((column) => column !== state);
40
+ this.positions.sticky = this.positions.sticky.filter((column) => column !== state);
41
+ this.positions.scroll = this.positions.scroll.filter((column) => column !== state);
42
+ this.positions.hidden = this.positions.hidden.filter((column) => column !== state);
43
+ };
44
+ if (state.defaults.sortby)
45
+ this.dataState.sortBy(key);
46
+ if (state.options.fixed) {
47
+ this.positions.fixed.push(state);
48
+ return clean;
49
+ }
50
+ if (state.defaults.show === false) {
51
+ this.positions.hidden.push(state);
52
+ }
53
+ if (state.defaults.sticky) {
54
+ this.positions.sticky.push(state);
55
+ }
56
+ else {
57
+ this.positions.scroll.push(state);
58
+ }
59
+ return clean;
60
+ }
61
+ if (state instanceof PanelState) {
62
+ const key = state.id;
63
+ this.panels[key] = state;
64
+ return () => delete this.panels[key];
65
+ }
66
+ }
67
+ static getContext() {
68
+ return getContext('svelte-tably');
69
+ }
70
+ constructor(tableProps) {
71
+ this.#props = tableProps;
72
+ this.id = tableProps.id ?? Array.from({ length: 12 }, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join('');
73
+ this.dataState = new Data(this, tableProps);
74
+ setContext('svelte-tably', this);
75
+ }
76
+ }
@@ -0,0 +1,14 @@
1
+ import type { TableState } from './table.svelte.js';
2
+ export declare class Virtualization<T extends Record<PropertyKey, unknown>> {
3
+ #private;
4
+ scrollTop: number;
5
+ viewport: {
6
+ height: number;
7
+ element: HTMLDivElement | null;
8
+ };
9
+ get topIndex(): number;
10
+ get virtualTop(): number;
11
+ get virtualBottom(): number;
12
+ get area(): T[];
13
+ constructor(table: TableState<T>);
14
+ }
@@ -0,0 +1,86 @@
1
+ import { tick, untrack } from 'svelte';
2
+ export class Virtualization {
3
+ scrollTop = $state(0);
4
+ viewport = $state({
5
+ height: 0,
6
+ element: null
7
+ });
8
+ get topIndex() { return this.#topIndex; }
9
+ get virtualTop() { return this.#virtualTop; }
10
+ get virtualBottom() { return this.#virtualBottom; }
11
+ get area() { return this.#area; }
12
+ #topIndex = 0;
13
+ #heightPerItem = $state(8);
14
+ #virtualTop = $state(0);
15
+ #virtualBottom = $state(0);
16
+ #area = $state([]);
17
+ #spacing = $derived(this.viewport.height / 2);
18
+ #renderItemLength = $derived(Math.ceil(Math.max(30, (this.viewport.height / this.#heightPerItem) * 2)));
19
+ constructor(table) {
20
+ let ticked = $state(false);
21
+ $effect.pre(() => {
22
+ table.dataState.origin;
23
+ untrack(() => {
24
+ ticked = false;
25
+ requestAnimationFrame(() => ticked = true);
26
+ });
27
+ });
28
+ $effect(() => {
29
+ if (!ticked)
30
+ return;
31
+ table.dataState.current;
32
+ untrack(() => {
33
+ if (!this.viewport.element) {
34
+ this.#heightPerItem = 8;
35
+ return;
36
+ }
37
+ tick().then(() => {
38
+ const target = this.viewport.element;
39
+ if (target.children.length === 0)
40
+ return;
41
+ const firstRow = target.children[0]?.getBoundingClientRect().top;
42
+ const lastRow = target.children[target.children.length - 1].getBoundingClientRect().bottom;
43
+ this.#heightPerItem = (lastRow - firstRow) / this.#area.length;
44
+ });
45
+ });
46
+ });
47
+ let waitAnimationFrame = false;
48
+ $effect(() => {
49
+ if (!ticked)
50
+ return;
51
+ this.scrollTop;
52
+ this.#heightPerItem;
53
+ table.dataState.current.length;
54
+ table.dataState.current;
55
+ untrack(() => {
56
+ if (!waitAnimationFrame) {
57
+ setTimeout(() => {
58
+ waitAnimationFrame = false;
59
+ let virtualTop = Math.max(this.scrollTop - this.#spacing, 0);
60
+ virtualTop -= virtualTop % this.#heightPerItem;
61
+ this.#virtualTop = virtualTop;
62
+ let virtualBottom = this.#heightPerItem * table.dataState.current.length - virtualTop - this.#spacing * 4;
63
+ virtualBottom = Math.max(virtualBottom, 0);
64
+ this.#virtualBottom = virtualBottom;
65
+ }, 1000 / 60);
66
+ }
67
+ waitAnimationFrame = true;
68
+ });
69
+ });
70
+ $effect(() => {
71
+ if (!ticked)
72
+ return;
73
+ table.dataState.sortReverse;
74
+ table.dataState.sortby;
75
+ this.#heightPerItem;
76
+ this.#virtualTop;
77
+ table.dataState.current.length;
78
+ table.dataState.current;
79
+ untrack(() => {
80
+ this.#topIndex = Math.round(this.#virtualTop / this.#heightPerItem || 0);
81
+ const end = this.#topIndex + this.#renderItemLength;
82
+ this.#area = table.dataState.current.slice(this.#topIndex, end);
83
+ });
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,21 @@
1
+ export type Simplify<T> = T extends infer V ? {
2
+ [K in keyof V]: V[K];
3
+ } : never;
4
+ export type AnyRecord = Record<PropertyKey, any>;
5
+ export declare function pick<T extends Record<PropertyKey, any>, K extends (keyof T)[]>(item: T, keys: K): Simplify<Pick<T, K[number]>>;
6
+ export declare function boundPick<T extends Record<PropertyKey, any>, K extends (keyof T)[]>(item: T, keys: K): {};
7
+ export declare function assign(item: Record<PropertyKey, any>, props?: Partial<Record<PropertyKey, any>>): void;
8
+ export declare function boundAssign(item: Record<PropertyKey, any>, props?: Partial<Record<PropertyKey, any>>): void;
9
+ export declare function mounted(): {
10
+ readonly isMounted: boolean;
11
+ };
12
+ export declare function getters<T extends AnyRecord>(obj: T): { readonly [K in keyof T]: T[K]; };
13
+ type SetterRecord = Record<PropertyKey, [() => any, (v: any) => void]>;
14
+ export declare function withSetters<T extends SetterRecord>(obj: T): T;
15
+ export declare function fromProps<T extends AnyRecord, B extends SetterRecord>(props: T, boundProps?: B): Simplify<{ [K in keyof B]: ReturnType<B[K][0]>; } & { readonly [K in keyof T]: T[K]; }>;
16
+ export declare function assignDescriptors<T extends AnyRecord, B extends AnyRecord>(target: T, source: B): T & B;
17
+ /** Capitalize by space */
18
+ export declare function capitalize(str: string): string;
19
+ /** Split words when going from lower case to uppercase; `someWords-split` -> `some Word Split...` */
20
+ export declare function segmentize(str: string): string;
21
+ export {};
@@ -0,0 +1,104 @@
1
+ import { onMount } from 'svelte';
2
+ export function pick(item, keys) {
3
+ return keys.reduce((acc, key) => ({ ...acc, [key]: item[key] }), {});
4
+ }
5
+ export function boundPick(item, keys) {
6
+ const obj = {};
7
+ for (const key in keys) {
8
+ obj[key] = [() => item[key], (v) => item[key] = v];
9
+ }
10
+ return Object.defineProperties({}, withSetters(obj));
11
+ }
12
+ export function assign(item, props = {}) {
13
+ for (const key in props) {
14
+ const value = props[key];
15
+ if (value === undefined)
16
+ continue;
17
+ if (typeof value === 'object') {
18
+ assign(item[key], value);
19
+ continue;
20
+ }
21
+ item[key] = value;
22
+ }
23
+ }
24
+ export function boundAssign(item, props = {}) {
25
+ for (const key in props) {
26
+ const value = props[key];
27
+ if (value === undefined)
28
+ continue;
29
+ if (typeof value === 'object') {
30
+ boundAssign(item[key], value);
31
+ continue;
32
+ }
33
+ Object.defineProperty(item, value, {
34
+ get() { return props[key]; },
35
+ set(v) { props[key] = v; }
36
+ });
37
+ }
38
+ }
39
+ export function mounted() {
40
+ let isMounted = $state(false);
41
+ onMount(() => { isMounted = true; });
42
+ return {
43
+ get isMounted() { return isMounted; }
44
+ };
45
+ }
46
+ export function getters(obj) {
47
+ let items = {};
48
+ for (const key in obj) {
49
+ items[key] = { get: () => obj[key] };
50
+ }
51
+ return items;
52
+ }
53
+ export function withSetters(obj) {
54
+ let items = {};
55
+ for (const key in obj) {
56
+ items[key] = {
57
+ get: () => obj[key][0](),
58
+ set: (v) => obj[key][1](v)
59
+ };
60
+ }
61
+ return items;
62
+ }
63
+ export function fromProps(props, boundProps) {
64
+ return Object.defineProperties({}, {
65
+ ...getters(props),
66
+ ...withSetters(boundProps ?? {})
67
+ });
68
+ }
69
+ export function assignDescriptors(target, source) {
70
+ for (const key of Object.keys(source)) {
71
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
72
+ if (descriptor) {
73
+ Object.defineProperty(target, key, descriptor);
74
+ }
75
+ else {
76
+ target[key] = source[key]; // Copy regular values if descriptor is missing
77
+ }
78
+ }
79
+ return target;
80
+ }
81
+ /** Capitalize by space */
82
+ export function capitalize(str) {
83
+ let parts = str.split(' ');
84
+ let result = '';
85
+ for (let part of parts) {
86
+ result += part.charAt(0).toUpperCase() + part.slice(1) + ' ';
87
+ }
88
+ return result;
89
+ }
90
+ /** Split words when going from lower case to uppercase; `someWords-split` -> `some Word Split...` */
91
+ export function segmentize(str) {
92
+ let result = '';
93
+ for (let i = 0; i < str.length; i++) {
94
+ const char = str[i];
95
+ const prevChar = i > 0 ? str[i - 1] : '';
96
+ if ((char === '-' || char === char.toUpperCase()) && prevChar !== ' ' && prevChar !== prevChar.toUpperCase()) {
97
+ result += ' ';
98
+ if (char === '-')
99
+ continue;
100
+ }
101
+ result += char;
102
+ }
103
+ return result.trim();
104
+ }
package/package.json CHANGED
@@ -1,54 +1,35 @@
1
1
  {
2
- "name": "svelte-tably",
3
- "version": "1.0.0-next.9",
4
- "repository": "github:refzlund/svelte-tably",
5
- "homepage": "https://github.com/Refzlund/svelte-tably",
6
- "bugs": {
7
- "url": "https://github.com/Refzlund/svelte-tably/issues"
8
- },
9
- "devDependencies": {
10
- "@sveltejs/adapter-auto": "^3.0.0",
11
- "@sveltejs/kit": "^2.9.0",
12
- "@sveltejs/package": "^2.0.0",
13
- "@sveltejs/vite-plugin-svelte": "^5.0.0",
14
- "floating-runes": "^1.0.0",
15
- "publint": "^0.2.0",
16
- "runic-reorder": "^1.0.0-next.1",
17
- "svelte": "^5.0.0",
18
- "svelte-check": "^4.0.0",
19
- "typescript": "^5.0.0",
20
- "vite": "^6.0.0"
21
- },
22
- "peerDependencies": {
23
- "svelte": "^5.0.0"
24
- },
25
- "exports": {
26
- ".": {
27
- "types": "./dist/index.d.ts",
28
- "svelte": "./dist/index.js"
29
- }
30
- },
31
- "files": [
32
- "dist",
33
- "!dist/**/*.test.*",
34
- "!dist/**/*.spec.*"
35
- ],
36
- "scripts": {
37
- "dev": "vite dev",
38
- "build": "vite build && npm run package",
39
- "preview": "vite preview",
40
- "package": "svelte-kit sync && svelte-package && publint",
41
- "prepublishOnly": "npm run package",
42
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
43
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
44
- },
45
- "sideEffects": [
46
- "**/*.css"
47
- ],
48
- "svelte": "./dist/index.js",
49
- "type": "module",
50
- "types": "./dist/index.d.ts",
51
- "dependencies": {
52
- "@faker-js/faker": "^9.3.0"
53
- }
54
- }
2
+ "name": "svelte-tably",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/Refzlund/svelte-tably.git"
7
+ },
8
+ "license": "MIT",
9
+ "bugs": {
10
+ "url": "https://github.com/Refzlund/svelte-tably/issues"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "!dist/**/*.test.*",
15
+ "!dist/**/*.spec.*"
16
+ ],
17
+ "sideEffects": [
18
+ "**/*.css"
19
+ ],
20
+ "svelte": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "runic-reorder": "^1.0.0"
31
+ },
32
+ "peerDependencies": {
33
+ "svelte": "^5.0.0"
34
+ }
35
+ }
@@ -1,164 +0,0 @@
1
- <!-- @component
2
-
3
- This is a description, \
4
- on how to use this.
5
-
6
- @example
7
- <Component />
8
-
9
- -->
10
-
11
- <script module lang="ts">
12
- export type RowCtx<V> = {
13
- readonly value: V
14
- readonly isHovered: boolean
15
- readonly index: number
16
- selected: boolean
17
- }
18
-
19
- export interface ColumnProps<T, V> {
20
- header?: Snippet<
21
- [
22
- /**
23
- * Is true when displaying in the header,
24
- * so additional content can be shown if desired,
25
- * so the header snippet can be re-used.
26
- */
27
- header?: boolean
28
- ]
29
- >
30
- row: Snippet<[item: T, row: RowCtx<V>]>
31
- statusbar?: Snippet
32
-
33
- id: string
34
-
35
- // options
36
- /**
37
- * Is this column sticky by default?
38
- * @default false
39
- */
40
- sticky?: boolean
41
- /**
42
- * Fixed is like sticky, but in its own category — meant to not be moved/hidden ex. select-boxes
43
- * @default false
44
- */
45
- fixed?: boolean
46
- /**
47
- * Is this column sorted by default?
48
- * @default false
49
- */
50
- sort?: boolean
51
- /**
52
- * Is this column visible by default?
53
- * @default true
54
- */
55
- show?: boolean
56
- /**
57
- * The width of the column in pixels by default
58
- * @default 150
59
- */
60
- width?: number
61
- /**
62
- * The value of the row. Required for sorting/filtering
63
- * @example row => row.name
64
- */
65
- value?: (item: T) => V
66
- /**
67
- * Makes the column sortable. Sorts based of a sorting function.
68
- *
69
- * **Important**   `value`-attribute is required adjacent to this.
70
- *
71
- * If `true` uses the default `.sort()` algorithm.
72
- *
73
- * @default false
74
- */
75
- sorting?: boolean | ((a: V, b: V) => number)
76
- /**
77
- * Is this column resizeable?
78
- * Can not be resized if Table is marked as `resizeable={false}`
79
- * @default true
80
- */
81
- resizeable?: boolean
82
-
83
- /**
84
- * Optional: Provide the table it is a part of
85
- */
86
- table?: TableState
87
- }
88
-
89
- export interface ColumnState<T = unknown, V = unknown, C extends ColumnProps<T, V> = ColumnProps<T, V>> {
90
- id: C['id']
91
- header?: C['header']
92
- row: C['row']
93
- statusbar?: C['statusbar']
94
-
95
- fixed?: boolean
96
-
97
- /** Default options for initial table */
98
- defaults: {
99
- sticky?: boolean
100
- sort?: boolean
101
- show?: boolean
102
- width?: number
103
- }
104
- /** More options */
105
- options: {
106
- value?: C['value']
107
- sorting?: boolean | ((a: any, b: any) => number)
108
- resizeable: boolean
109
- }
110
- }
111
- </script>
112
-
113
- <script lang="ts">
114
- import { onDestroy, type Snippet } from 'svelte'
115
- import { getTableState, type TableState } from './Table.svelte'
116
-
117
- type T = $$Generic<Record<PropertyKey, any>>
118
- type V = $$Generic
119
-
120
- let {
121
- header,
122
- row,
123
- statusbar,
124
- id,
125
-
126
- sticky = false,
127
- fixed = false,
128
- sort = false,
129
- show = true,
130
- width,
131
-
132
- resizeable = true,
133
- value,
134
- sorting,
135
-
136
- table
137
- }: ColumnProps<T, V> = $props()
138
-
139
- const column: ColumnState<T, V> = $state({
140
- id,
141
- header,
142
- row,
143
- statusbar,
144
- fixed,
145
- defaults: {
146
- sticky,
147
- sort,
148
- show,
149
- width
150
- },
151
- options: {
152
- value,
153
- sorting,
154
- resizeable
155
- }
156
- })
157
-
158
- table ??= getTableState()
159
- table.addColumn(id, column as ColumnState)
160
-
161
- onDestroy(() => {
162
- table.removeColumn(id)
163
- })
164
- </script>