Repository: vincjo/datatables Branch: main Commit: 755b8179c99d Files: 640 Total size: 3.9 MB Directory structure: gitextract_z5jwp21r/ ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── datatables.code-workspace ├── ecosystem.config.cjs ├── mdsvex.config.js ├── package.json ├── src/ │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib/ │ │ ├── index.ts │ │ ├── legacy/ │ │ │ ├── index.ts │ │ │ ├── local/ │ │ │ │ ├── Comparator.ts │ │ │ │ ├── Context.ts │ │ │ │ ├── DataHandler.ts │ │ │ │ ├── Datatable.svelte │ │ │ │ ├── Pagination.svelte │ │ │ │ ├── RowCount.svelte │ │ │ │ ├── RowsPerPage.svelte │ │ │ │ ├── Search.svelte │ │ │ │ ├── Th.svelte │ │ │ │ ├── ThFilter.svelte │ │ │ │ ├── handlers/ │ │ │ │ │ ├── EventHandler.ts │ │ │ │ │ ├── FilterHandler.ts │ │ │ │ │ ├── PageHandler.ts │ │ │ │ │ ├── SearchHandler.ts │ │ │ │ │ ├── SelectHandler.ts │ │ │ │ │ └── SortHandler.ts │ │ │ │ ├── helpers/ │ │ │ │ │ ├── AdvancedFilterHelper.ts │ │ │ │ │ ├── CalculationHelper.ts │ │ │ │ │ └── FilterHelper.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ └── remote/ │ │ │ ├── Context.ts │ │ │ ├── DataHandler.ts │ │ │ ├── Datatable.svelte │ │ │ ├── Pagination.svelte │ │ │ ├── README.md │ │ │ ├── RowCount.svelte │ │ │ ├── RowsPerPage.svelte │ │ │ ├── Search.svelte │ │ │ ├── SelectedCount.svelte │ │ │ ├── Th.svelte │ │ │ ├── ThFilter.svelte │ │ │ ├── handlers/ │ │ │ │ ├── EventHandler.ts │ │ │ │ ├── FilterHandler.ts │ │ │ │ ├── PageHandler.ts │ │ │ │ ├── SearchHandler.ts │ │ │ │ ├── SelectHandler.ts │ │ │ │ ├── SortHandler.ts │ │ │ │ └── TriggerHandler.ts │ │ │ └── index.ts │ │ ├── server/ │ │ │ └── index.ts │ │ ├── src/ │ │ │ ├── client/ │ │ │ │ ├── AbstractTableHandler.svelte.ts │ │ │ │ ├── TableHandler.svelte.ts │ │ │ │ ├── builders/ │ │ │ │ │ ├── AdvancedFilterBuilder.svelte.ts │ │ │ │ │ ├── CSVBuilder.svelte.ts │ │ │ │ │ ├── CalculationBuilder.svelte.ts │ │ │ │ │ ├── FilterBuilder.svelte.ts │ │ │ │ │ ├── QueryBuilder.svelte.ts │ │ │ │ │ ├── RecordFilterBuilder.svelte.ts │ │ │ │ │ ├── SearchBuilder.svelte.ts │ │ │ │ │ └── SortBuilder.svelte.ts │ │ │ │ ├── core/ │ │ │ │ │ ├── check.ts │ │ │ │ │ ├── entry.ts │ │ │ │ │ ├── field.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rows.ts │ │ │ │ │ └── value.ts │ │ │ │ ├── handlers/ │ │ │ │ │ ├── FilterHandler.svelte.ts │ │ │ │ │ ├── PageHandler.svelte.ts │ │ │ │ │ ├── QueryHandler.svelte.ts │ │ │ │ │ ├── SearchHandler.svelte.ts │ │ │ │ │ ├── SelectHandler.svelte.ts │ │ │ │ │ └── SortHandler.svelte.ts │ │ │ │ └── index.ts │ │ │ ├── server/ │ │ │ │ ├── AbstractTableHandler.svelte.ts │ │ │ │ ├── TableHandler.svelte.ts │ │ │ │ ├── builders/ │ │ │ │ │ ├── FilterBuilder.svelte.ts │ │ │ │ │ ├── SearchBuilder.svelte.ts │ │ │ │ │ └── SortBuilder.svelte.ts │ │ │ │ ├── handlers/ │ │ │ │ │ ├── FetchHandler.svelte.ts │ │ │ │ │ ├── FilterHandler.svelte.ts │ │ │ │ │ ├── PageHandler.svelte.ts │ │ │ │ │ ├── SearchHandler.svelte.ts │ │ │ │ │ ├── SelectHandler.svelte.ts │ │ │ │ │ └── SortHandler.svelte.ts │ │ │ │ └── index.ts │ │ │ └── shared/ │ │ │ ├── Datatable.svelte │ │ │ ├── EventDispatcher.ts │ │ │ ├── Pagination.svelte │ │ │ ├── RowCount.svelte │ │ │ ├── RowsPerPage.svelte │ │ │ ├── Search.svelte │ │ │ ├── Th.svelte │ │ │ ├── ThFilter.svelte │ │ │ ├── ThSort.svelte │ │ │ ├── builders/ │ │ │ │ ├── HighlightBuilder.svelte.ts │ │ │ │ └── ViewBuilder.svelte.ts │ │ │ ├── clsx/ │ │ │ │ ├── Datatable.svelte │ │ │ │ ├── Pagination.svelte │ │ │ │ └── ThSort.svelte │ │ │ └── index.ts │ │ └── style.css │ ├── routes/ │ │ ├── +layout.svelte │ │ ├── +page.svelte │ │ ├── Description.svelte │ │ ├── Header.svelte │ │ ├── Header_Github.svelte │ │ ├── Header_MobileNav.svelte │ │ ├── Header_Mode.svelte │ │ ├── Header_Theme.svelte │ │ ├── Header_Version.svelte │ │ ├── about/ │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── Nav.svelte │ │ │ ├── Nav_Mobile.svelte │ │ │ └── [slug]/ │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── api/ │ │ │ └── [mode]/ │ │ │ ├── +layout.server.ts │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── Nav.svelte │ │ │ ├── Nav_Key.svelte │ │ │ ├── Nav_Mobile.svelte │ │ │ ├── [slug]/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Content.svelte │ │ │ │ └── Content_Ext.svelte │ │ │ ├── content.svx │ │ │ ├── gen/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── Board.svelte │ │ │ └── md/ │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.svelte │ │ │ ├── Nav.svelte │ │ │ ├── Nav_Key.svelte │ │ │ ├── [slug]/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ ├── AST.svelte │ │ │ │ ├── Content.svelte │ │ │ │ └── Content_Ext.svelte │ │ │ └── content.svx │ │ ├── components/ │ │ │ ├── +layout.svelte │ │ │ └── +page.svelte │ │ ├── docs/ │ │ │ ├── client/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.server.ts │ │ │ │ ├── add-on/ │ │ │ │ │ ├── csv-export/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── record-filter/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── content.svx │ │ │ │ │ └── data_cars.ts │ │ │ │ ├── calculation/ │ │ │ │ │ ├── avg/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Advanced.svelte │ │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ │ └── code.svx │ │ │ │ │ ├── bounds/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ │ └── code.svx │ │ │ │ │ ├── data_cars.ts │ │ │ │ │ ├── data_grocery.ts │ │ │ │ │ ├── data_parcel.ts │ │ │ │ │ ├── distinct/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ │ └── code.svx │ │ │ │ │ ├── median/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Advanced.svelte │ │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ │ └── code.svx │ │ │ │ │ └── sum/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Advanced.svelte │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ └── code.svx │ │ │ │ ├── filters/ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── check/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Comparators.svelte │ │ │ │ │ │ ├── Comparators_Check.svelte │ │ │ │ │ │ ├── content.svx │ │ │ │ │ │ ├── content2.svx │ │ │ │ │ │ └── data.ts │ │ │ │ │ ├── criteria/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ ├── content.svx │ │ │ │ │ │ └── data.ts │ │ │ │ │ ├── highlight/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── input/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── nested/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Nested.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── getting-started/ │ │ │ │ │ ├── hello-world/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Client.svx │ │ │ │ │ │ └── Main.svelte │ │ │ │ │ ├── i18n/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ └── doc.svx │ │ │ │ │ ├── intro/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── overview/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── pagination/ │ │ │ │ │ ├── navigation/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Navigation.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── pages/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── CurrentPage.svelte │ │ │ │ │ │ ├── Pages.svelte │ │ │ │ │ │ ├── PagesWithEllipsis.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── row-count/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── rows-per-page/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── search/ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── highlight/ │ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Example.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── input/ │ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Example.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── recursive/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ ├── Main_Search.svelte │ │ │ │ │ │ ├── Main_Tree.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── regex/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── scope/ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Example.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── select/ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── all/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── row/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── scope/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── sort/ │ │ │ │ │ ├── button/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Main.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ ├── dates/ │ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ │ ├── Example.svelte │ │ │ │ │ │ └── content.svx │ │ │ │ │ └── nested/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Nested.svelte │ │ │ │ │ └── content.svx │ │ │ │ └── view/ │ │ │ │ ├── freeze/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Main.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── ordering/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ └── visible/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ └── content.svx │ │ │ └── server/ │ │ │ ├── +layout.svelte │ │ │ ├── +page.server.ts │ │ │ ├── data/ │ │ │ │ ├── invalidate/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── is-loading/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── api.ts │ │ │ │ │ └── content.svx │ │ │ │ └── load/ │ │ │ │ ├── +page.svelte │ │ │ │ └── content.svx │ │ │ ├── filters/ │ │ │ │ └── input/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── content.svx │ │ │ ├── getting-started/ │ │ │ │ ├── hello-world/ │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Main.svelte │ │ │ │ │ ├── api.ts │ │ │ │ │ └── content.svx │ │ │ │ ├── i18n/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Main.svelte │ │ │ │ │ └── doc.svx │ │ │ │ ├── intro/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── migration/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ └── overview/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ └── content.svx │ │ │ ├── pagination/ │ │ │ │ ├── navigation/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Navigation.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── pages/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── CurrentPage.svelte │ │ │ │ │ ├── Pages.svelte │ │ │ │ │ ├── PagesWithEllipsis.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── row-count/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ └── rows-per-page/ │ │ │ │ ├── +page.svelte │ │ │ │ └── content.svx │ │ │ ├── search/ │ │ │ │ └── input/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── content.svx │ │ │ ├── select/ │ │ │ │ ├── +layout.server.ts │ │ │ │ ├── Main.svelte │ │ │ │ ├── Main_OLD.svelte │ │ │ │ ├── all/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── content.svx │ │ │ │ ├── api.ts │ │ │ │ └── row/ │ │ │ │ ├── +page.svelte │ │ │ │ └── content.svx │ │ │ ├── sort/ │ │ │ │ └── button/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── content.svx │ │ │ ├── tips/ │ │ │ │ └── sticky-header/ │ │ │ │ ├── +page.svelte │ │ │ │ └── doc.svx │ │ │ └── view/ │ │ │ ├── +layout.server.ts │ │ │ ├── api.ts │ │ │ ├── freeze/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ └── content.svx │ │ │ ├── ordering/ │ │ │ │ ├── +page.svelte │ │ │ │ └── content.svx │ │ │ └── visible/ │ │ │ ├── +page.svelte │ │ │ ├── Main.svelte │ │ │ └── content.svx │ │ ├── examples/ │ │ │ ├── client/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Test.svelte │ │ │ │ ├── column-ordering/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Client.svx │ │ │ │ │ ├── Main.svelte │ │ │ │ │ └── SortButton.svelte │ │ │ │ ├── crud/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Datatable.svelte │ │ │ │ │ ├── Modal_Create.svelte │ │ │ │ │ ├── Modal_Destroy.svelte │ │ │ │ │ ├── Modal_Update.svelte │ │ │ │ │ ├── api.svelte.ts │ │ │ │ │ ├── doc.svx │ │ │ │ │ └── intro.svx │ │ │ │ ├── distinct/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Basic.svelte │ │ │ │ │ ├── RowCount.svelte │ │ │ │ │ ├── Search.svelte │ │ │ │ │ ├── code.svx │ │ │ │ │ └── data.ts │ │ │ │ ├── hello-world/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Client.svx │ │ │ │ │ ├── Main.svelte │ │ │ │ │ └── page.server.ts │ │ │ │ ├── nested-array/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Client.svx │ │ │ │ │ └── Main.svelte │ │ │ │ ├── pokedex/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── MCF_Main.svelte │ │ │ │ │ ├── MCF_PokemonStats.svelte │ │ │ │ │ ├── MCF_Table.svelte │ │ │ │ │ ├── MCF_TableFilter.svelte │ │ │ │ │ └── data.ts │ │ │ │ ├── shadcn/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Table.svelte │ │ │ │ │ ├── Table_Footer.svelte │ │ │ │ │ ├── Table_Footer_Pagination.svelte │ │ │ │ │ ├── Table_Footer_RowSelection.svelte │ │ │ │ │ ├── Table_Footer_RowsPerPage.svelte │ │ │ │ │ ├── Table_Header.svelte │ │ │ │ │ ├── Table_Header_ColumnVisibility.svelte │ │ │ │ │ ├── Table_Header_Filter.svelte │ │ │ │ │ ├── Table_Th.svelte │ │ │ │ │ └── utils.ts │ │ │ │ ├── test/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ ├── Table.svelte │ │ │ │ │ └── data2.ts │ │ │ │ ├── test-calculation/ │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── data.ts │ │ │ │ └── tree/ │ │ │ │ ├── +page.svelte │ │ │ │ ├── Client.svx │ │ │ │ ├── Main.svelte │ │ │ │ ├── Main_Search.svelte │ │ │ │ └── Main_Tree.svelte │ │ │ └── server/ │ │ │ ├── +layout.svelte │ │ │ ├── +page.server.ts │ │ │ ├── Features.svelte │ │ │ ├── beer-api/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── example.server.ts │ │ │ ├── hello-world/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── example.server.ts │ │ │ ├── pokedex-api/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── PokemonStats.svelte │ │ │ │ ├── PokemonTypes.svelte │ │ │ │ ├── api.ts │ │ │ │ └── example.server.ts │ │ │ ├── ssr-user/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── params.svelte.ts │ │ │ ├── todo-api/ │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── Main.svelte │ │ │ │ ├── api.ts │ │ │ │ └── example.server.ts │ │ │ └── user-api/ │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ ├── Main.svelte │ │ │ ├── api.ts │ │ │ └── example.server.ts │ │ └── export/ │ │ └── [mode]/ │ │ ├── gen/ │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── Board.svelte │ │ └── md/ │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ ├── Nav.svelte │ │ ├── Nav_Key.svelte │ │ ├── [slug]/ │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── AST.svelte │ │ │ ├── Content.svelte │ │ │ └── Content_Ext.svelte │ │ └── content.svx │ └── site/ │ ├── Banner.svelte │ ├── Logo.svelte │ ├── Site.svelte.ts │ ├── components/ │ │ ├── Demo.svelte │ │ ├── Demo_Code.svelte │ │ ├── Demo_Code_Icon.svelte │ │ ├── Demo_Code_Nav.svelte │ │ ├── Demo_CopyButton.svelte │ │ ├── Demo_Data.svelte │ │ ├── Highlight.svelte │ │ ├── Install.svelte │ │ ├── Nav_Search.svelte │ │ ├── docs/ │ │ │ ├── Layout.svelte │ │ │ ├── Nav.svelte │ │ │ └── Nav_Mobile.svelte │ │ └── examples/ │ │ ├── Layout.svelte │ │ ├── Nav.svelte │ │ └── Nav_Mobile.svelte │ ├── data/ │ │ ├── cars.ts │ │ ├── data.11000.ts │ │ ├── data.75.ts │ │ ├── data.ts │ │ ├── data_with_null.ts │ │ ├── int-bool-string.ts │ │ ├── pokedex.ts │ │ └── tree.ts │ ├── index.ts │ └── utils/ │ └── viewport.ts ├── static/ │ ├── documents/ │ │ ├── client/ │ │ │ ├── methods.clearFilters.json │ │ │ ├── methods.clearSearch.json │ │ │ ├── methods.clearSelection.json │ │ │ ├── methods.clearSort.json │ │ │ ├── methods.createAdvancedFilter.json │ │ │ ├── methods.createCSV.json │ │ │ ├── methods.createCalculation.json │ │ │ ├── methods.createFilter.json │ │ │ ├── methods.createQuery.json │ │ │ ├── methods.createRecordFilter.json │ │ │ ├── methods.createSearch.json │ │ │ ├── methods.createSort.json │ │ │ ├── methods.createView.json │ │ │ ├── methods.filter.json │ │ │ ├── methods.getSelectedRows.json │ │ │ ├── methods.getView.json │ │ │ ├── methods.on.json │ │ │ ├── methods.select.json │ │ │ ├── methods.selectAll.json │ │ │ ├── methods.setPage.json │ │ │ ├── methods.setRows.json │ │ │ ├── methods.setRowsPerPage.json │ │ │ ├── nav.json │ │ │ ├── properties.allRows.json │ │ │ ├── properties.clientWidth.json │ │ │ ├── properties.currentPage.json │ │ │ ├── properties.element.json │ │ │ ├── properties.filterCount.json │ │ │ ├── properties.filters.json │ │ │ ├── properties.i18n.json │ │ │ ├── properties.isAllSelected.json │ │ │ ├── properties.pageCount.json │ │ │ ├── properties.pages.json │ │ │ ├── properties.pagesWithEllipsis.json │ │ │ ├── properties.queries.json │ │ │ ├── properties.rowCount.json │ │ │ ├── properties.rows.json │ │ │ ├── properties.rowsPerPage.json │ │ │ ├── properties.selected.json │ │ │ ├── types.Check.json │ │ │ ├── types.ColumnView.json │ │ │ ├── types.Criterion.json │ │ │ ├── types.Field.json │ │ │ ├── types.Filter.json │ │ │ ├── types.Internationalization.json │ │ │ ├── types.Row.json │ │ │ ├── types.SearchType.json │ │ │ ├── types.Sort.json │ │ │ ├── types.SortParams.json │ │ │ ├── types.TableHandlerParams.json │ │ │ ├── types.TableParams.json │ │ │ └── types.ViewColumn.json │ │ ├── markdown/ │ │ │ ├── client/ │ │ │ │ ├── methods.clearSort.md │ │ │ │ ├── methods.createAdvancedFilter.md │ │ │ │ ├── methods.createCSV.md │ │ │ │ ├── methods.createCalculation.md │ │ │ │ ├── methods.createRecordFilter.md │ │ │ │ ├── methods.getSelectedRows.md │ │ │ │ ├── methods.setRows.md │ │ │ │ ├── methods.setRowsPerPage.md │ │ │ │ ├── properties.allRows.md │ │ │ │ ├── properties.filters.md │ │ │ │ ├── types.Check.md │ │ │ │ ├── types.Comparator.md │ │ │ │ ├── types.Criterion.md │ │ │ │ ├── types.Filter.md │ │ │ │ ├── types.Internationalization.md │ │ │ │ ├── types.Sort.md │ │ │ │ ├── types.SortParams.md │ │ │ │ └── types.TableParams.md │ │ │ ├── methods.clearFilters.md │ │ │ ├── methods.clearSearch.md │ │ │ ├── methods.clearSelection.md │ │ │ ├── methods.createFilter.md │ │ │ ├── methods.createSearch.md │ │ │ ├── methods.createSort.md │ │ │ ├── methods.createView.md │ │ │ ├── methods.getView.md │ │ │ ├── methods.on.md │ │ │ ├── methods.select.md │ │ │ ├── methods.selectAll.md │ │ │ ├── methods.setPage.md │ │ │ ├── methods.setRowsPerPage.md │ │ │ ├── properties.clientWidth.md │ │ │ ├── properties.currentPage.md │ │ │ ├── properties.element.md │ │ │ ├── properties.filterCount.md │ │ │ ├── properties.i18n.md │ │ │ ├── properties.isAllSelected.md │ │ │ ├── properties.pageCount.md │ │ │ ├── properties.pages.md │ │ │ ├── properties.pagesWithEllipsis.md │ │ │ ├── properties.rowCount.md │ │ │ ├── properties.rows.md │ │ │ ├── properties.rowsPerPage.md │ │ │ ├── properties.selected.md │ │ │ ├── properties.sort.md │ │ │ ├── server/ │ │ │ │ ├── methods.getState.md │ │ │ │ ├── methods.invalidate.md │ │ │ │ ├── methods.load.md │ │ │ │ ├── properties.filters.md │ │ │ │ ├── properties.isLoading.md │ │ │ │ ├── properties.sort.md │ │ │ │ ├── properties.totalRows.md │ │ │ │ ├── types.Filter.md │ │ │ │ ├── types.Sort.md │ │ │ │ └── types.State.md │ │ │ ├── types.ColumnView.md │ │ │ ├── types.Field.md │ │ │ ├── types.Internationalization.md │ │ │ └── types.Row.md │ │ └── server/ │ │ ├── methods.clearFilters.json │ │ ├── methods.clearSearch.json │ │ ├── methods.clearSelection.json │ │ ├── methods.createFilter.json │ │ ├── methods.createSearch.json │ │ ├── methods.createSort.json │ │ ├── methods.createView.json │ │ ├── methods.filter.json │ │ ├── methods.getState.json │ │ ├── methods.getView.json │ │ ├── methods.invalidate.json │ │ ├── methods.load.json │ │ ├── methods.on.json │ │ ├── methods.select.json │ │ ├── methods.selectAll.json │ │ ├── methods.setPage.json │ │ ├── methods.setRowsPerPage.json │ │ ├── methods.setTotalRows.json │ │ ├── nav.json │ │ ├── properties.clientWidth.json │ │ ├── properties.currentPage.json │ │ ├── properties.debounce.json │ │ ├── properties.element.json │ │ ├── properties.events.json │ │ ├── properties.filterCount.json │ │ ├── properties.filters.json │ │ ├── properties.i18n.json │ │ ├── properties.isAllSelected.json │ │ ├── properties.isLoading.json │ │ ├── properties.pageCount.json │ │ ├── properties.pages.json │ │ ├── properties.pagesWithEllipsis.json │ │ ├── properties.rowCount.json │ │ ├── properties.rows.json │ │ ├── properties.rowsPerPage.json │ │ ├── properties.search.json │ │ ├── properties.selectBy.json │ │ ├── properties.selected.json │ │ ├── properties.sort.json │ │ ├── properties.totalRows.json │ │ ├── types.ColumnView.json │ │ ├── types.Field.json │ │ ├── types.Filter.json │ │ ├── types.Internationalization.json │ │ ├── types.Row.json │ │ ├── types.Sort.json │ │ ├── types.State.json │ │ ├── types.TableParams.json │ │ └── types.ViewColumn.json │ ├── fonts/ │ │ ├── Inter/ │ │ │ ├── OFL.txt │ │ │ └── README.txt │ │ └── Roboto/ │ │ └── LICENSE.txt │ ├── global.css │ ├── gros-theme.css │ ├── gros.css │ ├── prism-dark.css │ └── prism-light.css ├── svelte.config.js ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /build /.svelte-kit /.vscode /package /build .env .env.* /dist !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* ================================================ FILE: .npmrc ================================================ engine-strict=true ================================================ FILE: CHANGELOG.md ================================================ # 2.8.0 - 2025-12-17 ### Added [client] Disable filter recursion [#187](https://github.com/vincjo/datatables/issues/187) ```ts const filter = table.createFilter(field).isNotRecursive() const advancedFilter = table.createAdvancedFilter(field).isNotRecursive() ``` # 2.7.0 - 2025-11-25 ### Fixed [server] optimize (reduce) the number of reloads by removing irrelevant triggering events ### Changed Add an optional debounce parameter to the TableHandler. Default = 0 This adds delays before an invalidation is triggered. # 2.6.6 - 2025-10-08 ### Fixed fix: `RecordFilter` search capabilities. # 2.6.5 - 2025-10-08 ### Fixed fix: Server-Side pagination - Changing filters resets to page 1 [#179](https://github.com/vincjo/datatables/issues/179) # 2.6.4 - 2025-09-17 ### Fixed fix: remove console.log in `Calculation.round()` # 2.6.3 - 2025-09-07 ### Fixed fix: multiple criteria filter (AdvancedFilter) handles nullish values # 2.6.2 - 2025-07-16 ### Fixed fix: Broken package with $lib import https://github.com/vincjo/datatables/issues/169 # 2.6.1 - 2025-07-14 ### Fixed fix: make sure `init()` arg can be undefined in any case. ### Changed - upgrade Svelte to the latest 5.36.0 # 2.6.0 - 2025-07-14 ### Added feat: add `init()` method to initialize a value in "search", "filter" and "sort". # 2.5.0 - 2025-02-06 ### Added feat: add the median calculation. ```ts const median = table.createCalculation('field').median() ``` # 2.4.0 - 2025-01-25 ### Added feat: (experimental) add `queries` to handle filtering inside nested array of objects. ```ts table.createQuery('login_count') .from(['groups', 'users']) .where(check.isGreaterThan) .set(1000) // will check if "user.login_count" is greater than 1000 in users in groups ``` ### Fixed - `selectAll`: remove duplicate keys [#157](https://github.com/vincjo/datatables/issues/157) # 2.3.1 - 2025-01-21 ### Changed - refactor: core functions has been organized into modules `/client/core/{value, entry, rows, check, field}` ### Fixed - fix: add "index.js" to make type exports compatible with ES Module # 2.3.0 - 2025-01-15 ### Added feat: add `search.recursive()` to handle search in tree data structures ([DOC](https://vincjo.fr/datatables/docs/client/search/recursive)). ### Changed - breaking: search is not recursive by default anymore [#152](https://github.com/vincjo/datatables/issues/152) - upgrade Svelte to the latest 5.18.0 # 2.2.0 - 2024-12-05 ### Added feat: add `table.clearSort()` method [#150](https://github.com/vincjo/datatables/issues/150) # 2.1.0 - 2024-11-26 ### Fixed - export type AdvancedFilterBuilder [#145](https://github.com/vincjo/datatables/issues/145) - use random string instead of `crypto.randomUUID()` [PR #147](https://github.com/vincjo/datatables/pull/147) # 2.0.5 - 2024-10-24 ### Fixed - fix: `state_unsafe_mutation` error. currentPage is mutated in setters instead: filter.set() / search.set(). [#128](https://github.com/vincjo/datatables/issues/128) [#138](https://github.com/vincjo/datatables/issues/138) # 2.0.4 - 2024-10-22 ### Fixed - fix: (legacy) remove self closing tags # 2.0.3 - 2024-10-22 ### Changed - docs: stay on the same page after switch between client-side and server-side navigation ### Fixed - fix: improve perf in the data filtering function - fix: prefer unknown type instead of enumerating primitives # 2.0.2 - 2024-10-21 ### Fixed - fix: remove runes tag from npm publication # 2.0.1 - 2024-10-21 ### Fixed - fix: `table.select(value: unknown)` select arg type is *unknown* instead of `T[keyof T]` # 2.0.0 - 2024-10-21 - published major release # 2.0.0-runes.46 - 2024-10-21 ### Fixed - fix: `EventDispatcher` # 2.0.0-runes.45 - 2024-10-16 ### Fixed - fix: `clearSearch` maximum call stack exceeded # 2.0.0-runes.44 - 2024-10-14 ### Added - export type SearchBuilder, CSVBuilder, FilterBuilder, CalculationBuilder... - added RecordFilter class ### Changed - feat: handle scrollTop status in setRows method - fix: replace structureCloned with $state.snapshot() # 2.0.0-runes.{40,41,42,43} - 2024-10-06 ### Fixed - back to `:global()` style: css import is not working as expected after packaging. # 2.0.0-runes.39 - 2024-10-06 ### Added - added `TableHandlerInterface` to improve shared components type. ### Changed - update dependencies: svelte-next.262 - breaking: `table.createCalcultaion().distinct()` parameter is now: `distinct({ sort: [field, direction] })` ### Fixed - remove dupplicate builder: `ColumnViewBuilders` - remove dupplicate type definition: `Row`, `ColumnView`, `Internationalization`, `Field` - use generic `T` type in `AbstractTableHandler` instead of `Row` (server-side pagination). - distinct values has a default order in addition to sort param - fix `$$Generic` type for server-side pagination - fix `isAllSelected` when `selectScope` = 'all' # 2.0.0-runes.38 - 2024-10-04 ### Fixed - CSS import in `` component. # 2.0.0-runes.37 - 2024-09-29 ### Added - feat: make selectBy parameter type of `Field` so it can combine multiple fields or use a nested property as identifier - docs: added a [migration guide](https://github.com/vincjo/datatables/blob/runes/MIGRATION.md) ### Changed - update dependencies: svelte-next.260 ### Fixed - added type="button" in `` and `` components, including the legacy part. # 2.0.0-runes.{33,34,35,36} - 2024-09-16 ### Added - feat: add headless option for Datatable component - feat: use @import instead of :global() in Datatable component ### Changed - update dependencies: svelte-next.257 # 2.0.0-runes.32 - 2024-09-16 ### Changed - update dependencies: svelte-next.246 # 2.0.0-runes.31 - 2024-07-25 ### Changed - rename type `ViewColumn` to `ColumnView` ### Fixed - improve types - fix criterion value type # 2.0.0-runes.30 - 2024-07-01 ### Fixed - client-side: refactor / improve types # 2.0.0-runes.29 - 2024-06-30 ### Fixed - client-side: untrack sort and event dispatcher when rows are updated. # 2.0.0-runes.28 - 2024-06-30 ### Changed - client-side *(breaking)*: sort params for distinct values is now an object `{ field: 'value' | 'count', direction: 'asc' | 'desc' }` instead of an array - client-side: use crypto UUID instead of js random string - client-side: improve types # 2.0.0-runes.27 - 2024-06-29 ### Added - client-side: regular expression search takes scope parameter into account # 2.0.0-runes.26 - 2024-06-27 ### Added - server-side: added a `table.isLoading` state which is true while `invalidate()` method runs # 2.0.0-runes.25 - 2024-06-26 ### Fixed - server-side: column filtering ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Contributions are welcome. 1. Fork the project 2. ⚠️ Create your feature branch `git checkout -b myfeature` 3. Commit your changes `git commit -m 'feat: my feature'` 4. Push to the branch `git push origin myfeature` 5. Open a Pull Request ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================
logo

svelte simple datatables

A powerful toolkit for building datatable components.

any text last commit

# 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 ```
# Sample code ```svelte {#each table.rows as row} {/each}
First name Last name
{row.first_name} {row.last_name}
``` ================================================ FILE: datatables.code-workspace ================================================ { "folders": [ { "path": "./" }, { "path": "../autodoc"} ], "settings": { "restoreTerminals.terminals": [ { "profile": "Git Bash", "splitTerminals": [{ "name": "API", "commands": ["cd ../autodoc && npm run dev"], }], }, { "profile": "Git Bash", "splitTerminals": [{ "name": "DEV", "commands": ["npm run dev"], }], }, { "profile": "Git Bash", "splitTerminals": [{ "name": "Git bash", "commands": ["cd ./"], }], } ], "terminal.integrated.persistentSessionReviveProcess": "never" } } ================================================ FILE: ecosystem.config.cjs ================================================ module.exports = { apps: [ { name: 'datatables', script: './build/index.js', watch: false, max_restarts: 10, env: { NODE_ENV: 'production', PORT: 3010 } } ], deploy: { production: { user: 'vincjo', host: ['vincjo.fr -p 625'], ref: 'origin/main', repo: 'git@github.com:vincjo/datatables.git', path: '/home/vincjo/www/datatables', 'post-deploy': 'npm install --force && npm run build && pm2 startOrRestart ecosystem.config.cjs --env production' } } } ================================================ FILE: mdsvex.config.js ================================================ import { defineMDSveXConfig as defineConfig } from 'mdsvex' const config = defineConfig({ extensions: ['.svelte.md', '.md', '.svx'], smartypants: { dashes: 'oldschool' }, remarkPlugins: [], rehypePlugins: [] }) export default config ================================================ FILE: package.json ================================================ { "name": "@vincjo/datatables", "version": "2.8.0", "keywords": [ "svelte sveltejs table tables datatable datatables filter headless sort selection lazy-loading" ], "description": "A powerful toolkit for building datatable components", "repository": { "type": "git", "url": "git+https://github.com/vincjo/datatables.git" }, "author": "vincjo", "contributors": [ "bn-l", "jst-r" ], "license": "MIT", "bugs": { "url": "https://github.com/vincjo/datatables/issues" }, "homepage": "https://vincjo.fr/datatables", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "package": "svelte-kit sync && svelte-package", "publish:runes": "svelte-kit sync && svelte-package && npm publish", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --plugin-search-dir . --write .", "deploy": "pm2 deploy ecosystem.config.cjs production", "deploy:setup": "pm2 deploy ecosystem.config.cjs production setup" }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", "@sveltejs/adapter-node": "^5.2.13", "@sveltejs/kit": "^2.23.0", "@sveltejs/package": "=2.3.7", "@sveltejs/vite-plugin-svelte": "^6.0.0", "@types/node": "^24.0.13", "dotenv": "^17.2.0", "gros": "^1.1.3", "mdsvex": "^0.12.6", "prism-svelte": "^0.5.0", "prismjs": "^1.30.0", "svelte": "5.36.0", "svelte-check": "^4.2.2", "svelte-dnd-action": "^0.9.63", "svelte-preprocess": "^6.0.3", "tslib": "^2.8.1", "typescript": "^5.8.3", "vite": "^7.0.4" }, "peerDependencies": { "svelte": "^5.16.0" }, "type": "module", "files": [ "dist" ], "svelte": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "css": "./dist/*.css", "svelte": "./dist/index.js", "default": "./dist/index.js" }, "./server": { "types": "./dist/server/index.d.ts", "svelte": "./dist/server/index.js", "default": "./dist/server/index.js" }, "./legacy": { "types": "./dist/legacy/index.d.ts", "svelte": "./dist/legacy/index.js", "default": "./dist/legacy/index.js" }, "./legacy/remote": { "types": "./dist/legacy/remote/index.d.ts", "svelte": "./dist/legacy/remote/index.js", "default": "./dist/legacy/remote/index.js" } } } ================================================ FILE: src/app.d.ts ================================================ /// // See https://kit.svelte.dev/docs/types#app // for information about these interfaces // and what to do when importing types declare namespace App { // interface Locals {} // interface PageData {} // interface Error {} // interface Platform {} } ================================================ FILE: src/app.html ================================================ svelte simple datatables %sveltekit.head%
%sveltekit.body%
================================================ FILE: src/hooks.server.ts ================================================ import type { RequestEvent } from '@sveltejs/kit' export const handle = async ({ event, resolve }) => { const theme = event.cookies.get('siteTheme') ?? 'light' const mode = getMode(event) return resolve(event, { transformPageChunk: ({ html }) => { return html.replace(`data-theme=""`, `data-theme="${theme}"`) .replace(`data-mode=""`, `data-mode="${mode}"`) } }) } const getMode = (event: RequestEvent) => { if (event.url.pathname.includes('/server')) { return 'server' } else if (event.url.pathname.includes('/client')) { return 'client' } return event.cookies.get('siteMode') ?? 'client' } ================================================ FILE: src/lib/index.ts ================================================ export { // class: TableHandler, RecordFilter, // components: Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, // utils: check, // types: 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 '$lib/src/client/index.js' ================================================ FILE: src/lib/legacy/index.ts ================================================ // 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' 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, // deprecated Selectable, Order, OrderBy, FilterBy, } ================================================ FILE: src/lib/legacy/local/Comparator.ts ================================================ import type { Criterion } from '$lib/legacy/local' export const check = { isLike: (entry: any, value: any) => { return stringify(entry).indexOf(stringify(value)) > -1 }, isNotLike: (entry: any, value: any) => { return stringify(entry).indexOf(stringify(value)) === -1 }, startsWith: (entry: any, value: any) => { return stringify(entry).startsWith(stringify(value)) }, endsWith: (entry: any, value: any) => { return stringify(entry).endsWith(stringify(value)) }, isEqualTo: (entry: any, value: any) => { return stringify(entry) === stringify(value) }, isNotEqualTo: (entry: any, value: any) => { return stringify(entry) !== stringify(value) }, isGreaterThan: (entry: number, value: number) => { if (isNull(entry)) return false return entry > value }, isGreaterThanOrEqualTo: (entry: number, value: number) => { if (isNull(entry)) return false return entry >= value }, isLessThan: (entry: number, value: number) => { if (isNull(entry)) return false return entry < value }, isLessThanOrEqualTo: (entry: number, value: number) => { if (isNull(entry)) return false return entry <= value }, isBetween: (entry: number, value: [min: number, max: number]) => { if (isNull(entry)) return false const [min, max] = value return entry >= min && entry <= max }, isStrictlyBetween: (entry: number, value: [min: number, max: number]) => { if (isNull(entry)) return false const [min, max] = value return entry > min && entry < max }, isTrue: (entry: boolean, _: any) => { return entry === true }, isFalse: (entry: boolean, _: any) => { return entry === false }, isNull: (entry: null, _: any) => { return entry === null || entry === undefined }, isNotNull: (entry: any, _: any) => { return entry === null || entry === undefined ? false : true }, whereIn: (entry: any, values: Criterion[] = []) => { 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: any, value: any) => { return check.isLike(entry, value) }, } /* utils */ function stringify(value: string | number | boolean = null) { return String(value) .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') } function isNull(entry: any) { if (entry === null || entry === undefined) return true } ================================================ FILE: src/lib/legacy/local/Context.ts ================================================ import { writable, derived, type Writable, type Readable } from 'svelte/store' import type { Filter, Sort, Comparator, Criterion, Field } from '$lib/legacy/local' import type { Params } from '$lib/legacy/local/DataHandler' import { isNull, parseField } from './utils' import { check } from './Comparator' import EventHandler from './handlers/EventHandler' export default class Context { public event : EventHandler public rowsPerPage : Writable public pageNumber : Writable public search : Writable<{ value?: string, scope?: (keyof Row)[] }> public filters : Writable[]> public filterCount : Readable public rawRows : Writable public filteredRows : Readable public pagedRows : Readable public rowCount : Readable<{ total: number, start: number, end: number }> public pages : Readable public pagesWithEllipsis : Readable public pageCount : Readable public sort : Writable<(Sort)> public selected : Writable<(Row | Row[keyof Row])[]> public selectScope : Writable<'all' | 'currentPage'> public isAllSelected : Readable constructor(data: Row[], params: 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() } private createFilterCount() { return derived(this.filters, ($filters) => $filters.length) } private 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) as Field[] const scope = fields.map((field: 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 } ) } private match(entry: Row[keyof Row], value: string|number|boolean|symbol|number[]|Criterion[], compare: Comparator = 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) } private createPagedRows() { return derived( [this.filteredRows, this.rowsPerPage, this.pageNumber], ([$filteredRows, $rowsPerPage, $pageNumber]) => { if (!$rowsPerPage) { return $filteredRows } return $filteredRows.slice( ($pageNumber - 1) * $rowsPerPage, $pageNumber * $rowsPerPage ) } ) } private 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) } } ) } private 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 ) }) } private 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) ] } }) } private createPageCount() { return derived(this.pages, ($pages) => { return $pages.length }) } private 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 } ) } } ================================================ FILE: src/lib/legacy/local/DataHandler.ts ================================================ 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' import type { Readable, Writable } from 'svelte/store' import type { Internationalization, Row, Field, Comparator } from '$lib/legacy/local' export type Params = { rowsPerPage?: number, i18n?: Internationalization } export default class DataHandler { private context : Context private sortHandler : SortHandler private selectHandler : SelectHandler private pageHandler : PageHandler private searchHandler : SearchHandler private filterHandler : FilterHandler public i18n : Internationalization constructor(data: T[] = [], params: 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) } public setRows(data: T[]) { this.context.rawRows.set(data) this.context.event.trigger('change') this.applySort() } public getRows(): Readable { return this.context.pagedRows } public getAllRows(): Readable { return this.context.filteredRows } public getRowCount(): Readable<{ total: number, start: number, end: number }> { return this.context.rowCount } public getRowsPerPage(): Writable { return this.context.rowsPerPage } public getPages(param = { ellipsis: false }): Readable { if (param.ellipsis) { return this.context.pagesWithEllipsis } return this.context.pages } public getPageCount(): Readable { return this.context.pageCount } public getPageNumber(): Readable { return this.context.pageNumber } public setPage(value: number | 'previous' | 'next'): void { switch (value) { case 'previous' : return this.pageHandler.previous() case 'next' : return this.pageHandler.next() default : return this.pageHandler.goto(value as number) } } public search(value: string, scope: Field[] = null) { this.searchHandler.set(value, scope) } public clearSearch() { this.searchHandler.clear() } public sort(orderBy: Field, identifier?: string) { this.setPage(1) this.sortHandler.set(orderBy, identifier) } public sortAsc(orderBy: Field, identifier?: string) { this.setPage(1) this.sortHandler.asc(orderBy, identifier) } public sortDesc(orderBy: Field, identifier?: string) { this.setPage(1) this.sortHandler.desc(orderBy, identifier) } public getSort(): Writable<{ identifier?: string, direction?: 'asc' | 'desc' }> { return this.context.sort } public applySort( params: { orderBy: Field, direction?: 'asc' | 'desc' } = null ) { this.sortHandler.apply(params) } public defineSort(orderBy: Field, direction?: 'asc' | 'desc') { this.sortHandler.define(orderBy, direction) } public clearSort() { this.sortHandler.clear() } public filter( value: string | number | null | undefined | boolean | number[], filterBy: Field, comparator: Comparator = null ) { this.filterHandler.set(value, filterBy, comparator) } public getFilters() { return this.filterHandler.get() } public createFilter( filterBy: Field, comparator?: Comparator ) { return new FilterHelper( this.filterHandler, filterBy, comparator ) } public createAdvancedFilter(filterBy: Field) { return new AdvancedFilterHelper(this.filterHandler, filterBy) } public getFilterCount(): Readable { return this.context.filterCount } public clearFilters(): void { this.filterHandler.clear() } public select(value: T | T[keyof T]) { this.selectHandler.set(value) } public getSelected() { return this.context.selected } public selectAll(params: { selectBy?: keyof T; scope?: 'all' | 'currentPage' } = {}): void { this.context.selectScope.set(params.scope === 'currentPage' ? 'currentPage' : 'all') this.selectHandler.all(params.selectBy ?? null) } public isAllSelected(): Readable { return this.context.isAllSelected } public on(event: 'change' | 'clearFilters' | 'clearSearch', callback: () => void) { this.context.event.add(event, callback) } public createCalculation(field: Field, param: { precision: number } = null) { return new CalculationHelper(this.context, field, { precision: param?.precision ?? 2 }) } public translate(i18n: Internationalization): Internationalization { 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 */ public update(data: any[]): void { 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 */ public applySorting( params: { orderBy: Field, direction?: 'asc' | 'desc' } = null ) { this.applySort(params) } /** * @deprecated use getSort() instead * @since v1.11.0 2023-07-11 */ public getSorted() { return this.getSort() } public getTriggerChange(): Writable { return this.context.event.triggerChange } } ================================================ FILE: src/lib/legacy/local/Datatable.svelte ================================================
{#if search} {/if} {#if rowsPerPage} {/if}
{#if rowCount} {/if} {#if pagination} {/if}
================================================ FILE: src/lib/legacy/local/Pagination.svelte ================================================
{#if small} {:else} {#each $pages as page} {/each} {/if}
================================================ FILE: src/lib/legacy/local/RowCount.svelte ================================================ ================================================ FILE: src/lib/legacy/local/RowsPerPage.svelte ================================================ ================================================ FILE: src/lib/legacy/local/Search.svelte ================================================ handler.search(value)} /> ================================================ FILE: src/lib/legacy/local/Th.svelte ================================================ handler.sort(orderBy, identifier)} class:sortable={orderBy} class:active={$sort.identifier === identifier} class={$$props.class ?? ''} rowspan={rowSpan} >
================================================ FILE: src/lib/legacy/local/ThFilter.svelte ================================================ handler.filter(value, filterBy, comparator)} /> ================================================ FILE: src/lib/legacy/local/handlers/EventHandler.ts ================================================ import { writable } from 'svelte/store' export default class EventHandler { private events = { change : [] as (() => void)[], clearFilters: [] as (() => void)[], clearSearch : [] as (() => void)[] } public triggerChange = writable(0) // legacy public add(event: keyof EventHandler['events'], callback: () => void) { this.events[event].push(callback) } public trigger(event: keyof EventHandler['events']) { for (const callback of this.events[event]) { callback() } /* legacy: support for triggerChange store */ if (event === 'change') { this.triggerChange.update((store) => { return store + 1 }) } } } ================================================ FILE: src/lib/legacy/local/handlers/FilterHandler.ts ================================================ import type { Filter, Field, Comparator, EventHandler, Criterion } from '$lib/legacy/local' import { isNotNull } from '../utils' import type Context from '$lib/legacy/local/Context' import { type Writable, type Readable, derived } from 'svelte/store' import { parseField } from '$lib/legacy/local/utils' // import { check } from '$lib/legacy/local/Comparator' type Value = string | number | null | undefined | boolean | number[] | Criterion[] // type Collection = { // value: unknown // filterBy: Field // set: (value: unknown, comparator: Comparator) => void, // clear: () => void // } export default class FilterHandler { protected filters: Writable[]> protected event: EventHandler private collection: Readable<{ value: unknown, filterBy: Field, check: string }[]> constructor(context: Context) { this.filters = context.filters this.event = context.event } public set(value: Value, filterBy: Field, comparator: Comparator = null, name?: string ) { 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 }) } public clear() { this.filters.set([]) this.event.trigger('change') this.event.trigger('clearFilters') } public get() { if (this.collection) { return this.collection } this.collection = this.createCollection() return this.collection } private createCollection() { return derived(this.filters, ($filters) => { return $filters.map( ({ value, callback, key, comparator }) => { const filterBy = key as Field ?? callback return { value, filterBy, check: comparator ? comparator.name : 'isLike' // set: (value: Value, comparator: Comparator = check.isLike) => { // this.set(value, filterBy, comparator) // }, // clear: () => { // this.set(undefined, filterBy) // } } }) }) } } ================================================ FILE: src/lib/legacy/local/handlers/PageHandler.ts ================================================ import type Context from '$lib/legacy/local/Context' import { type Writable, type Readable, get } from 'svelte/store' import type { EventHandler } from '$lib/legacy/local' export default class PageHandler { private pageNumber : Writable private rowCount : Readable<{ total: number, start: number, end: number }> private rowsPerPage : Writable private event : EventHandler constructor(context: Context) { this.pageNumber = context.pageNumber this.rowCount = context.rowCount this.rowsPerPage = context.rowsPerPage this.event = context.event } public goto(number: 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 }) } public previous() { const number = get(this.pageNumber) - 1 this.goto(number) } public next() { const number = get(this.pageNumber) + 1 this.goto(number) } } ================================================ FILE: src/lib/legacy/local/handlers/SearchHandler.ts ================================================ import type Context from '$lib/legacy/local/Context' import type { Writable } from 'svelte/store' import type { EventHandler, Field } from '$lib/legacy/local' export default class SearchHandler { private search : Writable<{ value?: string, scope?: Field[] }> private event : EventHandler constructor(context: Context) { this.search = context.search this.event = context.event } public set(value: string, scope: Field[] = null) { this.search.update((store) => { store = { value: value ?? '', scope: scope ?? null, } return store }) } public clear() { this.search.set({ value: null, scope: null }) this.event.trigger('change') this.event.trigger('clearSearch') } } ================================================ FILE: src/lib/legacy/local/handlers/SelectHandler.ts ================================================ import type Context from '$lib/legacy/local/Context' import { type Writable, type Readable, get } from 'svelte/store' import type EventHandler from './EventHandler' export default class SelectHandler { private filteredRows : Readable private pagedRows : Readable private selected : Writable<(Row | Row[keyof Row])[]> private scope : Writable<'currentPage' | 'all'> private isAllSelected : Readable private event : EventHandler constructor(context: 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 } public set(value: Row[keyof Row] | Row) { const selected = get(this.selected) if (selected.includes(value)) { this.selected.set(selected.filter((item) => item !== value)) } else { this.selected.set([value, ...selected]) } } public all(selectBy: keyof Row = 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) } } public clear() { this.selected.set([]) } } ================================================ FILE: src/lib/legacy/local/handlers/SortHandler.ts ================================================ import type Context from '$lib/legacy/local/Context' import type { Sort, Field, EventHandler } from '$lib/legacy/local' import { type Writable, get } from 'svelte/store' import { parseField } from '$lib/legacy/local/utils' export default class SortHandler { private rawRows : Writable private event : EventHandler private sort : Writable<(Sort)> private backup : Sort[] constructor(context: Context) { this.rawRows = context.rawRows this.event = context.event this.sort = context.sort this.backup = [] } public set(orderBy: Field = null, uid?: string) { 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) } } public asc(orderBy: Field, uid?: string) { 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 as string) if (typeof a === 'number') return a - (b as number) 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') } public desc(orderBy: Field, uid?: string) { 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 as string) if (typeof b === 'number') return b - (a as number) 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') } public apply(params: { orderBy: Field, direction?: 'asc' | 'desc' } = 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() } } public clear() { this.backup = [] this.sort.set({}) } public define(orderBy: Field, direction: 'asc' | 'desc' = 'asc') { if (!orderBy) return const { identifier, callback, key } = parseField(orderBy) this.sort.set({ identifier, callback, direction, key }) } private restore() { for (const sort of this.backup) { const { key, callback, direction } = sort const param = key as Field ?? callback this[direction](param) } } private log(sort: 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] } } } ================================================ FILE: src/lib/legacy/local/helpers/AdvancedFilterHelper.ts ================================================ import type { Field, Comparator, Criterion } from '$lib/legacy/local' import type FilterHandler from '$lib/legacy/local/handlers/FilterHandler' import { type Writable, writable } from 'svelte/store' import { check } from '$lib/legacy/local/Comparator' type Value = string | number | [min: number, max: number] export default class AdvancedFilterHandler { private filterHandler : FilterHandler private criteria : Criterion[] private filterBy : Field private selected : Writable constructor(filterHandler: FilterHandler, filterBy: Field) { this.filterHandler = filterHandler this.filterBy = filterBy this.criteria = [] this.selected = writable([]) } public set(value: Value, comparator: 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)) } public getSelected() { return this.selected } public clear() { this.criteria = [] this.selected.set([]) this.filterHandler.set(undefined, this.filterBy, check.whereIn) } } ================================================ FILE: src/lib/legacy/local/helpers/CalculationHelper.ts ================================================ import type { Field } from '$lib/legacy/local' import type Context from '$lib/legacy/local/Context' import { type Writable, type Readable, get, derived } from 'svelte/store' import { parseField } from '$lib/legacy/local/utils' export default class CalcultationHandler { private rawRows: Writable private filteredRows: Readable private callback: (row: Row) => Row[keyof Row] private precision: number constructor(context: Context, field: Field, param: { precision: number }) { this.rawRows = context.rawRows this.filteredRows = context.filteredRows this.callback = parseField(field).callback this.precision = param.precision } public distinct(callback: (values: any[]) => any[] = 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 })) } public avg(callback: (values: number[]) => any[] = null) { return derived(this.filteredRows, $filteredRows => { if ($filteredRows.length === 0) return 0 const values = $filteredRows.map(row => this.callback(row)).filter(Boolean) as number[] const array = callback ? callback(values) : values return this.round(array.reduce((acc, curr) => acc + curr, 0) / array.length) }) } public sum(callback: (values: number[]) => any[] = null) { return derived(this.filteredRows, $filteredRows => { const values = $filteredRows.map(row => this.callback(row)) as number[] const array = callback ? callback(values) : values return this.round(array.reduce((acc, curr) => acc + curr, 0)) }) } public bounds(callback: (values: number[]) => any[] = null): [min: number, max: number] { const rawRows = get(this.rawRows) const values = rawRows.map(row => this.callback(row)) const numbers = callback ? callback(values as number[]) : values return [ Math.min(...numbers.filter(Boolean)), Math.max(...numbers.filter(Boolean)) ] } public setPrecision(value: number) { this.precision = value } private round(value: number) { if (this.precision === 0) { return Math.round(value) } const denominator = Math.pow(10, this.precision) return Math.round( (value + Number.EPSILON) * denominator ) / denominator } } ================================================ FILE: src/lib/legacy/local/helpers/FilterHelper.ts ================================================ import type { Field, Comparator } from '$lib/legacy/local' import { check } from '$lib/legacy/local/Comparator' import type FilterHandler from '../handlers/FilterHandler' type Value = string | number | boolean export default class FilterHelper { private filterHandler : FilterHandler private filterBy : Field private uid : string private comparator : Comparator private callback : () => void constructor(filterHandler: FilterHandler, filterBy: Field, comparator?: Comparator) { this.filterHandler = filterHandler this.filterBy = filterBy this.uid = 'f_' + (Math.random()).toString(28).substring(2) this.comparator = comparator ?? check.isLike this.callback = () => null } public set(value: Value, comparator?: Comparator) { if (comparator) { this.comparator = comparator } this.filterHandler.set(value, this.filterBy, this.comparator, this.uid) } public clear() { this.callback() this.filterHandler.set(undefined, this.filterBy) } public on(event: 'clear', callback: () => void) { this.callback = callback } } ================================================ FILE: src/lib/legacy/local/index.ts ================================================ // 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 } 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 = keyof Row | ((row: Row) => Row[keyof Row]) export type Comparator = (entry: Row[keyof Row], value: any) => boolean export type Criterion = { value: string | number | [min: number, max: number], comparator: Comparator } export type Filter = { callback: (row: Row) => Row[keyof Row] identifier: string value?: string | number | boolean | symbol | Criterion[] | number[] comparator?: Comparator key?: string } export type Sort = { 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[keyof Row] | Row /** * @deprecated use type Field instead * * import type { Field } from '@vincjo/datatables' */ export type FilterBy = Field /** * @deprecated use type Field instead * * import type { Field } from '@vincjo/datatables' */ export type OrderBy = Field /** * @deprecated use type Sort instead * * import type { Sort } from '@vincjo/datatables' */ export type Order = Sort ================================================ FILE: src/lib/legacy/local/utils.ts ================================================ import type { Row, Field } from '$lib/legacy/local' export const isNull = (value: any) => { if (value === null || value === undefined || value === '') return true return false } export const isNotNull = (value: any) => { return !isNull(value) } export const parseField = (field: Field, uid?: string) => { const identifier = uid ?? field.toString() if (typeof field === 'string') { return { callback: (row: Row) => row[field], identifier, key: field as string } } else if (typeof field === 'function') { return { callback: field, identifier, key: undefined } } throw new Error(`Invalid field argument: ${String(field)}`) } ================================================ FILE: src/lib/legacy/remote/Context.ts ================================================ import { type Writable, writable, get, derived, type Readable } from 'svelte/store' import type { State, Sort, Filter } from '$lib/legacy/remote' import type { Params } from './DataHandler' import EventHandler from './handlers/EventHandler' export default class Context { public totalRows : Writable public rowsPerPage : Writable public pageNumber : Writable public event : EventHandler public search : Writable public filters : Writable[]> public rows : Writable public rowCount : Readable<{ total: number, start: number, end: number }> public pages : Readable public pagesWithEllipsis : Readable public pageCount : Readable public sort : Writable> public selected : Writable<(Row | Row[keyof Row])[]> public isAllSelected : Readable public selectedCount : Readable<{ count: number, total: number }> public selectBy : keyof Row | undefined constructor(data: Row[], params: 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 as keyof Row ?? undefined } public getState(): State { 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 as any, // deprecated sort: sort ?? undefined as any, filters: filters.length > 0 ? filters : undefined as any, setTotalRows: (value: number) => this.totalRows.set(value) } } private 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 }) }) } private 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) ] } }) } private createPageCount() { return derived(this.pages, ($pages) => { if (!$pages) return undefined return $pages.length }) } private 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) } } ) } private 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 as Row)) }) } private createSelectedCount() { return derived( [this.selected, this.totalRows], ([$selected, $totalRows]) => { return { count: $selected.length, total: $totalRows } } ) } } ================================================ FILE: src/lib/legacy/remote/DataHandler.ts ================================================ 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' import type { Writable, Readable } from 'svelte/store' import type { Internationalization, Row, State, Sort } from '$lib/legacy/remote' export type Params = { rowsPerPage ?: number, totalRows ?: number, selectBy ?: keyof Row, i18n ?: Internationalization } export default class DataHandler { private context : Context private triggerHandler : TriggerHandler private sortHandler : SortHandler private selectHandler : SelectHandler private pageHandler : PageHandler private searchHandler : SearchHandler private filterHandler : FilterHandler public i18n : Internationalization constructor(data: T[] = [], params: 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) } public onChange(callback: (state: State) => Promise) { this.triggerHandler.set(callback) } public invalidate() { this.triggerHandler.invalidate() } public setRows(data: T[]) { this.context.rows.set(data) } public setTotalRows(value: number) { this.context.totalRows.set(value) } public getRows(): Writable { return this.context.rows } public select(value: T[keyof T] | T) { this.selectHandler.set(value) } public getSelected() { return this.context.selected } public selectAll(): void { this.selectHandler.all() } public isAllSelected(): Readable { return this.context.isAllSelected } public getSelectedCount(): Readable<{ count: number, total: number }> { return this.context.selectedCount } public clearSelection() { this.selectHandler.clear() } public getRowsPerPage(): Writable { return this.context.rowsPerPage } public sort(orderBy: keyof T) { this.setPage(1) this.sortHandler.set(orderBy) } public applySort( params: { orderBy: keyof T, direction?: 'asc' | 'desc' } = null ) { this.sortHandler.apply(params) } public sortAsc(orderBy: keyof T) { this.setPage(1) this.sortHandler.asc(orderBy) } public sortDesc(orderBy: keyof T) { this.setPage(1) this.sortHandler.desc(orderBy) } public getSort(): Writable> { return this.context.sort } public search(value: string): void { this.setPage(1) this.context.search.set(value) } public clearSearch() { this.searchHandler.remove() } public filter(value: string | number, filterBy: keyof T) { this.setPage(1) return this.filterHandler.set(value, filterBy) } public clearFilters(): void { this.filterHandler.remove() } public getPages(params = { ellipsis: false }): Readable { if (params.ellipsis) { return this.context.pagesWithEllipsis } return this.context.pages } public getPageCount(): Readable { return this.context.pageCount } public getPageNumber(): Writable { return this.context.pageNumber } public setPage(value: number | 'previous' | 'next'): void { switch (value) { case 'previous' : return this.pageHandler.previous() case 'next' : return this.pageHandler.next() default : return this.pageHandler.goto(value as number) } } public getRowCount(): Readable<{ total: number, start: number, end: number }> { return this.context.rowCount } public on(event: 'change', callback: () => void) { this.context.event.add(event, callback) } public translate(i18n: Internationalization): Internationalization { 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 */ public getTriggerChange(): Writable { return this.context.event.triggerChange } /** * * @deprecated use applySort() instead */ public applySorting( params: { orderBy: keyof T, direction?: 'asc' | 'desc' } = null ) { this.applySort(params) } /** * * @deprecated use getSort() instead */ public getSorted() { return this.getSort() } } ================================================ FILE: src/lib/legacy/remote/Datatable.svelte ================================================
{#if search} {:else}
{/if} {#if rowsPerPage} {/if}
{#if selectedCount} {:else if rowCount} {/if} {#if pagination} {/if}
================================================ FILE: src/lib/legacy/remote/Pagination.svelte ================================================
{#if $pages === undefined} {:else} {#if small} {:else} {#each $pages as page} {/each} {/if} {/if}
================================================ FILE: src/lib/legacy/remote/README.md ================================================
logo

svelte simple datatables

For server-side pagination

npm version last commit

**→ [Home](https://vincjo.fr/datatables/remote/home)** **→ [Examples](https://vincjo.fr/datatables/remote/examples)** ================================================ FILE: src/lib/legacy/remote/RowCount.svelte ================================================ {#if $rowCount === undefined}
{:else} {/if} ================================================ FILE: src/lib/legacy/remote/RowsPerPage.svelte ================================================ ================================================ FILE: src/lib/legacy/remote/Search.svelte ================================================ ================================================ FILE: src/lib/legacy/remote/SelectedCount.svelte ================================================ ================================================ FILE: src/lib/legacy/remote/Th.svelte ================================================
================================================ FILE: src/lib/legacy/remote/ThFilter.svelte ================================================ ================================================ FILE: src/lib/legacy/remote/handlers/EventHandler.ts ================================================ import { writable } from 'svelte/store' export default class EventHandler { private events = { change : [] as (() => void)[], clearFilters: [] as (() => void)[], clearSearch : [] as (() => void)[] } public triggerChange = writable(0) // legacy public add(event: keyof EventHandler['events'], callback: () => void) { this.events[event].push(callback) } public trigger(event: keyof EventHandler['events']) { for (const callback of this.events[event]) { callback() } /* legacy: support for triggerChange store */ if (event === 'change') { this.triggerChange.update((store) => { return store + 1 }) } } } ================================================ FILE: src/lib/legacy/remote/handlers/FilterHandler.ts ================================================ import type { Filter } from '$lib/legacy/remote' import type Context from '$lib/legacy/remote/Context' import type { Writable } from 'svelte/store' export default class FilterHandler { public filters: Writable[]> constructor(context: Context) { this.filters = context.filters } public set(value: string | number, filterBy: keyof Row ) { 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 }) } public remove() { this.filters.set([]) } } ================================================ FILE: src/lib/legacy/remote/handlers/PageHandler.ts ================================================ import type Context from '$lib/legacy/remote/Context' import { type Writable, type Readable, get } from 'svelte/store' import type EventHandler from './EventHandler' export default class PageHandler { public totalRows : Writable public pageNumber : Writable public rowCount : Readable<{ total: number, start: number, end: number }> public rowsPerPage : Writable public event : EventHandler public pages : Readable public selected : Writable<(Row | Row[keyof Row])[]> constructor(context: 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 } public get() { return this.pages } public goto(number: 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 } }) } public previous() { const number = get(this.pageNumber) - 1 this.goto(number) } public next() { const number = get(this.pageNumber) + 1 this.goto(number) } } ================================================ FILE: src/lib/legacy/remote/handlers/SearchHandler.ts ================================================ import type Context from '$lib/legacy/remote/Context' import type { Writable } from 'svelte/store' export default class SearchHandler { private search: Writable constructor(context: Context) { this.search = context.search } public set(value: string) { this.search.set(value ?? null) } public remove() { this.search.set(null) } } ================================================ FILE: src/lib/legacy/remote/handlers/SelectHandler.ts ================================================ import type Context from '$lib/legacy/remote/Context' import { type Writable, type Readable, get } from 'svelte/store' export default class SelectHandler { private rows : Readable private selected : Writable<(Row | Row[keyof Row])[]> private isAllSelected : Readable private selectBy : keyof Row | undefined constructor(context: Context) { this.rows = context.rows this.selected = context.selected this.isAllSelected = context.isAllSelected this.selectBy = context.selectBy } public set(value: Row | Row[keyof Row]) { const selected = get(this.selected) if (selected.includes(value)) { this.selected.set(selected.filter((item) => item !== value)) } else { this.selected.set([value, ...selected]) } } public 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 as Row[keyof Row])) } return store = store.filter(item => !rows.includes(item as Row)) }) if (!isAllSelected) { this.selected.update( store => { if (this.selectBy) { store = [...rows.map((row) => row[this.selectBy]), ...store] } else { store = [...rows, ...store] } return store }) } } public clear() { this.selected.set([]) } } ================================================ FILE: src/lib/legacy/remote/handlers/SortHandler.ts ================================================ import type Context from '$lib/legacy/remote/Context' import type { Order } from '$lib/legacy/remote' import { type Writable, get } from 'svelte/store' import type EventHandler from './EventHandler' export default class SortHandler { private event : EventHandler private hasMultipleSort : boolean private sort : Writable> constructor(context: Context) { this.event = context.event this.hasMultipleSort = false this.sort = context.sort } public set(orderBy: keyof Row = 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) } } public asc(orderBy: keyof Row) { if (!orderBy) return this.sort.set({ orderBy, direction: 'asc' }) this.event.trigger('change') } public desc(orderBy: keyof Row) { if (!orderBy) return this.sort.set({ orderBy, direction: 'desc' }) this.event.trigger('change') } public apply(params: { orderBy: keyof Row, direction?: 'asc' | 'desc' } = 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 } // public set(orderBy: keyof Row, direction: 'asc' | 'desc') // { // const sort = { orderBy, direction } // if (this.hasMultipleSort === false) { // this.sort.set([ sort ]) // return // } // this.sort.update((store) => { // store = store.filter((item) => { // return (item.orderBy !== orderBy) && item.direction // }) // if (orderBy) { // store.push(sort) // } // return store // }) // } // public sort(orderBy: keyof Row = null) // { // if (!orderBy) return // const sort = get(this.sort) // const exists = sort.find(sort => sort.orderBy === orderBy) // if(!exists) { // this.sortAsc(orderBy) // } // else if (exists.direction === 'asc') { // this.sortDesc(exists.orderBy) // } // else if (exists.direction === 'desc') { // this.sortAsc(orderBy) // } // } // public sortAsc(orderBy: keyof Row) // { // if (!orderBy) return // this.set( orderBy, 'asc' ) // this.triggerChange.update((store) => { return store + 1 }) // } // public sortDesc(orderBy: keyof Row) // { // if (!orderBy) return // this.set( orderBy, 'desc' ) // this.triggerChange.update((store) => { return store + 1 }) // } // public applySorting(params: { orderBy: keyof Row, direction?: 'asc' | 'desc' } = null) // { // if (params) { // switch (params.direction) { // case 'asc' : return this.sortAsc(params.orderBy) // case 'desc': return this.sortDesc(params.orderBy) // default : return this.sort(params.orderBy) // } // } // const sort = get(this.sort) // if (sort.length > 0) { // for (const order of sort) { // return this.applySorting({ orderBy: order.orderBy, direction: order.direction }) // } // } // return // } } ================================================ FILE: src/lib/legacy/remote/handlers/TriggerHandler.ts ================================================ import type { State } from '$lib/legacy/remote' import type Context from '$lib/legacy/remote/Context' export default class TriggerHandler { private context: Context private reload: (state: State) => Promise constructor(context: Context) { this.context = context } public set(callback: (state: State) => Promise) { this.reload = callback } public async invalidate() { if (!this.reload) return const state = this.context.getState() const data = await this.reload(state) if (data) { this.context.rows.set(data) } } } ================================================ FILE: src/lib/legacy/remote/index.ts ================================================ // 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 } 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 = { filterBy: keyof Row value?: string | number | boolean } export type Sort = { orderBy?: keyof Row direction?: 'asc' | 'desc' } export type State = { pageNumber: number, rowsPerPage: number, offset: number, search: string | undefined, sort: Sort | undefined filters: Filter[] | undefined setTotalRows: (value: number) => void /** * @deprecated use 'sort' instead */ sorted: Sort | 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[keyof Row] | Row /** * @deprecated use type Sort instead * @since v1.13.0 2023-11-14 * * import type { Sort } from '@vincjo/datatables' */ export type Order = Sort ================================================ FILE: src/lib/server/index.ts ================================================ export { // class: TableHandler, // components: Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, // types: type Row, type State, type Filter, type Sort, type Internationalization, type TableHandlerInterface } from '$lib/src/server/index.js' ================================================ FILE: src/lib/src/client/AbstractTableHandler.svelte.ts ================================================ import type { Field, TableParams } from '$lib/src/client' import { EventDispatcher } from '$lib/src/shared' import { type Search, type Filter, type Query, type Sort, data, parse } from './core' export default abstract class AbstractTableHandler { protected selectBy ?: Field protected selectScope = $state<'all' | 'currentPage'>('currentPage') protected highlight : boolean protected event = new EventDispatcher() protected rawRows = $state.raw([]) protected search = $state<(Search)>({ value: null }) protected sort = $state<(Sort)>({}) public filters = $state<(Filter)[]>([]) public queries = $state<(Query)[]>([]) public rowsPerPage = $state(10) public currentPage = $state(1) public element = $state(undefined) public clientWidth = $state(1000) public filterCount = $derived(this.filters.length) public allRows = $derived[]>(this.createAllRows()) public rows = $derived[]>(this.createRows()) public rowCount = $derived<{total: number, start: number, end: number, selected: number}>(this.createRowCount()) public pages = $derived(this.createPages()) public pageCount = $derived(this.pages.length) public pagesWithEllipsis = $derived(this.createPagesWithEllipsis()) public selected = $state([]) public isAllSelected = $derived(this.createIsAllSelected()) constructor(data: Row[], params: TableParams) { this.rawRows = data this.rowsPerPage = params.rowsPerPage ?? null this.highlight = params.highlight ?? false this.selectBy = params.selectBy } private 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 } private createRows() { if (!this.rowsPerPage) return this.allRows return this.allRows.slice( (this.currentPage - 1) * this.rowsPerPage, this.currentPage * this.rowsPerPage ) } private 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 } } private createPages() { if (!this.rowsPerPage) { return [1] } const pages = Array.from(Array(Math.ceil(this.allRows.length / this.rowsPerPage))) return pages.map((_, i) => i + 1 ) } private 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) ] } } private 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)) } } ================================================ FILE: src/lib/src/client/TableHandler.svelte.ts ================================================ 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 type { Internationalization, Row, Field, Check, TableParams, ColumnView, TableHandlerInterface } from '$lib/src/client' 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 implements TableHandlerInterface { public i18n : Internationalization private view : ViewBuilder private sortHandler : SortHandler private filterHandler : FilterHandler private queryHandler : QueryHandler private selectHandler : SelectHandler private pageHandler : PageHandler private searchHandler : SearchHandler constructor(data: T[] = [], params: TableParams = { 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) } public setRows(data: T[]): void { 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) } }) } public setRowsPerPage(value: number): void { this.rowsPerPage = value this.setPage(1) } public setPage(value: number | 'previous' | 'next' | 'last'): void { 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 as number) } } public createSearch(scope?: Field[]): SearchBuilder { return new SearchBuilder(this.searchHandler, scope) } public clearSearch(): void { this.searchHandler.clear() this.event.dispatch('clearSearch') this.setPage(1) } public createRecordFilter(records?: Row[]): RecordFilterBuilder { return new RecordFilterBuilder(records) } public createSort(field: Field, params?: { locales: Intl.LocalesArgument, options: Intl.CollatorOptions}): SortBuilder { return new SortBuilder(this.sortHandler, field, params) } public clearSort() { this.sortHandler.clear() } public clearFilters(): void { this.filters = [] this.event.dispatch('clearFilters') this.setPage(1) } public createAdvancedFilter(field: Field, check?: Check): AdvancedFilterBuilder { return new AdvancedFilterBuilder(this.filterHandler, field, check) } public createFilter(field: Field, check?: Check): FilterBuilder { return new FilterBuilder(this.filterHandler, field, check) } public createQuery(): QueryBuilder { return new QueryBuilder(this.queryHandler) } public select(value: unknown): void { this.selectHandler.set(value) } public selectAll(params: { scope?: 'all' | 'currentPage' } = {}): void { this.selectScope = (params.scope === 'all') ? 'all' : 'currentPage' this.selectHandler.all(this.selectScope) } public getSelectedRows(): T[] { return this.selectHandler.getRows() } public clearSelection(): void { this.selectHandler.clear() } public on(event: 'change' | 'clearFilters' | 'clearSearch', callback: () => void) { this.event.add(event, callback) } public createCalculation(field: Field): CalculationBuilder { return new CalculationBuilder(this, field) } public createCSV(): CSVBuilder { return new CSVBuilder(this) } public createView(columns: ColumnView[]): ViewBuilder { this.view = new ViewBuilder(this, columns) return this.view } public getView(): ViewBuilder { return this.view } private translate(i18n: Internationalization): void { 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 } } } ================================================ FILE: src/lib/src/client/builders/AdvancedFilterBuilder.svelte.ts ================================================ import type { Field, Check, Criterion } from '$lib/src/client' import type FilterHandler from '$lib/src/client/handlers/FilterHandler.svelte' import { check as comparator } from '$lib/src/client/core' export default class AdvancedFilterBuilder { public criteria = $state([]) private id = Math.random().toString(36).substring(2, 15) private filterHandler : FilterHandler private collection : Criterion[] private field : Field private check : Check private isRecursive = true constructor(filterHandler: FilterHandler, field: Field, check?: Check) { this.filterHandler = filterHandler this.field = field this.collection = [] this.check = check ?? comparator.isEqualTo this.cleanup() } public set(value: unknown, check?: Check): void { 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) } public isNotRecursive() { this.isRecursive = false return this } public clear(): void { this.collection = [] this.criteria = [] this.filterHandler.unset(this.id) } private cleanup() { this.filterHandler['table'].on('clearFilters', () => this.clear()) } } ================================================ FILE: src/lib/src/client/builders/CSVBuilder.svelte.ts ================================================ import type { TableHandler } from '$lib/src/client' export default class CSVBuilder { private table: TableHandler constructor(table: TableHandler) { this.table = table } public download(filename: string) { 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) } public get(): string { const rows = this.getRows() rows.unshift(this.getHeader().join(',')) return rows.join('\r\n') } private 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(',') }) } private getHeader() { const [row] = this.table.allRows return Object.entries(row).map(([key, _]) => { return `"${key}"` }) } } ================================================ FILE: src/lib/src/client/builders/CalculationBuilder.svelte.ts ================================================ import type { Field, TableHandler } from '$lib/src/client' import { sort, parse } from '$lib/src/client/core' type Sort = [key: 'value' | 'count', direction: 'asc' | 'desc'] export default class CalcultationBuilder { private callback : (row: $state.Snapshot) => string | number private precision : number private table : TableHandler constructor(table: TableHandler, field: Field) { this.table = table this.callback = parse(field).callback as (row: $state.Snapshot) => string | number } public distinct(param?: { sort: Sort }): { value: string, count: number }[] { const values = this.table.allRows.map(row => this.callback(row)) const aggregate: { [key: string ]: number } = 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 } public avg(param?: { precision: number }): number { 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) as number[] return this.round(values.reduce((acc, curr) => acc + curr, 0) / values.length) } public sum(param?: { precision: number }): number { this.precision = param?.precision ?? 2 const values = this.table.allRows.map(row => this.callback(row)) as number[] return this.round(values.reduce((acc, curr) => acc + curr, 0)) } public median(param?: { precision: number }): number { this.precision = param?.precision ?? 2 const values = [...this.table.allRows.map(row => this.callback(row))] .sort((a: number, b: number) => a - b) .filter(Boolean) as number[] 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) ) } public bounds(): number[] { const values = this.table.allRows.map(row => this.callback(row)) const numbers = values.filter(Boolean) as number[] if (numbers.length === 0) return [null, null] return [ Math.min(...numbers), Math.max(...numbers) ] } private round(value: number) { return Number(value.toFixed(this.precision)) } } ================================================ FILE: src/lib/src/client/builders/FilterBuilder.svelte.ts ================================================ import type { Field, Check } from '$lib/src/client' import type FilterHandler from '../handlers/FilterHandler.svelte' import { check as comparator } from '$lib/src/client/core' import type { FilterInterface } from '$lib/src/shared' export default class FilterBuilder implements FilterInterface { public value = $state('') private id = Math.random().toString(36).substring(2, 15) private filterHandler : FilterHandler private field : Field private check : Check private isRecursive = true constructor(filterHandler: FilterHandler, field: Field, check?: Check) { this.filterHandler = filterHandler this.field = field this.check = check ?? comparator.isLike this.cleanup() } public set(check?: Check) { this.filterHandler.set(this.value, this.field, check ?? this.check, this.id, this.isRecursive) } public init(value?: unknown) { this.value = value this.set() return this } public isNotRecursive() { this.isRecursive = false return this } public clear() { this.value = '' this.filterHandler.unset(this.id) } private cleanup() { this.filterHandler['table'].on('clearFilters', () => this.clear()) } } ================================================ FILE: src/lib/src/client/builders/QueryBuilder.svelte.ts ================================================ import type { Check } from '$lib/src/client' import type QueryHandler from '../handlers/QueryHandler.svelte' export default class QueryBuilder { public value = $state('') private id = Math.random().toString(36).substring(2, 15) private queryHandler : QueryHandler private path : string[] = [] private check : Check constructor(queryHandler: QueryHandler) { this.queryHandler = queryHandler this.cleanup() } public from(path: string[]) { this.path = path return this } public where(filter: (row: any, value?: unknown) => boolean) { this.check = filter return this } public set(value?: unknown) { if (value) this.value = value this.queryHandler.set(this.path, this.value, this.check, this.id) } public clear() { this.value = '' this.queryHandler.unset(this.id) } private cleanup() { this.queryHandler['table'].on('clearFilters', () => this.clear()) } } ================================================ FILE: src/lib/src/client/builders/RecordFilterBuilder.svelte.ts ================================================ import { match, check, isNotNull } from '$lib/src/client/core' import type { Row } from '$lib/src/client' export default class RecordFilterBuilder { public value = $state('') public records = $derived(this.createRecords()) private rawRecords = $state.raw([]) private filter = $state('') constructor(records: Row[]) { this.rawRecords = records } public set() { this.filter = this.value } private createRecords(): readonly Row[] { if (isNotNull(this.filter)) { return this.rawRecords.filter(record => match(record, this.filter, { check: check.isLike })) } return this.rawRecords } } ================================================ FILE: src/lib/src/client/builders/SearchBuilder.svelte.ts ================================================ import { type Field } from '$lib/src/client' import type SearchHandler from '../handlers/SearchHandler.svelte' import type { SearchInterface } from '$lib/src/shared' export default class SearchBuilder implements SearchInterface { public value = $state('') private scope : Field[] private searchHandler : SearchHandler constructor(searchHandler: SearchHandler, scope?: Field[]) { this.searchHandler = searchHandler this.scope = scope this.cleanup() } public set() { this.searchHandler.set(this.value, this.scope) } public init(value?: string) { this.value = value this.set() return this } public recursive() { this.searchHandler.recursive(this.value, this.scope) } public regex() { this.searchHandler.regex(this.value, this.scope) } public clear() { this.value = '' this.searchHandler.clear() } private cleanup() { this.searchHandler['table'].on('clearSearch', () => this.clear()) } } ================================================ FILE: src/lib/src/client/builders/SortBuilder.svelte.ts ================================================ import type { Field, SortParams } from '$lib/src/client' import type SortHandler from '../handlers/SortHandler.svelte' import type { SortInterface } from '$lib/src/shared' export default class SortBuilder implements SortInterface { public direction = $derived<'asc' | 'desc'>(this.createDirection()) public isActive = $derived(this.createIsActive()) private id = Math.random().toString(36).substring(2, 15) private sortHandler : SortHandler private field : Field private params : SortParams constructor(sortHandler: SortHandler, field: Field, params: SortParams) { this.sortHandler = sortHandler this.field = field this.params = params ?? {} } public set() { this.sortHandler.set(this.field, this.id, this.params) } public init(direction?: 'asc' | 'desc') { if (!direction) return this this[direction]() return this } public asc() { this.sortHandler.asc(this.field, this.id, this.params) } public desc() { this.sortHandler.desc(this.field, this.id, this.params) } public clear() { this.sortHandler.clear() } private createIsActive() { if (this.id === this.sortHandler['table']['sort']?.id) { return true } return false } private createDirection() { if (this.isActive === false) return null return this.sortHandler['table']['sort']?.direction } } ================================================ FILE: src/lib/src/client/core/check.ts ================================================ import { isNull, stringify, isNotNull } from './value' import type { Criterion, Check } from '$lib/src/client' export const check: { [name: string]: Check } = { isLike: (entry: unknown, value: unknown) => stringify(entry).includes(stringify(value)), isNotLike: (entry: unknown, value: unknown) => stringify(entry).includes(stringify(value)) === false, startsWith: (entry: unknown, value: unknown) => stringify(entry).startsWith(stringify(value)), endsWith: (entry: unknown, value: unknown) => stringify(entry).endsWith(stringify(value)), isEqualTo: (entry: unknown, value: unknown) => stringify(entry) === stringify(value), isNotEqualTo: (entry: unknown, value: unknown) => stringify(entry) !== stringify(value), isGreaterThan: (entry: number, value: number) => isNull(entry) ? false : (entry > value), isGreaterThanOrEqualTo: (entry: number, value: number) => isNull(entry) ? false : (entry >= value), isLessThan: (entry: number, value: number) => isNull(entry) ? false : (entry < value), isLessThanOrEqualTo: (entry: number, value: number) => isNull(entry) ? false : (entry <= value), isBetween: (entry: number, [min, max]: number[]) => isNull(entry) ? false : (entry >= min && entry <= max), isStrictlyBetween: (entry: number, [min, max]: number[]) => isNull(entry) ? false : (entry > min && entry < max), isTrue: (entry: unknown, _: unknown) => entry === true, isFalse: (entry: unknown, _: unknown) => entry === false, isNull: (entry: unknown, _: unknown) => isNull(entry), isNotNull: (entry: unknown, _: unknown) => isNotNull(entry), // multiple criteria whereIn: (entry: unknown, criteria: Criterion[] = []) => { if (criteria.length === 0) return false for(const { value, check } of criteria) { if (value?.['key']) { return checkByKey(entry, value as any, check) } else if (check(entry, value)) { return true } } return false }, // regexp match: (entry: unknown, pattern: string) => { const match = pattern.match(/^([\/~@;%#'])(.*?)\1([gimsuy]*)$/) const regex = match ? new RegExp(match[2], match[3] .split('') .filter((char, pos, flagArr) => flagArr.indexOf(char) === pos) .join('') ) : new RegExp(pattern) return stringify(entry).match(regex) ? true : false }, } const checkByKey = (entry: unknown, param: { key: string, value: unknown }, check: Check) => { if (!param?.key) return false const { key, value } = param if (Array.isArray(entry) === false && typeof entry === 'object') { const keys = Object.keys(entry) if (keys.includes(key) && check(entry[key], value)) { return true } } return false } ================================================ FILE: src/lib/src/client/core/entry.ts ================================================ import { isNull, isObject, isObjectArray } from './value' import { check } from './check' import type { Criterion, Check } from '$lib/src/client' type Params = { isRecursive?: boolean, check?: Check, highlight?: boolean, } export const match = (entry: unknown, value: unknown | Criterion[], params: Params): boolean => { params.check = params.check ?? check.isLike if (isNull(value)) { return true } else if (isObjectArray(entry)) { // if (isObject(entry) && entry.is_map === true) return true return Object.keys(entry).some(k => match(entry[k], value, params)) } return params.check(entry, value) } export const sift = (entry: unknown, value: unknown, params: Params) => { if (Array.isArray(entry)) { entry = entry.filter((item: unknown) => { // to disable deletion while filtering, check is worth true anyway const check = params.isRecursive === true ? match(item, value, params) : true if (typeof item === 'object' && check === true) { for (const k of Object.keys(item)) { item[k] = sift(item[k], value, params) } } return check }) } if (params.highlight && (typeof entry === 'string' || typeof entry === 'number') && typeof value === 'string' && match(entry, value, params)) { return emphasize(entry, value) } return entry } const emphasize = (entry: string | number, value: string) => { const search = value .replace(/a/g, '[aàâáä]') .replace(/e/g, '[eèêéë]') .replace(/i/g, '[iìîíï]') .replace(/o/g, '[oòôо́ö]') .replace(/u/g, '[uùûúü]') .replace(/y/g, '[yỳŷýÿ]') const exp = new RegExp(`${search}`, 'gi') return String(entry).replace(exp, `$&`) } export const deepEmphasize = (entry: Row, value: string, callback: (entry: Row) => unknown) => { const path = callback.toString() .split('=>')[1] .replace(/\(\)/g, '') .replace(/\?/g, '') .split('.').splice(1).join('.') .trim() if (path.indexOf(' ') > -1) return entry return deepSet(entry, path, value) } const deepSet = (entry: Row, path: string, value: string) => { const initial = entry const keys = path.replace(/\[/g, '.[').split(".") try { for (let i = 0; i < keys.length; i++) { let current = keys[i] let next = keys[i + 1] if (current.includes('[')) { current = String(parseInt(current.substring(1, current.length - 1))) } if (next && next.includes('[')) { next = String(parseInt(next.substring(1, next.length - 1))) } if (next !== undefined) { entry[current] = entry[current] ? entry[current] : (isNaN(Number(next)) ? {} : []) } else { entry[current] = emphasize(entry[current] as string | number, value) } entry = entry[current] as Row } return initial } catch(_) { return initial } } ================================================ FILE: src/lib/src/client/core/field.ts ================================================ import type { Field } from '$lib/src/client' export const parse = (field: Field, id?: string) => { if (typeof field === 'string') { return { callback: (row: $state.Snapshot) => row[(field as keyof $state.Snapshot)], id: id, key: field, } } else if (typeof field === 'function') { return { callback: field as (row: $state.Snapshot) => unknown, id: id, key: undefined, } } throw new Error(`Invalid field argument: ${String(field)}`) } ================================================ FILE: src/lib/src/client/core/index.ts ================================================ import type { Field, Check } from '$lib/src/client' export { isNull, isNotNull, stringify } from './value' export { match, sift, deepEmphasize } from './entry' export { check } from './check' export { data, sort } from './rows' export { parse } from './field' export type Search = { value: string scope?: Field[] isRecursive?: boolean check?: Check } export type Filter = { callback: (row: $state.Snapshot) => unknown id: string value?: unknown isRecursive?: boolean check?: Check key?: string } export type Query = { path: string[] id: string value?: unknown check?: Check } export type Sort = { callback?: (row: $state.Snapshot) => unknown id?: string direction?: 'asc' | 'desc' key?: string, } ================================================ FILE: src/lib/src/client/core/rows.ts ================================================ import type { Field } from '$lib/src/client' import { type Search, type Filter, type Query } from '$lib/src/client/core' import { match, sift, deepEmphasize } from './entry' import { isNull } from './value' import { parse } from './field' export const data = { search: (allRows: $state.Snapshot, { scope, isRecursive, value, check }: Search, highlight: boolean = false) => { return allRows.filter(row => { const keys = scope ?? Object.keys(row) as Field[] const fields = keys.map(field => parse(field)) for(const { key, callback } of fields) { if (key) { row[key] = sift(row[key], value, { highlight: highlight, isRecursive: isRecursive === true }) } else if (highlight) { row = deepEmphasize(row, value, callback) } } return fields.some(({ callback }) => { return match(callback(row), value, { check }) }) }) }, filter: (allRows: $state.Snapshot, { callback, isRecursive, value, check, key }: Filter, highlight: boolean = false) => { return allRows.filter((row) => { const checked = match(callback(row), value, { check }) if (key) { row[key] = sift(row[key], value, { highlight: highlight, check: check, isRecursive: isRecursive === false ? false : true }) } else if (highlight && checked && value && typeof value === 'string') { row = deepEmphasize(row, value, callback) } return checked }) }, query: (allRows: $state.Snapshot, { path, value, check }: Query) => { return allRows.filter(row => { if (path.length === 0) { return check(row, value) } let obj = row let i = 1 let verify = false function recursive(i) { } // ['groups', 'users'] // row[groups].map(group => group.users.map(user) => { // }) // row.groups.filter(group => group.users.filter(user => )) for (const prop of path) { if (i === 1 && i < path.length) { obj = obj[prop] for (const subobj of obj[prop]) { } } if (i === path.length) { console.log(prop, obj) obj[prop] = data.query(obj[prop], { path: [], value, check } as Query) if (obj[prop].length > 0) { verify = true } } obj = obj[prop] i++ } return verify }) // return allRows.map(row => { // let obj = row // const [root, ...props] = path // let i = 1 // for (const prop of path) { // if (obj[prop]) { // if (obj[root]) { // obj[root] = data.query(obj[root], { path, value, check } as Query) // } // if (i < props.length) { // obj[prop] = data.query(obj[prop], { path, value, check } as Query) // } // else { // obj[prop] = obj[prop].filter((item: any) => check(item)) // } // } // i++ // } // return row // }).filter(Boolean) } } export const sort = { asc: (a: unknown, b: unknown, locales?: Intl.LocalesArgument, options?: Intl.CollatorOptions) => { if (a === b) return 0 else if (isNull(a)) return 1 else if (isNull(b)) return -1 else if (typeof a === 'boolean') return a === false ? 1 : -1 else if (typeof a === 'string') return a.localeCompare(b as string, locales, options) else if (typeof a === 'number') return a - (b as number) else if (typeof a === 'object') return JSON.stringify(a).localeCompare(JSON.stringify(b), locales, options) return String(a).localeCompare(String(b), locales, options) }, desc: (a: unknown, b: unknown, locales?: Intl.LocalesArgument, options?: Intl.CollatorOptions) => { if (a === b) return 0 else if (isNull(a)) return 1 else if (isNull(b)) return -1 else if (typeof b === 'boolean') return b === false ? 1 : -1 else if (typeof b === 'string') return b.localeCompare(a as string, locales, options) else if (typeof b === 'number') return b - (a as number) else if (typeof b === 'object') return JSON.stringify(b).localeCompare(JSON.stringify(a), locales, options) else return String(b).localeCompare(String(a), locales, options) } } ================================================ FILE: src/lib/src/client/core/value.ts ================================================ export const isNull = (value: unknown) => { if (value === null || value === undefined || value === '') return true return false } export const isNotNull = (value: unknown) => { return !isNull(value) } export const stringify = (value: unknown = null) => { return String(value) .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') } export const isObject = (value: unknown) => { if (typeof value !== 'object') return false else if (value === null) return false else if (Array.isArray(value)) return false return true } export const isObjectArray = (value: unknown) => { if (typeof value !== 'object') return false else if (value === null) return false // test else if (Array.isArray(value)) { if (isNotNull(value[0]) && typeof value[0] !== 'object') return false } return true } ================================================ FILE: src/lib/src/client/handlers/FilterHandler.svelte.ts ================================================ import type { Field, Check, TableHandler } from '$lib/src/client' import { isNotNull, parse } from '$lib/src/client/core' export default class FilterHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(value: unknown, field: Field, check: Check = null, id: string, isRecursive = true) { this.table.setPage(1) const { callback, key } = parse(field, id) const filter = { value, id, callback, check, key, isRecursive } this.table.filters = this.table.filters.filter(filter => filter.id !== id) if (isNotNull(value)) { this.table.filters.push(filter) } } public unset(id: string) { this.table.setPage(1) this.table.filters = this.table.filters.filter(filter => filter.id !== id) } } ================================================ FILE: src/lib/src/client/handlers/PageHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/client' export default class PageHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public goto(page: number) { if (this.table.rowsPerPage) { if (page >= 1 && page <= this.table.pageCount) { this.table.currentPage = page this.table['event'].dispatch('change') } } } public previous() { this.goto(this.table.currentPage - 1) } public next() { this.goto(this.table.currentPage + 1) } public last() { this.goto(this.table.pageCount) } } ================================================ FILE: src/lib/src/client/handlers/QueryHandler.svelte.ts ================================================ import type { Field, Check, TableHandler } from '$lib/src/client' import { isNotNull } from '$lib/src/client/core' export default class QueryHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(path: string[], value: unknown, check: Check, id: string) { this.table.setPage(1) this.table.queries = this.table.queries.filter(query => query.id !== id) this.table.queries.push({ path, value, check, id }) } public unset(id: string) { this.table.setPage(1) this.table.queries = this.table.queries.filter(query => query.id !== id) } } ================================================ FILE: src/lib/src/client/handlers/SearchHandler.svelte.ts ================================================ import { type TableHandler, type Field, type Criterion, check } from '$lib/src/client' export default class SearchHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(value: string, scope?: Field[]) { this.table.setPage(1) this.table['search'] = { value: value, scope: scope } } public recursive(value: string, scope?: Field[]) { this.table.setPage(1) this.table['search'] = { value: value, scope: scope, isRecursive: true } } public regex(pattern: string, scope?: Field[]) { this.table.setPage(1) this.table['search'] = { value: pattern, scope: scope, check: check.match } } public clear() { this.table.setPage(1) this.table['search'] = { value: '' } } } ================================================ FILE: src/lib/src/client/handlers/SelectHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/client' import { parse } from '$lib/src/client/core' export default class SelectHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(value: unknown) { if (this.table.selected.includes(value)) { this.table.selected = this.table.selected.filter((item) => item !== value) } else { this.table.selected = [value, ...this.table.selected] } } public all(scope: 'all' | 'currentPage') { const rows = (scope === 'currentPage') ? this.table.rows : this.table.allRows const { callback } = parse(this.table['selectBy']) const selection = rows.map(callback) if (scope === 'currentPage') { if (this.table.isAllSelected) { this.table.selected = this.table.selected.filter(item => selection.includes(item) === false) } else { this.table.selected = [...new Set([...selection, ...this.table.selected])] } } else { this.table.isAllSelected ? this.clear() : this.table.selected = selection } } public clear() { this.table.selected = [] } public getRows() { const { callback } = parse(this.table['selectBy']) return this.table['rawRows'].filter(row => { return this.table.selected.includes(callback(row as $state.Snapshot)) }) } } ================================================ FILE: src/lib/src/client/handlers/SortHandler.svelte.ts ================================================ import type { Field, TableHandler, SortParams } from '$lib/src/client' import { type Sort, parse, sort } from '$lib/src/client/core' export default class SortHandler { private backup : Sort[] private table : TableHandler constructor(table: TableHandler) { this.table = table this.backup = [] } public set(field: Field, id: string, params: SortParams = {}) { if (this.table['sort'].id !== id) { this.table['sort'].direction = null } if (this.table['sort'].direction === null || this.table['sort'].direction === 'desc') { this.asc(field, id, params) } else if (this.table['sort'].direction === 'asc') { this.desc(field, id, params) } } public asc(field: Field, id: string, { locales, options }: SortParams = {}) { if (!field) return const { callback, key } = parse(field, id) this.table['sort'] = { id, callback, direction: 'asc', key } this.table['rawRows'] = [...this.table['rawRows']].sort((x, y) => { const [a, b] = [callback(x as $state.Snapshot), callback(y as $state.Snapshot)] return sort.asc(a, b, locales, options) }) this.save({ id, callback, direction: 'asc' }) this.table.setPage(1) } public desc(field: Field, id: string, { locales, options }: SortParams = {}) { if (!field) return const { callback, key } = parse(field, id) this.table['sort'] = { id, callback, direction: 'desc', key } this.table['rawRows'] = [...this.table['rawRows']].sort((x, y) => { const [a, b] = [callback(x as $state.Snapshot), callback(y as $state.Snapshot)] return sort.desc(a, b, locales, options) }) this.save({ id, callback, direction: 'desc' }) this.table.setPage(1) } public clear() { this.backup = [] this.table['sort'] = {} } public restore() { for (const { key, callback, direction, id } of this.backup) { const field = (key ?? callback) as Field this[direction](field, id) } } private save(sort: Sort) { this.backup = this.backup.filter(item => item.id !== sort.id ) if (this.backup.length >= 3) { const [_, slot2, slot3] = this.backup this.backup = [slot2, slot3, sort] } else { this.backup = [...this.backup, sort] } } } ================================================ FILE: src/lib/src/client/index.ts ================================================ export { default as TableHandler } from './TableHandler.svelte' export { check } from './core' import type { Internationalization, Field } from '$lib/src/shared' export type { default as AdvancedFilterBuilder } from './builders/AdvancedFilterBuilder.svelte' export type { default as CalculationBuilder } from './builders/CalculationBuilder.svelte' export type { default as CSVBuilder } from './builders/CSVBuilder.svelte' export type { default as FilterBuilder } from './builders/FilterBuilder.svelte' export type { default as RecordFilterBuilder } from './builders/RecordFilterBuilder.svelte' export type { default as SearchBuilder } from './builders/SearchBuilder.svelte' export type { default as SortBuilder } from './builders/SortBuilder.svelte' export { default as RecordFilter } from './builders/RecordFilterBuilder.svelte' export { Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, type Row, type Field, type Internationalization, type ColumnView, type TableHandlerInterface, } from '$lib/src/shared' export type SortParams = { locales?: Intl.LocalesArgument, options?: Intl.CollatorOptions } export type Check = (entry: unknown, value: unknown) => boolean export type TableParams = { rowsPerPage?: number, selectBy?: Field, highlight?: boolean, i18n?: Internationalization, } export type Criterion = { value: unknown, check: Check } ================================================ FILE: src/lib/src/server/AbstractTableHandler.svelte.ts ================================================ import type { State, Sort, Filter, TableParams } from '$lib/src/server' import { EventDispatcher } from '$lib/src/shared' export default class AbstractTableHandler { protected selectBy ?: keyof Row protected event = new EventDispatcher() protected search = $state('') protected sort = $state<(Sort)>({}) public debounce : number public totalRows = $state(undefined) public isLoading = $state(false) public rowsPerPage = $state(10) public currentPage = $state(1) public filters = $state<(Filter)[]>([]) public filterCount = $derived(this.filters.length) public rows = $state([]) public rowCount = $derived<{total: number, start: number, end: number, selected: number}>(this.createRowCount()) public pages = $derived(this.createPages()) public pageCount = $derived(this.createPageCount()) public pagesWithEllipsis = $derived(this.createPagesWithEllipsis()) public selected = $state<(Row[keyof Row])[]>([]) public isAllSelected = $derived(this.createIsAllSelected()) public element = $state(undefined) public clientWidth = $state(1000) constructor(data: Row[], params: TableParams) { this.rows = data this.selectBy = params.selectBy as keyof Row ?? undefined this.totalRows = params.totalRows this.rowsPerPage = params.rowsPerPage ?? 10 this.debounce = params.debounce ?? 0 } public getState(): State { return { currentPage: this.currentPage, rowsPerPage: this.rowsPerPage, offset: this.rowsPerPage * (this.currentPage - 1), search: this.search, sort: this.sort.field ? this.sort : undefined, filters: this.filters.length > 0 ? this.filters : undefined, setTotalRows: (value: number) => this.totalRows = value } } private createPages() { if (!this.rowsPerPage || !this.totalRows) { return undefined } const pages = Array.from(Array(Math.ceil(this.totalRows / this.rowsPerPage))) return pages.map((_, i) => i + 1) } private createPageCount() { if (!this.pages) return undefined return this.pages.length } private createPagesWithEllipsis() { if (!this.pages) { return undefined } 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 < this.pageCount - 3) { return [ firstPage, ellipse, ...this.pages.slice(this.currentPage - 2, this.currentPage + 1), ellipse, lastPage ] } else { return [ firstPage, ellipse, ...this.pages.slice(this.pageCount - 5, this.pageCount) ] } } private createRowCount() { if (!this.rowsPerPage || !this.totalRows) { return { total: undefined, start: undefined, end: undefined, selected: this.selected.length } } return { total: this.totalRows, start: this.currentPage * this.rowsPerPage - this.rowsPerPage + 1, end: Math.min(this.currentPage * this.rowsPerPage, this.totalRows), selected: this.selected.length } } private createIsAllSelected() { if (this.rows.length === 0) { return false } const ids = this.rows.map(row => row[this.selectBy]) return ids.every(id => this.selected.includes(id)) } } ================================================ FILE: src/lib/src/server/TableHandler.svelte.ts ================================================ import AbstractTableHandler from './AbstractTableHandler.svelte' import FetchHandler from './handlers/FetchHandler.svelte' import SortHandler from './handlers/SortHandler.svelte' import SelectHandler from './handlers/SelectHandler.svelte' import PageHandler from './handlers/PageHandler.svelte' import SearchHandler from './handlers/SearchHandler.svelte' import FilterHandler from './handlers/FilterHandler.svelte' import ViewBuilder from '../shared/builders/ViewBuilder.svelte' import SearchBuilder from './builders/SearchBuilder.svelte' import SortBuilder from './builders/SortBuilder.svelte' import FilterBuilder from './builders/FilterBuilder.svelte' import type { Internationalization, Row, Field, State, ColumnView, TableParams } from '$lib/src/server' import type { TableHandlerInterface } from '$lib/src/shared' export default class TableHandler extends AbstractTableHandler implements TableHandlerInterface { private fetchHandler : FetchHandler private sortHandler : SortHandler private selectHandler : SelectHandler private pageHandler : PageHandler private searchHandler : SearchHandler private filterHandler : FilterHandler private view : ViewBuilder public i18n : Internationalization constructor(data: T[] = [], params: TableParams = { rowsPerPage: 5 }) { super(data, params) this.i18n = this.translate(params.i18n) this.fetchHandler = new FetchHandler(this) this.sortHandler = new SortHandler(this) this.selectHandler = new SelectHandler(this) this.pageHandler = new PageHandler(this) this.searchHandler = new SearchHandler(this) this.filterHandler = new FilterHandler(this) } public load(callback: (state: State) => Promise): void { this.fetchHandler.set(callback) } public invalidate(): void { this.fetchHandler.invalidate() } public setRowsPerPage(value: number) { this.rowsPerPage = value this.setPage(1) } public setPage(value: number | 'previous' | 'next' | 'last'): void { switch (value) { case 'previous' : return this.pageHandler.previous() case 'next' : return this.pageHandler.next() case 'last' : return this.pageHandler.goto(this.pageCount) default : return this.pageHandler.goto(value as number) } } public clearSearch(): void { this.searchHandler.clear() } public createSearch(): SearchBuilder { return new SearchBuilder(this) } public createSort(field: Field): SortBuilder { if (typeof field === 'function') { throw new Error(`Invalid field argument: ${String(field)}. Function type arguments are not allowed in server-side mode`) } return new SortBuilder(this.sortHandler, field) } public clearFilters(): void { this.filterHandler.clear() this.invalidate() } public createFilter(field: Field): FilterBuilder { if (typeof field === 'function') { throw new Error(`Invalid field argument: ${String(field)}. Function type arguments are not allowed in server-side mode`) } return new FilterBuilder(this.filterHandler, field) } public select(value: T[keyof T]) { this.selectHandler.set(value) } public selectAll(): void { this.selectHandler.all() } public clearSelection(): void { this.selectHandler.clear() } public on(event: 'change' | 'clearFilters' | 'clearSearch', callback: () => void): void { this.event.add(event, callback) } public createView(columns: ColumnView[]): ViewBuilder { this.view = new ViewBuilder(this, columns) return this.view } public getView(): ViewBuilder { return this.view } private translate(i18n: Internationalization): Internationalization { 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 } } } ================================================ FILE: src/lib/src/server/builders/FilterBuilder.svelte.ts ================================================ import type FilterHandler from '../handlers/FilterHandler.svelte' export default class FilterBuilder { public value = $state('') private timeout = undefined private filterHandler : FilterHandler private field : keyof Row constructor(filterHandler: FilterHandler, field: keyof Row) { this.filterHandler = filterHandler this.field = field this.cleanup() } public set() { this.filterHandler.set(this.value, this.field) clearTimeout(this.timeout) this.timeout = setTimeout( () => { this.filterHandler['table'].setPage(1) }, 400) } public init(value?: string) { if (!value) return this this.value = value this.filterHandler.set(this.value, this.field) return this } public clear() { this.value = '' this.filterHandler.unset(this.field) } private cleanup() { this.filterHandler['table'].on('clearFilters', () => this.clear()) } } ================================================ FILE: src/lib/src/server/builders/SearchBuilder.svelte.ts ================================================ import type TableHandler from '../TableHandler.svelte' import type { SearchInterface } from '$lib/src/shared' export default class SearchBuilder implements SearchInterface { public value = $state('') private timeout = undefined private table : TableHandler constructor(table: TableHandler) { this.table = table this.cleanup() } public set() { this.table['search'] = this.value clearTimeout(this.timeout) this.timeout = setTimeout( () => { this.table.setPage(1) }, 400) } public init(value?: string) { if (!value) return this this.value = value this.table['search'] = value return this } public clear() { this.value = '' this.table['search'] = '' // this.table.invalidate() this.table.setPage(1) } private cleanup() { this.table.on('clearSearch', () => this.clear()) } } ================================================ FILE: src/lib/src/server/builders/SortBuilder.svelte.ts ================================================ import type SortHandler from '../handlers/SortHandler.svelte' import type { SortInterface } from '$lib/src/shared' export default class SortBuilder implements SortInterface { private sortHandler : SortHandler private field : keyof Row public isActive = $derived(this.createIsActive()) public direction = $derived<'asc' | 'desc'>(this.createDirection()) constructor(sortHandler: SortHandler, field: keyof Row) { this.sortHandler = sortHandler this.field = field } public set() { this.sortHandler.set(this.field) } public init(direction?: 'asc' | 'desc') { if (!direction) return this this[direction]() return this } public asc() { this.sortHandler.asc(this.field) } public desc() { this.sortHandler.desc(this.field) } private createIsActive() { if (this.field === this.sortHandler['table']['sort']?.field) { return true } return false } private createDirection() { if (this.isActive === false) return null return this.sortHandler['table']['sort']?.direction } } ================================================ FILE: src/lib/src/server/handlers/FetchHandler.svelte.ts ================================================ import type { State, TableHandler } from '$lib/src/server' export default class FetchHandler { private table: TableHandler private reload: (state: State) => Promise private timeout: NodeJS.Timeout constructor(table: TableHandler) { this.table = table } public set(callback: (state: State) => Promise) { this.reload = callback } public async invalidate() { if (!this.reload) return clearTimeout(this.timeout) this.timeout = setTimeout(() => this.trigger(), this.table.debounce) } private async trigger() { this.table.isLoading = true const state = this.table.getState() const data = await this.reload(state) this.table.isLoading = false if (data) { this.table.rows = data } } } ================================================ FILE: src/lib/src/server/handlers/FilterHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/server' export default class FilterHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(value: string | number, field: keyof Row) { this.table.filters = this.table.filters.filter(filter => filter.field !== field && filter.value) if (value) { this.table.filters.push({ value, field }) } } public unset(field: keyof Row) { this.table.filters = this.table.filters.filter(filter => filter.field !== field) } public clear() { this.table.filters = [] this.table['event'].dispatch('clearFilters') } } ================================================ FILE: src/lib/src/server/handlers/PageHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/server' export default class PageHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public goto(number: number) { if (this.table.rowsPerPage && this.table.totalRows) { if (number >= 1 && number <= this.table.pageCount) { this.table.currentPage = number this.table['event'].dispatch('change') this.table.invalidate() } } else { if (number >= 1) { this.table.currentPage = number this.table['event'].dispatch('change') this.table.invalidate() } } } public previous() { this.goto(this.table.currentPage - 1) } public next() { this.goto(this.table.currentPage + 1) } } ================================================ FILE: src/lib/src/server/handlers/SearchHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/server' export default class SearchHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public clear() { this.table['event'].dispatch('clearSearch') } } ================================================ FILE: src/lib/src/server/handlers/SelectHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/server' export default class SelectHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(value: Row[keyof Row]) { if (this.table.selected.includes(value)) { this.table.selected = this.table.selected.filter((item) => item !== value) } else { this.table.selected = [value, ...this.table.selected] } } public all() { const selection = this.table.rows.map((row) => row[this.table['selectBy']]) if (this.table.isAllSelected) { this.table.selected = this.table.selected.filter(item => selection.includes(item) === false) } else { this.table.selected = [...new Set([...selection, ...this.table.selected])] } } public clear() { this.table.selected = [] } } ================================================ FILE: src/lib/src/server/handlers/SortHandler.svelte.ts ================================================ import type { TableHandler } from '$lib/src/server' export default class SortHandler { private table: TableHandler constructor(table: TableHandler) { this.table = table } public set(field: keyof Row) { const sort = this.table['sort'] if(!sort || sort.field !== field) { this.asc(field) } else if (sort.direction === 'asc') { this.desc(field) } else if (sort.direction === 'desc') { this.asc(field) } } public asc(field: keyof Row) { this.table['sort'] = { field, direction: 'asc' } this.table.setPage(1) } public desc(field: keyof Row) { this.table['sort'] = { field, direction: 'desc' } this.table.setPage(1) } } ================================================ FILE: src/lib/src/server/index.ts ================================================ export { default as TableHandler } from './TableHandler.svelte' import type { Row, Internationalization } from '$lib/src/shared/index.js' export { Datatable, Search, RowsPerPage, Th, ThSort, ThFilter, Pagination, RowCount, type Row, type Field, type ColumnView, type Internationalization, type TableHandlerInterface } from '$lib/src/shared' export type State = { currentPage : number, rowsPerPage : number, offset : number, search ?: string, sort ?: Sort filters ?: Filter[] setTotalRows: (value: number) => void } export type TableParams = { rowsPerPage ?: number, totalRows ?: number, selectBy ?: keyof Row, debounce ?: number i18n ?: Internationalization } export type Filter = { field: keyof Row value?: unknown } export type Sort = { field?: keyof Row direction?: 'asc' | 'desc' } ================================================ FILE: src/lib/src/shared/Datatable.svelte ================================================
{#if header} {@render header()} {:else if basic === true} {/if}
{@render children()}
{#if footer} {@render footer()} {:else if basic === true} {/if}
================================================ FILE: src/lib/src/shared/EventDispatcher.ts ================================================ export default class EventDispatcher { private listeners = { change : [] as (() => void)[], clearFilters: [] as (() => void)[], clearSearch : [] as (() => void)[] } private queue: Set = new Set() public add(event: keyof EventDispatcher['listeners'], callback: () => void) { this.listeners[event].push(callback) } public async dispatch(event: keyof EventDispatcher['listeners']) { this.queue.add(event) await new Promise((resolve) => setTimeout(resolve, 40)) if (this.queue.size > 0) { this.run() } this.queue.clear() } private run() { for (const event of this.queue) { for (const callback of this.listeners[event]) { callback() } } // console.log(this.queue) } } ================================================ FILE: src/lib/src/shared/Pagination.svelte ================================================
{#if table.pages === undefined} {@render nopage()} {:else if table.clientWidth < 600} {@render small()} {:else} {@render ellipsis()} {/if}
{#snippet nopage()} {/snippet} {#snippet small()} {/snippet} {#snippet ellipsis()} {#each table.pagesWithEllipsis as page} {/each} {/snippet} ================================================ FILE: src/lib/src/shared/RowCount.svelte ================================================ {#snippet selectedRows()} {selected} {#if total} of {total} {/if} row(s) selected. {#if selected > 0} {/if} {/snippet} {#snippet small()} {#if total > 0} {start}- {end}/ {total} {:else} {table.i18n.noRows} {/if} {/snippet} {#snippet rowCount()} {#if total > 0} {@html table.i18n.rowCount .replace('{start}', `${start}`) .replace('{end}', `${end}`) .replace('{total}', `${total}`)} {:else} {table.i18n.noRows} {/if} {/snippet} ================================================ FILE: src/lib/src/shared/RowsPerPage.svelte ================================================ ================================================ FILE: src/lib/src/shared/Search.svelte ================================================ search.set()} placeholder={table.i18n.search} spellcheck="false" /> ================================================ FILE: src/lib/src/shared/Th.svelte ================================================ {@render children?.()} ================================================ FILE: src/lib/src/shared/ThFilter.svelte ================================================ filter.set()} /> ================================================ FILE: src/lib/src/shared/ThSort.svelte ================================================ sort.set()} class:active={sort.isActive}>
{@render children()}
================================================ FILE: src/lib/src/shared/builders/HighlightBuilder.svelte.ts ================================================ import type { TableHandlerInterface } from '$lib/src/shared' import { stringify } from '$lib/src/client/core' export default class HighlightBuilder { public selector : string private table : TableHandlerInterface private interval : NodeJS.Timeout private keywords = $derived(this.createKeywords()) constructor(table: TableHandlerInterface, selector: string = 'tbody') { this.table = table this.selector = selector this.interval = setInterval(() => this.createHighlight(), 200) } private createHighlight() { if (!this.table?.element) { return } clearInterval(this.interval) const node = this.table.element.querySelector(this.selector) this.table.on('change', () => { // this.reset(node) this.emphasize(node) }) } private createKeywords(): string[] { return [ this.table['search'].value ?? null, // ...this.table.filters.map(filter => filter.value) ].filter(Boolean) } private emphasize(node: Node) { if (this.keywords.length === 0) return if (node.nodeType === Node.ELEMENT_NODE) { for (const child of node.childNodes) { if (child.nodeName !== 'EM') { this.emphasize(child) } } } else if (node.nodeType === Node.TEXT_NODE) { for (const keyword of this.keywords) { const index = stringify(node.nodeValue).indexOf(keyword) if (index > -1) { const em = document.createElement('em') em.classList.add('highlight') const mid = (node as Text).splitText(index) mid.splitText(keyword.length) mid.parentNode.insertBefore(em, mid) mid.parentNode.removeChild(mid) em.appendChild(mid) } } } } } ================================================ FILE: src/lib/src/shared/builders/ViewBuilder.svelte.ts ================================================ import type { TableHandlerInterface, ColumnView } from '$lib/src/shared' export default class ViewBuilder { public columns = $state([]) private table : TableHandlerInterface private interval : NodeJS.Timeout private mutation : MutationObserver constructor(table: TableHandlerInterface, columns: ColumnView[]) { this.table = table this.columns = [] this.interval = setInterval(() => this.createColumns(columns), 200) } public toggle(name: string) { if (!this.table.element) return const column = this.columns.find(column => column.name === name) if (!column) return column.toggle() } private createColumns(columns: ColumnView[]) { if (!this.table?.element) { return } clearInterval(this.interval) this.columns = columns.map(({ name, index, isVisible, isFrozen }) => { return { name, index, isVisible: isVisible === false ? false : true, isFrozen: isFrozen === true ? true : false, element: this.table.element, toggle: function() { this.isVisible = !this.isVisible this.element.querySelectorAll(`tr > *:nth-child(${this.index + 1})`).forEach((element: HTMLElement) => { element.classList.toggle('hidden') }) } } }) this.preset() this.mutation = new MutationObserver(() => { setTimeout(() => { this.preset() }, 2) }) this.mutation.observe(this.table.element, { childList: true, subtree: true }) } private preset() { let left = 0 for (const { isVisible, isFrozen, index } of this.columns) { if (isFrozen === true) { left += this.freeze(index, left) } if (isVisible === false) { this.table.element.querySelectorAll(`tr > *:nth-child(${index + 1})`).forEach((element: HTMLElement) => { element.classList.add('hidden') }) } } } private freeze(index: number, left = 0) { const column = this.table.element.querySelector(`thead th:nth-child(${index + 1})`) as HTMLElement const { width } = column.getBoundingClientRect() this.table.element.querySelectorAll(`tr > *:nth-child(${index + 1})`).forEach((element: HTMLElement) => { element.style.position = 'sticky' element.style.left = left + 'px' element.style.width = width + 'px' }) return width } public setPosition(current: number, destination: number) { this.table.element.querySelectorAll('tr').forEach(row => { const cells = [].slice.call(row.querySelectorAll('th, td')) if (current > destination) { cells[destination].parentNode.insertBefore( cells[current], cells[destination] ) } else { cells[destination].parentNode.insertBefore( cells[current], cells[destination].nextSibling ) } }) } } ================================================ FILE: src/lib/src/shared/clsx/Datatable.svelte ================================================
{#if header} {@render header()} {:else if basic === true} {/if}
{@render children()}
{#if footer} {@render footer()} {:else if basic === true} {/if}
================================================ FILE: src/lib/src/shared/clsx/Pagination.svelte ================================================
{#if table.pages === undefined} {@render nopage()} {:else if table.clientWidth < 600} {@render small()} {:else} {@render ellipsis()} {/if}
{#snippet nopage()} {/snippet} {#snippet small()} {/snippet} {#snippet ellipsis()} {#each table.pagesWithEllipsis as page} {/each} {/snippet} ================================================ FILE: src/lib/src/shared/clsx/ThSort.svelte ================================================ sort.set()} class={{ active: sort.isActive }}>
{@render children()}
================================================ FILE: src/lib/src/shared/index.ts ================================================ export { default as Datatable } from './Datatable.svelte' export { default as Pagination } from './Pagination.svelte' export { default as RowCount } from './RowCount.svelte' export { default as Search } from './Search.svelte' export { default as Th } from './Th.svelte' export { default as ThSort } from './ThSort.svelte' export { default as ThFilter } from './ThFilter.svelte' export { default as RowsPerPage } from './RowsPerPage.svelte' export { default as EventDispatcher } from './EventDispatcher' import type { Check } from '$lib/src/client' export type Row = { [key: string | number | symbol]: any } export type Field = keyof Row | ((row: Row) => unknown) export interface TableHandlerInterface { clientWidth : number, element : HTMLElement, pages : readonly number[], pagesWithEllipsis : readonly number[], currentPage : number, pageCount : number, i18n : Internationalization, rowCount : { selected: number, start: number, end: number, total: number } rowsPerPage : number, clearSelection() : void, createSearch(): SearchInterface, createFilter(field: Field, check?: Check): FilterInterface, createSort(field: Field): SortInterface, setPage(value?: number | 'previous' | 'next' | 'last'): void, on(event: string, callback: () => void): void } export type Internationalization = { search ?: string, show ?: string, entries ?: string, filter ?: string, rowCount ?: string, noRows ?: string, previous ?: string, next ?: string } export type ColumnView = { index : number, name ?: string, isVisible ?: boolean, isFrozen ?: boolean, toggle ?: () => void } export interface SearchInterface { value: string, set: () => void, init: (value: string) => SearchInterface } export interface FilterInterface { value: unknown, set: () => void, init: (value: unknown) => FilterInterface } export interface SortInterface { isActive: boolean, direction: 'asc' | 'desc', set: () => void, init: (direction?: 'asc' | 'desc') => SortInterface } ================================================ FILE: src/lib/style.css ================================================ .svelte-simple-datatable table { border-collapse: separate; border-spacing: 0; width: 100%; background: inherit; } .svelte-simple-datatable table thead { position: sticky; inset-block-start: 0; background: inherit; z-index: 1; } .svelte-simple-datatable thead tr { background: inherit; } .svelte-simple-datatable thead tr th { background: inherit; } .svelte-simple-datatable thead tr:first-child th { padding: 8px 20px; background: inherit; } .svelte-simple-datatable tbody { background: inherit; } .svelte-simple-datatable tbody tr { transition: background, 0.2s; background: inherit; } .svelte-simple-datatable tbody tr:hover { background: var(--grey-lighten-3, #fafafa); } .svelte-simple-datatable tbody td { padding: 4px 20px; border-right: 1px solid var(--grey-lighten, #eee); border-bottom: 1px solid var(--grey-lighten, #eee); background: inherit; } .svelte-simple-datatable tbody td:last-child { border-right: none; } .svelte-simple-datatable u.highlight { text-decoration: none; background: rgba(251, 192, 45, 0.6); border-radius: 2px; } .svelte-simple-datatable footer.divider { border-top: 1px solid var(--grey, #e0e0e0); } ================================================ FILE: src/routes/+layout.svelte ================================================
{@render children()}
================================================ FILE: src/routes/+page.svelte ================================================ svelte simple datatables

svelte simple datatables

A powerful toolkit for building datatable components.

Streamline your data workflow with a robust API providing advanced features while reducing code complexity.

================================================ FILE: src/routes/Description.svelte ================================================
  • {@html checked} Headless solution
  • {@html checked} SSR friendly
  • {@html checked} Typescript support
  • {@html checked} No dependency
================================================ FILE: src/routes/Header.svelte ================================================
================================================ FILE: src/routes/Header_Github.svelte ================================================ {#if isMobile} Github↗ {:else} {/if} ================================================ FILE: src/routes/Header_MobileNav.svelte ================================================ {#if show} {/if} ================================================ FILE: src/routes/Header_Mode.svelte ================================================ {#if isMobile} {:else} {/if} ================================================ FILE: src/routes/Header_Theme.svelte ================================================ {#if isMobile} {:else} {/if} ================================================ FILE: src/routes/Header_Version.svelte ================================================ {#snippet content()} {/snippet} ================================================ FILE: src/routes/about/+layout.svelte ================================================