├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── assets └── svgs │ └── arrow.svg ├── css └── styles.css ├── dist ├── action-table-filters.js ├── action-table-filters.js.map ├── action-table-pagination.js ├── action-table-pagination.js.map ├── action-table-switch.js ├── action-table-switch.js.map ├── action-table.css ├── action-table.js ├── action-table.js.map ├── index.html ├── index.js ├── index.js.map ├── main.js └── main.js.map ├── docs ├── action-table-filters.js ├── action-table-filters.js.map ├── action-table-pagination.js ├── action-table-pagination.js.map ├── action-table-switch.js ├── action-table-switch.js.map ├── action-table.css ├── action-table.js ├── action-table.js.map ├── index.html ├── index.js ├── index.js.map ├── main.js └── main.js.map ├── huge.html ├── index.html ├── large.html ├── notes.md ├── package.json ├── pnpm-lock.yaml ├── small.html ├── src ├── action-table-filter-menu.ts ├── action-table-filter-range.ts ├── action-table-filter-switch.ts ├── action-table-filters.ts ├── action-table-no-results.ts ├── action-table-pagination-options.ts ├── action-table-pagination.ts ├── action-table-switch.ts ├── action-table.ts ├── global.ts ├── main.ts ├── random-number.ts └── types.ts ├── test.html ├── tsconfig.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 8 | parserOptions: { 9 | ecmaVersion: "latest", 10 | sourceType: "module", 11 | }, 12 | rules: { 13 | "no-console": "warn", 14 | }, 15 | parser: "@typescript-eslint/parser", 16 | plugins: ["@typescript-eslint"], 17 | root: true, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | ## npm 9 | /node_modules/ 10 | /npm-debug.log 11 | 12 | ## testing 13 | /coverage/ 14 | 15 | ## temp folders 16 | /.tmp/ 17 | 18 | # build 19 | /out-tsc/ 20 | 21 | # Local Netlify folder 22 | .netlify 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | .hintrc 3 | .prettierrc 4 | index.html 5 | pnpm-lock.yaml 6 | public 7 | src 8 | tsconfig.json 9 | vite.config.ts 10 | .github/ 11 | docs -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "printWidth": 180 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Colin Fahrion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Action Table Web Component 2 | 3 | Native HTML web component for adding sort functionality and filtering to HTML tables. This component does not use the Shadow DOM. Instead it includes a custom css stylesheet you can use, which you can override or customize. 4 | 5 | Check out the [Demo Page](https://colinaut.github.io/action-table/) 6 | 7 | ## Installation 8 | 9 | ### Main files 10 | 11 | * `dist/index.js` the main file installs action-table.js and action-table-filters.js. You can also install the components separately: 12 | * `dist/action-table.js` automatic sort headers and public accessible filtering functions 13 | * `dist/action-table-filters.js` includes action-table-filters wrapper which adds event listeners for form elements; also includes the action-table-filter-menu and action-table-filter-switch components. 14 | * `dist/action-table-pagination.js` includes action-table-pagination for displaying pagination buttons and action-table-pagination-options for a menu to allow users to adjust the number of rows displayed per page. 15 | * `dist/action-table-switch.js` optional action-table-switch element. Not installed by default index.js. 16 | * `dist/action-table.css` optional but recommended stylesheet. You can override the styles or write your own. 17 | 18 | ### CDN 19 | ``` 20 | 21 | 22 | 23 | 24 | ``` 25 | 26 | ### NPM 27 | 28 | ``` 29 | npm i @colinaut/action-table 30 | 31 | pnpm i @colinaut/action-table 32 | ``` 33 | 34 | Then `import "@colinaut/action-table";` into your main script. You may also load the CSS styles or write your own. 35 | 36 | ### Eleventy static site 37 | 38 | If you are using [Eleventy](https://www.11ty.dev), and want to install locally rather than rely on the CDN, you can install via NPM/PNPM and then pass through the JS and CSS files so that it is included in the output. Then you would just need to add it to the head. 39 | 40 | ``` 41 | eleventyConfig.addPassthroughCopy({ 42 | "node_modules/@colinaut/action-table/dist/*.js": "js/action-table", 43 | "node_modules/@colinaut/action-table/dist/action-table.css": "css", 44 | }) 45 | ``` 46 | ``` 47 | 48 | 49 | 50 | ``` 51 | 52 | ## Action Table 53 | 54 | To use the `` component you must wrap it around a HTML table. The table must include both a thead and a tbody for the main sortable rows. Action Table does not work where columns span more than one column. 55 | 56 | The component will automatically make the table sortable based on these header cells, using the innerText of the "thead th" as the column name. If for some reason you do not want a column to be sortable then add `no-sort` attribute to the th. 57 | 58 | The column names are important if you are using the filter function. If the header cell contains a SVG rather than text then it will use the SVG title attribute. If you want to supply a different column name you can add a `data-col` attribute to the th. 59 | 60 | **Accessibility Optimizations** 61 | 62 | When initialized, for accessibility action-table wraps the innerHTML in a button. When sorting it adds aria-sort the th with the sort direction. It also adds "sorted" class to the td when a column is sorted. 63 | 64 | **Content Editable cells** 65 | 66 | You can add `contenteditable` to any td and action-table will recognize this, any changes made by the users will update the table on blur (when the user tabs or clicks away). 67 | 68 | **Attributes:** 69 | 70 | * sort: Name of the default column to sort by. 71 | * direction: Set the direction "ascending" or "descending". _Defaults to ascending_. 72 | * store: Add the store attribute to have action-table store the sort and filters in localStorage so that it retains the state on reload. localStorage will override initial attributes. String added to the `store` attribute will be the name used for localStorage. _Defaults to 'action-table' if none specified_. 73 | * urlparams: Add the urlparams attribute to have the action-table grab sort and filters from url params on load. URL params will override sort and direction attributes, and localStorage. 74 | 75 | ### Sorting 76 | 77 | Sorting is done based on the cell value: numbers are sorted numerically, machine readable dates are sorted by date, and otherwise sorted as text. By adding the `store` attribute to the action-table it will store the sort and filters in localStorage. 78 | 79 | The sort order can also be set with a comma separated list in the `data-order` attribute on the th for the column. For instance, adding this attribute `data-order="Jan,Feb,Mar"` to the th will force the column to sort using that order as long as the cell values match. Non-matching cell values revert to base sort function. 80 | 81 | The cell value is determined by: 82 | 83 | * if there is a `data-sort` attribute on the td it uses it 84 | * Otherwise it uses td cell textContent 85 | * If there is no `data-sort` or textContent it will read: 86 | * SVGs using the SVG title attribute 87 | * Checkboxes based on checked status 88 | 89 | ### Filtering 90 | 91 | Filtering is done via the public filterTable() method. You can trigger this with javascript or better just use the action-table-filters element to set up controls. If the filter hides all results, the table automatically show a message indicating "No Results" along with a button to reset the filters. If on load all results are filtered out then it will automatically reset the filters. By adding the `store` attribute to the action-table it will store the sort and filters in localStorage. This protects against odd filter conditions in localStorage or URLparams. 92 | 93 | Filtering is based on the content of the cell much like sorting, except it uses a `data-filter` attribute: 94 | 95 | * if there is a `data-filter` attribute on the td it uses it 96 | * Otherwise it uses td cell textContent 97 | * If there is no `data-filter` or textContent it will read: 98 | * SVGs using the SVG title attribute 99 | * Checkboxes based on checked status 100 | 101 | ## Action Table Filters 102 | 103 | The `` is a wrapper element for filter menus and switches. In order for it to work it must live inside the `` element. You can add your own filters manually using select menus, checkboxes, or radio buttons. There are two special elements which does some the work for you: `` and ``. 104 | 105 | ### Action Table Filter Menu 106 | 107 | The menu defaults to a select menu but can be changed to checkboxes or radio buttons. On load this custom element automatically finds all unique values in the cells of the specified column and creates a menu with those options. You can also have columns where cells contain multiple values by including those options in span tags. If the th for the column includes `data-order` attribute it will use this when ordering the options list. Note, select menus and radio buttons have a minimum of 2 options and checkboxes have a minimum of 1 option (below the minimum it won't render). 108 | 109 | If you want to filter based on values different from the content then add `dataset-filter` attribute with the filter values to the td. This is useful for when you want to simplify the menu; for instance, when a cell that displays date and time but you only want to filter by the date. 110 | 111 | ``` 112 | 113 | ``` 114 | 115 | **Attributes:** 116 | 117 | * name: The name of the column to filter; or search entire table with name "action-table". 118 | * label: The label to display. _Defaults to the column name_. 119 | * all: Text used for "All" for first select menu option and first radio button that resets the filter. _Defaults to "All"_. 120 | * options: (Optional) To override the generated menu add a list of options as a comma delimited string. NOTE: If you set the name to "action-table" you must set options manually. 121 | * type: The menu type. _Defaults to 'select', can also be 'checkbox' or 'radio'_. 122 | * multiple: Adds multiple attributes to select menu. This has poor accessibility so it is recommended to use checkboxes instead. 123 | * exclusive: Only applies to checkboxes and multiple select menus. Add exclusive if you want multiple selections to be exclusive rather than the default inclusive. 124 | * regex: This will cause the table filter to use regex for matching (see Regex Filtering below). 125 | * exact: This will only use exact matches instead of includes. 126 | * descending: Reverses the order of the options. 127 | 128 | ### Action Table Filter Switch 129 | 130 | This custom element is used primarily for filtering columns that contain checkbox switches or the optional `action-table-switch` element. Though can also work with normal text based cells if you want a filter switch for a single value. It can be easily styled using the "switch" or "star" class defined in action-table.css styles. 131 | 132 | ``` 133 | 134 | ``` 135 | 136 | **Attributes:** 137 | 138 | * name: The name of the column to filter; or search entire table with name "action-table". 139 | * label: The label to display. _Defaults to the column name_. 140 | * value: (Optional) _defaults to checkbox.checked value of "on"_. 141 | 142 | ### Action Table Filter Range 143 | 144 | Custom dual range slider element for filtering by number ranges. Can only be used for columns that contain numbers only. It will automatically find the minimum and maximum values for the column. You can manually set min and max values by setting min and max attributes. This element relies heavily on the action-table.css styles. 145 | 146 | ``` 147 | 148 | ``` 149 | 150 | **Attributes:** 151 | 152 | * name: The name of the column to filter. 153 | * label: The label to display. _Defaults to the column name_. 154 | * min: (Optional) manually set min value for ranges. 155 | * max: (Optional) manually set value for ranges. 156 | 157 | ### Action Table Filter Manual Search Field 158 | 159 | Just add `` and action-table-filters will listen for input changes and filter the results. This uses "input" event as default but if you prefer blur then add `data-event="blur"` attribute to the input field. 160 | 161 | If you want to search every column then use `name="action-table"`. By default this searches every column. You can limit this search by adding the columns to be searched as a comma separated string as such to the search input element. `data-cols="column,column2"`. 162 | 163 | ### Action Table Filter Reset 164 | 165 | Just add a `` and action-table-filters will trigger a reset for all filters on button press. The reset button will automatically be enabled/disabled based on whether the table is filtered. 166 | 167 | ### Regex Filtering 168 | 169 | Filtering is handled using includes by default. You can use regex where regex is based on RegExp(value, "i") which allows you to get fancy with your filtering. This is useful for filtering [number ranges with regex](https://www.regex-range.com/). 170 | 171 | ### Manually making your own filters 172 | 173 | Any select menu, checkbox group, or radio button group can be created and the `` will make it active. 174 | 175 | * The select or input element must be named the same as the column name it is filtering, or named "action-table" if you want it to search entire row. 176 | * The values are the filter values. Any select option, checkbox, or radio button where value="" will reset the filter. 177 | * Checkboxes can be styled with "switch" or "star" by adding the class to a wrapping element. 178 | * Multiple selected checkboxes are inclusive by default unless you add the attribute 'exclusive' on a parent wrapper for the group. 179 | * You can add 'regex' attribute to the element or wrapping element to have it use regex. 180 | * Make your own range inputs using `data-range="min"` or `"data-range=max"`. The action-table-filters will listen for changes. 181 | 182 | ## Action Table No Results 183 | 184 | The `` element is for alerting when the table has no results due to filtering. It is a purely functional element that you supply the content to. It is hidden by default and shows when the table has no results. If you include a reset type button it will trigger the reset. 185 | 186 | **Example:** 187 | 188 | ``` 189 | 190 | No results found 191 | 192 | ``` 193 | 194 | ## Action Table Pagination 195 | 196 | To add pagination to any table add the number of visible rows with `pagination="10"` attribute on the action-table element. Then add the `` element within the action-table element. 197 | 198 | **Attributes:** 199 | 200 | * label: String that will be displayed as the pagination title. This attribute is special as it automatically replaces {rows} for the current rows range and {total} for the total rows. _Defaults to "Showing {rows} of {total}:"_ 201 | 202 | ## Action Table Pagination Options 203 | 204 | The `` element adds a select menu to allow users to change the number of pagination rows. 205 | 206 | **Attributes:** 207 | 208 | * options: (Required) comma separated list of numbers for the options menu 209 | * label: Select menu label text. _Defaults to "Rows per:"_ 210 | 211 | ## Action Table Switch 212 | 213 | The `` element is an optional element used to add toggle checkbox switches to the table. It is not added in the default index.js import. The action-table.css file includes "star" and "switch" classes for easy styling. On it's own it's not much different than just manually adding a checkbox to the table using the same styles. I've included it as a basic element for strapping functionality onto as I assume you'll want to do something when people click it. It fires off a custom event, `action-table-switch`, containing details about the element. You can also extend the ActionTableSwitch class and replace the sendEvent() function with your own. 214 | 215 | ``` 216 | 217 | ``` 218 | 219 | **Attributes:** 220 | 221 | * checked: Indicates checked status. 222 | * label: Sets the aria-label for the input. _Defaults to "switch"_. Be explicit for accessibility purposes. 223 | * value: Sets the value. 224 | * name: Sets the name. 225 | 226 | ## Custom Events 227 | 228 | The components use a number of custom events for reactively passing variables. You can tap into this if you want to add your own custom JavaScript. 229 | 230 | * action-table: The action-table element dispatches an 'action-table' event whenever it triggers a change in the pagination, numberOfPages, rowsVisible, or page variable. The action-table-pagination element listens for this events in order to update itself when the table is filtered or when the pagination attribute is changed. Event detail type is ActionTableEventDetail. 231 | * action-table-filter: The action-table element listens for this events in order to filter the table. The action-table-filters element dispatches this whenever a filter menu or input is changed. Event detail type is FiltersObject. 232 | * action-table-filters-reset: The action-table-filters element listens for this to reset all filters. The action-table-no-results dispatches this when the reset button is triggered. No event detail. 233 | * action-table-update: The action-table element listens for this event in order to update the content in a specific table cell. This is mainly useful for custom elements inside of table cells with dynamic content. Event detail type is UpdateContentDetail. 234 | 235 | ## CSS Variables 236 | 237 | The action-table.css includes some CSS variables for easy overrides. 238 | 239 | ``` 240 | action-table { 241 | --highlight: paleturquoise; 242 | --focus: dodgerblue; 243 | --star-checked: orange; 244 | --star-unchecked: gray; 245 | --switch-checked: green; 246 | --switch-unchecked: lightgray; 247 | --border: 1px solid lightgray; 248 | --border-radius: 0.2em; 249 | --th-bg: whitesmoke; 250 | --th-sorted: rgb(244, 220, 188); 251 | --col-sorted: rgb(255, 253, 240); 252 | --td-options-bg: whitesmoke; 253 | --page-btn: whitesmoke; 254 | --page-btn-active: rgb(244, 220, 188); 255 | } 256 | 257 | action-table-filter-range { 258 | --thumb-size: 1.3em; 259 | --thumb-bg: #fff; 260 | --thumb-border: solid 2px #9e9e9e; 261 | --thumb-shadow: 0 1px 4px 0.5px rgba(0, 0, 0, 0.25); 262 | --thumb-highlight: var(--highlight); 263 | --track-bg: lightgray; 264 | --track-shadow: inset 0 0 2px #00000099; 265 | --track-highlight: var(--highlight); 266 | --ticks-color: #b2b2b2; 267 | --ticks-width: 1; 268 | } 269 | ``` 270 | -------------------------------------------------------------------------------- /assets/svgs/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------- */ 2 | /* Action Table CSS */ 3 | /* -------------------------------------------------------------------------- */ 4 | 5 | action-table { 6 | display: block; 7 | --highlight: paleturquoise; 8 | --focus: dodgerblue; 9 | --star-checked: orange; 10 | --star-unchecked: gray; 11 | --switch-checked: green; 12 | --switch-unchecked: lightgray; 13 | --border: 1px solid lightgray; 14 | --border-radius: 0.2em; 15 | --th-bg: whitesmoke; 16 | --th-sorted: rgb(244, 220, 188); 17 | --col-sorted: rgb(255, 253, 240); 18 | --td-options-bg: whitesmoke; 19 | --page-btn: whitesmoke; 20 | --page-btn-active: rgb(244, 220, 188); 21 | --responsive-scroll-gradient: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0)), linear-gradient(to right, rgba(255, 255, 255, 0), #fff 70%) 0 100%, 22 | radial-gradient(farthest-side at 0% 50%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) 0 100%; 23 | } 24 | 25 | action-table :where(table) { 26 | border-collapse: collapse; 27 | margin: 1em 0; 28 | max-width: 100%; 29 | overflow: auto; 30 | display: block; 31 | /* Responsive scroll shadow gradient. Code from https://adrianroselli.com/2020/11/under-engineered-responsive-tables.html */ 32 | background: var(--responsive-scroll-gradient); 33 | background-repeat: no-repeat; 34 | background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%; 35 | background-position: 0 0, 100%, 0 0, 100%; 36 | background-attachment: local, local, scroll, scroll; 37 | } 38 | 39 | action-table :where(th) { 40 | border: var(--border); 41 | padding: 0; 42 | text-align: left; 43 | background: var(--th-bg); 44 | } 45 | 46 | action-table :where(th[no-sort]) { 47 | padding: 0.2em 0.5em 0.2em 0.5em; 48 | } 49 | 50 | action-table :where(th button) { 51 | cursor: pointer; 52 | font-weight: bold; 53 | border: 0; 54 | width: 100%; 55 | height: 100%; 56 | display: block; 57 | padding: 0.2em 1.5em 0.2em 0.5em; 58 | background-color: transparent; 59 | position: relative; 60 | text-align: left; 61 | font-size: inherit; 62 | line-height: inherit; 63 | } 64 | 65 | action-table :where(th button:hover, th:has(button):hover, th button:focus, th:has(button):focus) { 66 | background-color: var(--highlight); 67 | } 68 | 69 | action-table :where(th button)::after { 70 | content: ""; 71 | background-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath d='M9 16.172l-6.071-6.071-1.414 1.414 8.485 8.485 8.485-8.485-1.414-1.414-6.071 6.071v-16.172h-2z'%3E%3C/path%3E%3C/svg%3E%0A"); 72 | background-repeat: no-repeat; 73 | background-position: center right; 74 | background-size: 0.7em; 75 | height: 0.7em; 76 | width: 0.7em; 77 | display: block; 78 | opacity: 0.2; 79 | position: absolute; 80 | right: 0.4em; 81 | top: 50%; 82 | transform: translateY(-50%); 83 | float: right; 84 | transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; 85 | } 86 | 87 | action-table :where(th[aria-sort$="ing"] button, th[aria-sort$="ing"]:has(button)) { 88 | background-color: var(--th-sorted); 89 | } 90 | 91 | action-table :where(th[aria-sort$="ing"] button)::after { 92 | opacity: 1; 93 | } 94 | 95 | action-table :where(th[aria-sort="descending"] button)::after { 96 | opacity: 1; 97 | transform: translateY(-50%) rotate(180deg); 98 | } 99 | 100 | action-table :where(td) { 101 | border: var(--border); 102 | padding: 0.2em 0.4em; 103 | } 104 | 105 | action-table .sorted { 106 | background-color: var(--col-sorted); 107 | } 108 | 109 | /* Use spans for multiple values in cell */ 110 | action-table :where(td span) { 111 | background-color: var(--td-options-bg); 112 | padding: 0 0.4em 0.1em 0.4em; 113 | margin: 0 0.2em; 114 | } 115 | 116 | /* Hide custom elements by default as they only work with js loaded */ 117 | action-table:not(:defined), 118 | action-table-filters:not(:defined) { 119 | visibility: hidden; 120 | } 121 | 122 | action-table :where(select, input, button) { 123 | font-size: inherit; 124 | } 125 | action-table :where(input[type="search"], select) { 126 | border: var(--border); 127 | border-radius: var(--border-radius); 128 | } 129 | action-table .selected { 130 | background-color: var(--highlight); 131 | transition: color 0.2s ease; 132 | } 133 | 134 | action-table .no-results :where(td) { 135 | padding: 1em; 136 | text-align: center; 137 | } 138 | action-table :where(button) { 139 | cursor: pointer; 140 | } 141 | 142 | /* -------------------------------------------------------------------------- */ 143 | /* Action Table Filters */ 144 | /* -------------------------------------------------------------------------- */ 145 | 146 | /* -------------------------------------------------------------------------- */ 147 | /* Action Table Filter Elements */ 148 | /* -------------------------------------------------------------------------- */ 149 | 150 | action-table-filter-menu, 151 | action-table-filter-switch label, 152 | action-table-filter-menu .filter-label { 153 | display: inline-flex; 154 | flex-wrap: wrap; 155 | align-items: center; 156 | } 157 | action-table-filter-menu label, 158 | action-table-filter-menu .filter-label { 159 | margin-inline-end: 0.3em; 160 | } 161 | 162 | /* -------------------------------------------------------------------------- */ 163 | /* Switch Toggle */ 164 | /* -------------------------------------------------------------------------- */ 165 | 166 | action-table .switch label { 167 | display: inline-flex; 168 | align-items: center; 169 | margin: 0; 170 | } 171 | action-table .switch input { 172 | appearance: none; 173 | position: relative; 174 | display: inline-block; 175 | background: var(--switch-unchecked); 176 | cursor: pointer; 177 | height: 1.4em; 178 | width: 2.75em; 179 | vertical-align: middle; 180 | border-radius: 2em; 181 | box-shadow: 0px 1px 3px #0003 inset; 182 | transition: 0.25s linear background; 183 | } 184 | action-table .switch input::before { 185 | content: ""; 186 | display: block; 187 | width: 1em; 188 | height: 1em; 189 | background: #fff; 190 | border-radius: 1em; 191 | position: absolute; 192 | top: 0.2em; 193 | left: 0.2em; 194 | box-shadow: 0px 1px 3px #0003; 195 | transition: 0.25s linear transform; 196 | transform: translateX(0em); 197 | } 198 | action-table .switch :checked { 199 | background: var(--switch-checked); 200 | } 201 | action-table .switch :checked::before { 202 | transform: translateX(1.3em); 203 | } 204 | action-table .switch input:focus, 205 | action-table .star input:focus { 206 | outline: transparent; 207 | } 208 | action-table .switch input:focus-visible, 209 | action-table .star input:focus-visible { 210 | outline: 2px solid var(--focus); 211 | outline-offset: 2px; 212 | } 213 | 214 | /* -------------------------------------------------------------------------- */ 215 | /* Star Switch Toggle */ 216 | /* -------------------------------------------------------------------------- */ 217 | 218 | action-table .star input { 219 | appearance: none; 220 | position: relative; 221 | cursor: pointer; 222 | height: 1.6em; 223 | width: 1.6em; 224 | vertical-align: middle; 225 | border-radius: 0.3em; 226 | } 227 | action-table .star input::before { 228 | content: ""; 229 | background: var(--star-unchecked); 230 | cursor: pointer; 231 | position: absolute; 232 | height: 1.6em; 233 | width: 1.6em; 234 | vertical-align: middle; 235 | transition: 0.25s linear background; 236 | clip-path: polygon(50% 0%, 62% 29%, 98% 35%, 74% 58%, 79% 91%, 50% 76%, 21% 91%, 26% 58%, 2% 35%, 34% 29%); 237 | } 238 | 239 | action-table .star input:checked::before { 240 | background: var(--star-checked); 241 | } 242 | 243 | /* -------------------------------------------------------------------------- */ 244 | /* Action Table Pagination */ 245 | /* -------------------------------------------------------------------------- */ 246 | 247 | action-table-pagination { 248 | display: flex; 249 | justify-content: start; 250 | align-items: center; 251 | flex-wrap: wrap; 252 | gap: 0.6em; 253 | max-width: 100%; 254 | overflow: auto; 255 | } 256 | 257 | action-table-pagination .pagination-buttons { 258 | display: flex; 259 | justify-content: start; 260 | align-items: center; 261 | gap: 0.3em; 262 | } 263 | action-table-pagination button { 264 | cursor: pointer; 265 | font-size: inherit; 266 | background-color: var(--page-btn); 267 | border: 0; 268 | border-radius: 0.3em; 269 | padding: 0.2em 0.5em; 270 | } 271 | 272 | action-table-pagination button:hover { 273 | background-color: var(--highlight); 274 | } 275 | 276 | action-table-pagination .active { 277 | font-weight: bold; 278 | background-color: var(--page-btn-active); 279 | } 280 | 281 | @keyframes fade-in { 282 | from { 283 | opacity: 0; 284 | } 285 | } 286 | 287 | @keyframes fade-out { 288 | to { 289 | opacity: 0; 290 | } 291 | } 292 | 293 | @keyframes slide-from-bottom { 294 | from { 295 | transform: translateY(50px); 296 | } 297 | } 298 | 299 | @keyframes slide-to-top { 300 | to { 301 | transform: translateY(-50px); 302 | } 303 | } 304 | 305 | /* table { 306 | view-transition-name: row; 307 | } */ 308 | ::view-transition-old(row) { 309 | animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-top; 310 | } 311 | 312 | ::view-transition-new(row) { 313 | animation: 1210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-bottom; 314 | } 315 | 316 | /* -------------------------------------------------------------------------- */ 317 | /* Action Table Filter Range */ 318 | /* -------------------------------------------------------------------------- */ 319 | action-table-filter-range { 320 | --thumb-size: 1.3em; 321 | --thumb-bg: #fff; 322 | --thumb-border: solid 2px #9e9e9e; 323 | --thumb-shadow: 0 1px 4px 0.5px rgba(0, 0, 0, 0.25); 324 | --thumb-highlight: var(--highlight); 325 | --track-bg: lightgray; 326 | --track-shadow: inset 0 0 2px #00000099; 327 | --track-highlight: var(--highlight); 328 | --ticks-color: #b2b2b2; 329 | --ticks-width: 1; 330 | } 331 | action-table-filter-range, 332 | action-table-filter-range > div, 333 | action-table-filter-range label { 334 | display: flex; 335 | align-items: center; 336 | flex-wrap: wrap; 337 | gap: 0.6em; 338 | } 339 | 340 | /* ----------------- Action Table Filter Range Fancy Overlap ---------------- */ 341 | action-table-filter-range .range-slider-group { 342 | display: grid; 343 | } 344 | 345 | action-table-filter-range .range-slider-group > * { 346 | grid-column: 1; 347 | grid-row: 1; 348 | position: relative; 349 | } 350 | 351 | action-table-filter-range .range-slider-group::after { 352 | content: ""; 353 | width: 100%; 354 | height: 0.5em; 355 | background-color: var(--track-bg); 356 | border-radius: 50px; 357 | background-size: 100% 2px; 358 | box-shadow: var(--track-shadow); 359 | grid-column: 1; 360 | grid-row: 1; 361 | justify-self: center; 362 | } 363 | 364 | action-table-filter-range .range-slider-highlight { 365 | background-color: var(--track-highlight); 366 | width: 100%; 367 | height: 0.35em; 368 | } 369 | 370 | action-table-filter-range input { 371 | -webkit-appearance: none; 372 | appearance: none; 373 | margin: 0; 374 | width: 100%; 375 | background: transparent; 376 | padding: 0.2em 0; 377 | pointer-events: none; 378 | cursor: -webkit-grab; 379 | cursor: grab; 380 | } 381 | 382 | action-table-filter-range input::-webkit-slider-runnable-track { 383 | -webkit-appearance: none; 384 | appearance: none; 385 | background: transparent; 386 | } 387 | 388 | action-table-filter-range input::-webkit-slider-thumb { 389 | -webkit-appearance: none; 390 | height: var(--thumb-size); 391 | width: var(--thumb-size); 392 | border-radius: 50%; 393 | background: var(--thumb-bg); 394 | border: var(--thumb-border); 395 | box-shadow: var(--thumb-shadow); 396 | pointer-events: auto; 397 | } 398 | 399 | action-table-filter-range input::-moz-range-thumb { 400 | -webkit-appearance: none; 401 | height: var(--thumb-size); 402 | width: var(--thumb-size); 403 | background: var(--thumb-bg); 404 | border-radius: 50%; 405 | border: var(--thumb-border); 406 | box-shadow: var(--thumb-shadow); 407 | pointer-events: auto; 408 | } 409 | 410 | action-table-filter-range input::-ms-thumb { 411 | -webkit-appearance: none; 412 | height: var(--thumb-size); 413 | width: var(--thumb-size); 414 | background: var(--thumb-bg); 415 | border-radius: 50%; 416 | border: var(--thumb-border); 417 | box-shadow: var(--thumb-shadow); 418 | pointer-events: auto; 419 | } 420 | 421 | action-table-filter-range input::-webkit-slider-thumb:hover { 422 | background: var(--thumb-highlight); 423 | } 424 | 425 | action-table-filter-range svg { 426 | color: var(--ticks-color); 427 | position: relative; 428 | top: -0.6em; 429 | width: calc(100% - var(--thumb-size)); 430 | justify-self: center; 431 | border-left: 1px solid var(--ticks-color); 432 | border-right: 1px solid var(--ticks-color); 433 | box-sizing: border-box; 434 | } 435 | -------------------------------------------------------------------------------- /dist/action-table-filters.js: -------------------------------------------------------------------------------- 1 | var x=Object.defineProperty;var y=(g,p,e)=>p in g?x(g,p,{enumerable:!0,configurable:!0,writable:!0,value:e}):g[p]=e;var E=(g,p,e)=>(y(g,typeof p!="symbol"?p+"":p,e),e);class A extends HTMLElement{constructor(){super();E(this,"options",[])}findOptions(e){e=e.toLowerCase();const t=this.closest("action-table"),o=t.cols,n=t.tbody,s=o.findIndex(i=>i.name===e);if(s===-1)return;const h=`td:nth-child(${s+1})`,u=n.querySelectorAll(h);let a=[];Array.from(u).forEach(i=>{const l=i.querySelectorAll("span, ul > li");if(!i.dataset.filter&&(l==null?void 0:l.length)>0){const d=Array.from(l).map(f=>f.textContent||"");a=a.concat(d)}else a.push(t.getCellValues(i).filter)}),a=Array.from(new Set(a));const r=o[s].order;function c(i){return r!=null&&r.includes(i)?r.indexOf(i).toString():i}a.sort((i,l)=>(i=c(i),l=c(l),t.alphaNumSort(i,l))),this.hasAttribute("descending")&&a.reverse(),this.options=a}connectedCallback(){var t;const e=this.getAttribute("name");e&&(this.hasAttribute("options")?this.options=((t=this.getAttribute("options"))==null?void 0:t.split(","))||[]:this.findOptions(e),this.render(e))}render(e){if(this.options.length<1)return;const t=this.getAttribute("type")||"select";if(t!=="checkbox"&&this.options.length<2)return;const o=this.getAttribute("label")||e,n=this.hasAttribute("multiple")?"multiple":"",s=this.getAttribute("all")||"All";let h="",u="";const a=t==="select"?``:`${o}`;t==="select"&&(h=`"),t==="radio"&&(h=``);const r=`${a}${h}${this.options.map(c=>t==="select"?``:t==="radio"||t==="checkbox"?``:"").join("")}${u}`;this.innerHTML=`${r}`}}customElements.define("action-table-filter-menu",A);class L extends HTMLElement{connectedCallback(){const p=this.getAttribute("name");p&&(this.innerHTML=``)}}customElements.define("action-table-filter-switch",L);class T extends HTMLElement{constructor(){super();E(this,"min",0);E(this,"rangeTotal",0);this.render(),this.addEventListeners()}get name(){return this.getAttribute("name")||""}addEventListeners(){this.addEventListener("input",e=>{const t=this.querySelectorAll("input"),o=this.querySelector("output"),[n,s]=Array.from(t).map(r=>Number(r.value)),h=n.toString(),u=s.toString();o instanceof HTMLOutputElement&&(o.textContent=`${h}-${u}`);const a=this.querySelector(".range-slider-highlight");a instanceof HTMLSpanElement&&(a.style.marginLeft=`${(n-this.min)/this.rangeTotal*100}%`,a.style.width=`${(s-n)/this.rangeTotal*100}%`),n>s&&(e.stopPropagation(),t[0].value=u,t[1].value=h)})}findMinMax(){const e=this.getAttribute("min"),t=this.getAttribute("max");if(e&&t)return[Number(e),Number(t)];const o=this.closest("action-table"),n=o.cols,s=o.tbody,h=n.findIndex(r=>r.name===this.name.toLowerCase());if(h===-1)return[0,0];const u=`td:nth-child(${h+1})`,a=s.querySelectorAll(u);return Array.from(a).reduce((r,c)=>{const i=Number(o.getCellValues(c).filter);let l=r.length===2?r[0]:i,d=r.length===2?r[1]:i;return l=li?d:i,[l,d]},[])}render(){const[e,t]=this.findMinMax(),o=e.toString(),n=t.toString();this.rangeTotal=t-e,this.min=e;const s=this.getAttribute("label")||this.name,h=document.createElement("div");h.textContent=s;const u=document.createElement("div");u.classList.add("range-slider-group");const a=Math.pow(10,Math.round(Math.log10(this.rangeTotal)))/10;function r(m,b){for(const v in b)m.setAttribute(v,b[v])}const c=[e];for(let m=e+a;m<=t;m+=a)c.push(Math.round(m/a)*a);c.includes(t)||c.push(t);const i=document.createElementNS("http://www.w3.org/2000/svg","svg");r(i,{role:"presentation",width:"100%",height:"5"});const l=100/(c.length-1);for(let m=1;m0&&this.setFilterElements(e)}toggleHighlight(e){e.value?e.classList.add("selected"):e.classList.remove("selected")}addEventListeners(){var o;const e=(n,s)=>n.hasAttribute(s)||!!n.closest(`[${s}]`);this.addEventListener("input",n=>{const s=n.target;if(s instanceof HTMLSelectElement||s instanceof HTMLInputElement){const h=e(s,"exclusive"),u=e(s,"regex"),a=e(s,"exact"),r=s.dataset.cols?s.dataset.cols.toLowerCase().split(","):void 0,c=s.name.toLowerCase();if(s instanceof HTMLSelectElement){this.toggleHighlight(s);const i=Array.from(s.selectedOptions).map(l=>l.value);this.dispatch({[c]:{values:i,exclusive:h,regex:u,exact:a,cols:r}})}if(s instanceof HTMLInputElement){if(s.type==="checkbox"){const i=this.querySelectorAll("input[type=checkbox][name="+s.name+"]"),l=Array.from(i).filter(d=>d.checked).map(d=>d.value);this.dispatch({[c]:{values:l,exclusive:h,regex:u,exact:a,cols:r}})}if(s.type==="radio"&&this.dispatch({[c]:{values:[s.value],exclusive:h,regex:u,exact:a,cols:r}}),s.type==="range"){const i=this.querySelectorAll("input[type=range][name='"+s.name+"']");let l=[];const d=[];i.forEach(f=>{f.dataset.range==="min"&&(d[0]=f.min,l[0]=f.value),f.dataset.range==="max"&&(d[1]=f.max,l[1]=f.value)}),l.every((f,m)=>f===d[m])&&(l=[]),this.dispatch({[c]:{values:l,range:!0}})}}}}),this.querySelectorAll("input[type='search']").forEach(n=>{function s(u,a=300){let r;return(...c)=>{clearTimeout(r),r=setTimeout(()=>{u(...c)},a)}}const h=n.dataset.event||"input";n.addEventListener(h,()=>{this.toggleHighlight(n);const u=e(n,"exclusive"),a=e(n,"regex"),r=e(n,"exact"),c=n.dataset.cols?n.dataset.cols.toLowerCase().split(","):void 0;s(()=>this.dispatch({[n.name]:{values:[n.value],exclusive:u,regex:a,exact:r,cols:c}}))()})}),(o=this.resetButton)==null||o.addEventListener("click",()=>{this.resetAllFilterElements(),this.dispatch()}),this.actionTable.addEventListener("action-table-filters-reset",()=>{this.resetAllFilterElements()})}dispatchInput(e){e.dispatchEvent(new Event("input",{bubbles:!0}))}dispatch(e){if(this.resetButton)if(e){let t=this.actionTable.filters||{};t={...t,...e},this.enableReset(this.hasFilters(t))}else this.enableReset(!1);this.dispatchEvent(new CustomEvent("action-table-filter",{detail:e,bubbles:!0}))}enableReset(e=!0){this.resetButton&&(e?this.resetButton.removeAttribute("disabled"):this.resetButton.setAttribute("disabled",""))}hasFilters(e){return Object.keys(e).some(t=>e[t].values.some(o=>o!==""))}resetAllFilterElements(){this.querySelectorAll("select, input").forEach(t=>{t instanceof HTMLInputElement&&(t.type==="checkbox"||t.type==="radio")&&(t.value===""?t.checked=!0:t.checked=!1),(t instanceof HTMLSelectElement||t instanceof HTMLInputElement&&t.type==="search")&&(t.value="",this.toggleHighlight(t)),t instanceof HTMLInputElement&&t.type==="range"&&(t.value=t.dataset.range==="max"?t.max:t.min,this.dispatchInput(t))})}setFilterElements(e){this.hasFilters(e)?(this.enableReset(),Object.keys(e).forEach(t=>this.setFilterElement(t,e[t].values))):this.resetAllFilterElements()}setSelectValueIgnoringCase(e,t){t=t.toLowerCase(),Array.from(e.options).some(o=>(o.value.toLowerCase()||o.text.toLowerCase())===t?(o.selected=!0,this.toggleHighlight(e),!0):!1)}setFilterElement(e,t){if(t.length===0)return;this.querySelectorAll(`select[name="${e}" i], input[name="${e}" i]`).forEach(n=>{n instanceof HTMLSelectElement&&(n.value=t[0],this.setSelectValueIgnoringCase(n,t[0])),n instanceof HTMLInputElement&&(n.type==="checkbox"&&t.includes(n.value)&&(n.checked=!0),n.type==="radio"&&n.value===t[0]&&(n.checked=!0),n.type==="search"&&(n.value=t[0],this.toggleHighlight(n)),n.type==="range"&&(n.dataset.range==="min"&&(n.value=t[0]||n.min,this.dispatchInput(n)),n.dataset.range==="max"&&(n.value=t[1]||n.max,this.dispatchInput(n))))})}}customElements.define("action-table-filters",$); 5 | //# sourceMappingURL=action-table-filters.js.map 6 | -------------------------------------------------------------------------------- /dist/action-table-pagination.js: -------------------------------------------------------------------------------- 1 | var f=Object.defineProperty;var d=(r,a,s)=>a in r?f(r,a,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[a]=s;var c=(r,a,s)=>(d(r,typeof a!="symbol"?a+"":a,s),s);class m extends HTMLElement{constructor(){super();const a=this.closest("action-table"),{pagination:s}=a,t=i=>i.map(e=>``).join(""),n=this.options.length>0?``:"";this.innerHTML=n,this.addEventListener("change",i=>{if(i.target instanceof HTMLSelectElement){const e=Number(i.target.value);isNaN(e)||(a.pagination=e)}})}get options(){const a=this.getAttribute("options");if(a){const s=a.split(",").map(t=>Number(t)).filter(t=>!isNaN(t));if(s.length>0)return s}return[]}}customElements.define("action-table-pagination-options",m);class $ extends HTMLElement{constructor(){super();c(this,"page",1);c(this,"numberOfPages",1);c(this,"group",1);c(this,"maxGroups",1);c(this,"actionTable",this.closest("action-table"));c(this,"rowsVisible",0);this.addEventListeners()}connectedCallback(){this.render()}render(){const{page:s,numberOfPages:t}=this.actionTable;this.numberOfPages=t,this.page=s;const n=Number(this.getAttribute("max-buttons"))||10,i=Math.ceil(t/n);let e=this.group;e>i?e=i:e<1&&(e=1);const l=(e-1)*n+1;function p(o,b="",h){return``}let g="";if(e>1&&(g+=`${p(1,"first")}${p(l-1,"prev","...")}`),t>0){for(let o=l;o<=t;o++)if(g+=p(o),o!==t&&o>=n*e){g+=`${p(o+1,"next","...")}${p(t,"last")}`;break}}const u=o=>` class="pagination-${o}"`;this.innerHTML=` ${g}`,this.changeLabel(s),this.group=e,this.maxGroups=i}changeLabel(s){const{pagination:t,rowsSet:n}=this.actionTable;this.rowsVisible=n.size;const e=(this.getAttribute("label")||"Showing {rows} of {total}:").replace("{rows}",`${s*t-t+1}-${s*t}`).replace("{total}",`${n.size}`),l=this.querySelector("span.pagination-label");l&&(l.textContent=e)}addEventListeners(){this.addEventListener("click",s=>{const t=s.target;if(t instanceof HTMLButtonElement){let n=1;t.dataset.page&&(n=Number(t.dataset.page),t.classList.add("active"),this.querySelectorAll("button").forEach(l=>{l!==t&&l.classList.remove("active")}));let i=this.group;const e=l=>t.classList.contains(l);e("next")&&i++,e("prev")&&i--,e("first")&&(i=1),e("last")&&(i=this.maxGroups),this.actionTable.page=this.page=n,this.changeLabel(n),this.group!==i&&(this.group=i,this.render())}}),this.actionTable.addEventListener("action-table",s=>{const{page:t,pagination:n,numberOfPages:i,rowsVisible:e}=s.detail;(t&&t!==this.page||i!==void 0&&i!==this.numberOfPages||n!==void 0||e!==this.rowsVisible)&&this.render()})}}customElements.define("action-table-pagination",$); 2 | //# sourceMappingURL=action-table-pagination.js.map 3 | -------------------------------------------------------------------------------- /dist/action-table-pagination.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action-table-pagination.js","sources":["../src/action-table-pagination-options.ts","../src/action-table-pagination.ts"],"sourcesContent":["import type { ActionTable } from \"./action-table\";\n\nexport class ActionTablePaginationOptions extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst actionTable = this.closest(\"action-table\") as ActionTable;\n\t\tconst { pagination } = actionTable;\n\t\tconst paginationOptions = (options: number[]) => options.map((opt) => ``).join(\"\");\n\n\t\tconst paginationSelect =\n\t\t\tthis.options.length > 0\n\t\t\t\t? ``\n\t\t\t\t: \"\";\n\n\t\tthis.innerHTML = paginationSelect;\n\t\tthis.addEventListener(\"change\", (e) => {\n\t\t\tif (e.target instanceof HTMLSelectElement) {\n\t\t\t\tconst value = Number(e.target.value);\n\t\t\t\tif (!isNaN(value)) {\n\t\t\t\t\tactionTable.pagination = value;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tget options(): number[] {\n\t\tconst options = this.getAttribute(\"options\");\n\t\tif (options) {\n\t\t\tconst paginationArray = options\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((item) => Number(item))\n\t\t\t\t.filter((p) => !isNaN(p));\n\t\t\tif (paginationArray.length > 0) {\n\t\t\t\treturn paginationArray;\n\t\t\t}\n\t\t}\n\t\treturn [];\n\t}\n}\n\ncustomElements.define(\"action-table-pagination-options\", ActionTablePaginationOptions);\n","import type { ActionTable } from \"./action-table\";\nimport \"./action-table-pagination-options\";\n\nexport class ActionTablePagination extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.addEventListeners();\n\t}\n\n\tprivate page = 1;\n\tprivate numberOfPages = 1;\n\tprivate group = 1;\n\tprivate maxGroups = 1;\n\tprivate actionTable = this.closest(\"action-table\") as ActionTable;\n\tprivate rowsVisible = 0;\n\n\tpublic connectedCallback(): void {\n\t\tthis.render();\n\t}\n\n\tpublic render() {\n\t\tconsole.log(\"render pagination\");\n\n\t\tconst { page, numberOfPages } = this.actionTable;\n\t\t// reassign number of pages based on this.actionTable\n\t\tthis.numberOfPages = numberOfPages;\n\t\tthis.page = page;\n\t\t// temporarily local variables\n\t\tconst maxButtons = Number(this.getAttribute(\"max-buttons\")) || 10;\n\t\tconst maxGroups = Math.ceil(numberOfPages / maxButtons); // reassign to this at end of render\n\t\tlet group = this.group; // reassign to this at end of render\n\n\t\tif (group > maxGroups) {\n\t\t\tgroup = maxGroups;\n\t\t} else if (group < 1) {\n\t\t\tgroup = 1;\n\t\t}\n\n\t\tconst startIndex = (group - 1) * maxButtons + 1;\n\n\t\t/* -------------------------------------------------------------------------- */\n\t\t/* Render the buttons */\n\t\t/* -------------------------------------------------------------------------- */\n\n\t\t/* ----------------------------- Button strings ----------------------------- */\n\t\tfunction pageButton(i: number, className: string = \"\", text?: string): string {\n\t\t\treturn ``;\n\t\t}\n\n\t\t/* -------------------------- Start making buttons -------------------------- */\n\n\t\tlet paginatedButtons = \"\";\n\n\t\tif (group > 1) {\n\t\t\tpaginatedButtons += `${pageButton(1, \"first\")}${pageButton(startIndex - 1, \"prev\", \"...\")}`;\n\t\t}\n\n\t\tif (numberOfPages > 0) {\n\t\t\t// for looping through the number of pages\n\t\t\tfor (let i = startIndex; i <= numberOfPages; i++) {\n\t\t\t\t// code to handle each page\n\t\t\t\tpaginatedButtons += pageButton(i);\n\t\t\t\tif (i !== numberOfPages && i >= maxButtons * group) {\n\t\t\t\t\tpaginatedButtons += `${pageButton(i + 1, \"next\", \"...\")}${pageButton(numberOfPages, \"last\")}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst classAttr = (suffix: string) => ` class=\"pagination-${suffix}\"`;\n\n\t\tthis.innerHTML = ` ${paginatedButtons}`;\n\t\tthis.changeLabel(page);\n\n\t\t// assign temporary variables back to this\n\t\tthis.group = group;\n\t\tthis.maxGroups = maxGroups;\n\t}\n\n\tprivate changeLabel(page: number) {\n\t\tconst { pagination, rowsSet } = this.actionTable;\n\t\t// update rowsVisible from current set size\n\t\tthis.rowsVisible = rowsSet.size;\n\n\t\tconst label = this.getAttribute(\"label\") || \"Showing {rows} of {total}:\";\n\n\t\tconst labelStr = label.replace(\"{rows}\", `${page * pagination - pagination + 1}-${page * pagination}`).replace(\"{total}\", `${rowsSet.size}`);\n\n\t\tconst labelSpan = this.querySelector(\"span.pagination-label\");\n\t\tif (labelSpan) labelSpan.textContent = labelStr;\n\t}\n\n\tprivate addEventListeners(): void {\n\t\tthis.addEventListener(\"click\", (event) => {\n\t\t\tconst target = event.target;\n\t\t\tif (target instanceof HTMLButtonElement) {\n\t\t\t\t// temp variable\n\t\t\t\t// must trigger action-table page change if it changes\n\t\t\t\tlet page: number = 1;\n\n\t\t\t\tif (target.dataset.page) {\n\t\t\t\t\t// set the current page before setting the current page on the action table so that it doesn't rerender when setProps is returned\n\t\t\t\t\tpage = Number(target.dataset.page);\n\n\t\t\t\t\ttarget.classList.add(\"active\");\n\t\t\t\t\tthis.querySelectorAll(\"button\").forEach((button) => {\n\t\t\t\t\t\tif (button !== target) {\n\t\t\t\t\t\t\tbutton.classList.remove(\"active\");\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t// temp variables\n\t\t\t\t// Must rerender if the group changes\n\t\t\t\tlet group = this.group;\n\n\t\t\t\tconst hasClass = (className: string) => {\n\t\t\t\t\treturn target.classList.contains(className);\n\t\t\t\t};\n\t\t\t\tif (hasClass(\"next\")) {\n\t\t\t\t\tgroup++;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"prev\")) {\n\t\t\t\t\tgroup--;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"first\")) {\n\t\t\t\t\tgroup = 1;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"last\")) {\n\t\t\t\t\tgroup = this.maxGroups;\n\t\t\t\t}\n\n\t\t\t\tthis.actionTable.page = this.page = page;\n\t\t\t\tthis.changeLabel(page);\n\n\t\t\t\tif (this.group !== group) {\n\t\t\t\t\tthis.group = group;\n\t\t\t\t\tthis.render();\n\t\t\t\t}\n\t\t\t\t// }\n\t\t\t}\n\t\t});\n\n\t\tthis.actionTable.addEventListener(\"action-table\", (e) => {\n\t\t\tconst { page, pagination, numberOfPages, rowsVisible } = e.detail;\n\t\t\tconsole.log(\"action-table pagination\", e.detail);\n\t\t\tif (\n\t\t\t\t(page && page !== this.page) ||\n\t\t\t\t(numberOfPages !== undefined && numberOfPages !== this.numberOfPages) ||\n\t\t\t\tpagination !== undefined ||\n\t\t\t\trowsVisible !== this.rowsVisible\n\t\t\t) {\n\t\t\t\tconsole.log(\"action-table pagination render\", page, this.page, pagination, numberOfPages, this.numberOfPages);\n\t\t\t\tthis.render();\n\t\t\t}\n\t\t});\n\t}\n}\n\ncustomElements.define(\"action-table-pagination\", ActionTablePagination);\n"],"names":["ActionTablePaginationOptions","actionTable","pagination","paginationOptions","options","opt","paginationSelect","e","value","paginationArray","item","p","ActionTablePagination","__publicField","page","numberOfPages","maxButtons","maxGroups","group","startIndex","pageButton","i","className","text","paginatedButtons","classAttr","suffix","rowsSet","labelStr","labelSpan","event","target","button","hasClass","rowsVisible"],"mappings":"wKAEO,MAAMA,UAAqC,WAAY,CAC7D,aAAc,CACP,QACA,MAAAC,EAAc,KAAK,QAAQ,cAAc,EACzC,CAAE,WAAAC,CAAe,EAAAD,EACjBE,EAAqBC,GAAsBA,EAAQ,IAAKC,GAAQ,WAAWH,IAAeG,EAAM,WAAa,EAAE,IAAIA,CAAG,WAAW,EAAE,KAAK,EAAE,EAE1IC,EACL,KAAK,QAAQ,OAAS,EACnB,0CAA0C,KAAK,aAAa,OAAO,GAAK,WAAW,mBAAmBH,EAAkB,KAAK,OAAO,CAAC,oBACrI,GAEJ,KAAK,UAAYG,EACZ,KAAA,iBAAiB,SAAWC,GAAM,CAClC,GAAAA,EAAE,kBAAkB,kBAAmB,CAC1C,MAAMC,EAAQ,OAAOD,EAAE,OAAO,KAAK,EAC9B,MAAMC,CAAK,IACfP,EAAY,WAAaO,EAE3B,CAAA,CACA,CACF,CAEA,IAAI,SAAoB,CACjB,MAAAJ,EAAU,KAAK,aAAa,SAAS,EAC3C,GAAIA,EAAS,CACZ,MAAMK,EAAkBL,EACtB,MAAM,GAAG,EACT,IAAKM,GAAS,OAAOA,CAAI,CAAC,EAC1B,OAAQC,GAAM,CAAC,MAAMA,CAAC,CAAC,EACrB,GAAAF,EAAgB,OAAS,EACrB,OAAAA,CAET,CACA,MAAO,EACR,CACD,CAEA,eAAe,OAAO,kCAAmCT,CAA4B,ECrC9E,MAAMY,UAA8B,WAAY,CACtD,aAAc,CACP,QAICC,EAAA,YAAO,GACPA,EAAA,qBAAgB,GAChBA,EAAA,aAAQ,GACRA,EAAA,iBAAY,GACZA,EAAA,mBAAc,KAAK,QAAQ,cAAc,GACzCA,EAAA,mBAAc,GARrB,KAAK,kBAAkB,CACxB,CASO,mBAA0B,CAChC,KAAK,OAAO,CACb,CAEO,QAAS,CAGf,KAAM,CAAE,KAAAC,EAAM,cAAAC,GAAkB,KAAK,YAErC,KAAK,cAAgBA,EACrB,KAAK,KAAOD,EAEZ,MAAME,EAAa,OAAO,KAAK,aAAa,aAAa,CAAC,GAAK,GACzDC,EAAY,KAAK,KAAKF,EAAgBC,CAAU,EACtD,IAAIE,EAAQ,KAAK,MAEbA,EAAQD,EACHC,EAAAD,EACEC,EAAQ,IACVA,EAAA,GAGH,MAAAC,GAAcD,EAAQ,GAAKF,EAAa,EAO9C,SAASI,EAAWC,EAAWC,EAAoB,GAAIC,EAAuB,CAC7E,MAAO,gCAAgCT,IAASO,EAAI,UAAUC,CAAS,GAAK,GAAGA,CAAS,EAAE,gBAAgBD,CAAC,YAAYC,CAAS,KAAKC,GAAQF,CAAC,WAC/I,CAIA,IAAIG,EAAmB,GAMvB,GAJIN,EAAQ,IACSM,GAAA,GAAGJ,EAAW,EAAG,OAAO,CAAC,GAAGA,EAAWD,EAAa,EAAG,OAAQ,KAAK,CAAC,IAGtFJ,EAAgB,GAEnB,QAASM,EAAIF,EAAYE,GAAKN,EAAeM,IAG5C,GADAG,GAAoBJ,EAAWC,CAAC,EAC5BA,IAAMN,GAAiBM,GAAKL,EAAaE,EAAO,CAC/BM,GAAA,GAAGJ,EAAWC,EAAI,EAAG,OAAQ,KAAK,CAAC,GAAGD,EAAWL,EAAe,MAAM,CAAC,GAC3F,KACD,EAIF,MAAMU,EAAaC,GAAmB,sBAAsBA,CAAM,IAE7D,KAAA,UAAY,QAAQD,EAAU,OAAO,CAAC,iBAAiBA,EAAU,SAAS,CAAC,IAAID,CAAgB,UACpG,KAAK,YAAYV,CAAI,EAGrB,KAAK,MAAQI,EACb,KAAK,UAAYD,CAClB,CAEQ,YAAYH,EAAc,CACjC,KAAM,CAAE,WAAAZ,EAAY,QAAAyB,GAAY,KAAK,YAErC,KAAK,YAAcA,EAAQ,KAI3B,MAAMC,GAFQ,KAAK,aAAa,OAAO,GAAK,8BAErB,QAAQ,SAAU,GAAGd,EAAOZ,EAAaA,EAAa,CAAC,IAAIY,EAAOZ,CAAU,EAAE,EAAE,QAAQ,UAAW,GAAGyB,EAAQ,IAAI,EAAE,EAErIE,EAAY,KAAK,cAAc,uBAAuB,EACxDA,IAAWA,EAAU,YAAcD,EACxC,CAEQ,mBAA0B,CAC5B,KAAA,iBAAiB,QAAUE,GAAU,CACzC,MAAMC,EAASD,EAAM,OACrB,GAAIC,aAAkB,kBAAmB,CAGxC,IAAIjB,EAAe,EAEfiB,EAAO,QAAQ,OAEXjB,EAAA,OAAOiB,EAAO,QAAQ,IAAI,EAE1BA,EAAA,UAAU,IAAI,QAAQ,EAC7B,KAAK,iBAAiB,QAAQ,EAAE,QAASC,GAAW,CAC/CA,IAAWD,GACPC,EAAA,UAAU,OAAO,QAAQ,CACjC,CACA,GAIF,IAAId,EAAQ,KAAK,MAEX,MAAAe,EAAYX,GACVS,EAAO,UAAU,SAAST,CAAS,EAEvCW,EAAS,MAAM,GAClBf,IAEGe,EAAS,MAAM,GAClBf,IAEGe,EAAS,OAAO,IACXf,EAAA,GAELe,EAAS,MAAM,IAClBf,EAAQ,KAAK,WAGT,KAAA,YAAY,KAAO,KAAK,KAAOJ,EACpC,KAAK,YAAYA,CAAI,EAEjB,KAAK,QAAUI,IAClB,KAAK,MAAQA,EACb,KAAK,OAAO,EAGd,CAAA,CACA,EAED,KAAK,YAAY,iBAAiB,eAAiBX,GAAM,CACxD,KAAM,CAAE,KAAAO,EAAM,WAAAZ,EAAY,cAAAa,EAAe,YAAAmB,GAAgB3B,EAAE,QAGzDO,GAAQA,IAAS,KAAK,MACtBC,IAAkB,QAAaA,IAAkB,KAAK,eACvDb,IAAe,QACfgC,IAAgB,KAAK,cAGrB,KAAK,OAAO,CACb,CACA,CACF,CACD,CAEA,eAAe,OAAO,0BAA2BtB,CAAqB"} -------------------------------------------------------------------------------- /dist/action-table-switch.js: -------------------------------------------------------------------------------- 1 | class t extends HTMLElement{constructor(){super(),this.render(),this.addEventListeners()}get checked(){return this.hasAttribute("checked")}set checked(e){e?this.setAttribute("checked",""):this.removeAttribute("checked")}get label(){return this.getAttribute("label")||"switch"}get name(){return this.getAttribute("name")||""}get value(){return this.getAttribute("value")||"on"}addEventListeners(){const e=this.querySelector("input");e&&e.addEventListener("change",()=>{this.checked=e.checked,this.sendEvent()})}async sendEvent(){const e={checked:this.checked,id:this.id||this.dataset.id,name:this.name,value:this.value};this.dispatchEvent(new CustomEvent("action-table-switch",{detail:e,bubbles:!0}))}render(){const e=document.createElement("input");e.type="checkbox",e.name=this.name,e.value=this.value,e.checked=this.checked,e.setAttribute("aria-label",this.label),this.replaceChildren(e)}}customElements.define("action-table-switch",t); 2 | //# sourceMappingURL=action-table-switch.js.map 3 | -------------------------------------------------------------------------------- /dist/action-table-switch.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action-table-switch.js","sources":["../src/action-table-switch.ts"],"sourcesContent":["/* -------------------------------------------------------------------------- */\n/* Action Table Switch */\n/* -------------------------------------------------------------------------- */\n/* ----- Optional element added as an example to be extended by the user ---- */\n\nexport class ActionTableSwitch extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.render();\n\t\tthis.addEventListeners();\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Attributes */\n\t/* -------------------------------------------------------------------------- */\n\n\tget checked(): boolean {\n\t\treturn this.hasAttribute(\"checked\");\n\t}\n\tset checked(value: boolean) {\n\t\tif (value) {\n\t\t\tthis.setAttribute(\"checked\", \"\");\n\t\t} else {\n\t\t\tthis.removeAttribute(\"checked\");\n\t\t}\n\t}\n\tget label(): string {\n\t\treturn this.getAttribute(\"label\") || \"switch\";\n\t}\n\n\tget name(): string {\n\t\treturn this.getAttribute(\"name\") || \"\";\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute(\"value\") || \"on\";\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Event Listeners */\n\t/* -------------------------------------------------------------------------- */\n\n\tprivate addEventListeners() {\n\t\tconst input = this.querySelector(\"input\");\n\t\tif (input) {\n\t\t\tinput.addEventListener(\"change\", () => {\n\t\t\t\tthis.checked = input.checked;\n\t\t\t\tthis.sendEvent();\n\t\t\t});\n\t\t}\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Private Methods */\n\t/* -------------------------------------------------------------------------- */\n\n\t/* ----------------- Send Event Triggered by checkbox change ---------------- */\n\n\tprivate async sendEvent() {\n\t\tconst detail = { checked: this.checked, id: this.id || this.dataset.id, name: this.name, value: this.value };\n\t\tthis.dispatchEvent(new CustomEvent(\"action-table-switch\", { detail, bubbles: true }));\n\t}\n\n\tprivate render(): void {\n\t\tconst checkbox = document.createElement(\"input\");\n\t\tcheckbox.type = \"checkbox\";\n\t\tcheckbox.name = this.name;\n\t\tcheckbox.value = this.value;\n\t\tcheckbox.checked = this.checked;\n\t\tcheckbox.setAttribute(\"aria-label\", this.label);\n\t\tthis.replaceChildren(checkbox);\n\t}\n}\n\ncustomElements.define(\"action-table-switch\", ActionTableSwitch);\n"],"names":["ActionTableSwitch","value","input","detail","checkbox"],"mappings":"AAKO,MAAMA,UAA0B,WAAY,CAClD,aAAc,CACP,QACN,KAAK,OAAO,EACZ,KAAK,kBAAkB,CACxB,CAMA,IAAI,SAAmB,CACf,OAAA,KAAK,aAAa,SAAS,CACnC,CACA,IAAI,QAAQC,EAAgB,CACvBA,EACE,KAAA,aAAa,UAAW,EAAE,EAE/B,KAAK,gBAAgB,SAAS,CAEhC,CACA,IAAI,OAAgB,CACZ,OAAA,KAAK,aAAa,OAAO,GAAK,QACtC,CAEA,IAAI,MAAe,CACX,OAAA,KAAK,aAAa,MAAM,GAAK,EACrC,CAEA,IAAI,OAAgB,CACZ,OAAA,KAAK,aAAa,OAAO,GAAK,IACtC,CAMQ,mBAAoB,CACrB,MAAAC,EAAQ,KAAK,cAAc,OAAO,EACpCA,GACGA,EAAA,iBAAiB,SAAU,IAAM,CACtC,KAAK,QAAUA,EAAM,QACrB,KAAK,UAAU,CAAA,CACf,CAEH,CAQA,MAAc,WAAY,CACzB,MAAMC,EAAS,CAAE,QAAS,KAAK,QAAS,GAAI,KAAK,IAAM,KAAK,QAAQ,GAAI,KAAM,KAAK,KAAM,MAAO,KAAK,OAChG,KAAA,cAAc,IAAI,YAAY,sBAAuB,CAAE,OAAAA,EAAQ,QAAS,EAAM,CAAA,CAAC,CACrF,CAEQ,QAAe,CAChB,MAAAC,EAAW,SAAS,cAAc,OAAO,EAC/CA,EAAS,KAAO,WAChBA,EAAS,KAAO,KAAK,KACrBA,EAAS,MAAQ,KAAK,MACtBA,EAAS,QAAU,KAAK,QACfA,EAAA,aAAa,aAAc,KAAK,KAAK,EAC9C,KAAK,gBAAgBA,CAAQ,CAC9B,CACD,CAEA,eAAe,OAAO,sBAAuBJ,CAAiB"} -------------------------------------------------------------------------------- /dist/action-table.css: -------------------------------------------------------------------------------- 1 | action-table{display:block;--highlight: paleturquoise;--focus: dodgerblue;--star-checked: orange;--star-unchecked: gray;--switch-checked: green;--switch-unchecked: lightgray;--border: 1px solid lightgray;--border-radius: .2em;--th-bg: whitesmoke;--th-sorted: rgb(244, 220, 188);--col-sorted: rgb(255, 253, 240);--td-options-bg: whitesmoke;--page-btn: whitesmoke;--page-btn-active: rgb(244, 220, 188);--responsive-scroll-gradient: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0)), linear-gradient(to right, rgba(255, 255, 255, 0), #fff 70%) 0 100%, radial-gradient(farthest-side at 0% 50%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%}action-table :where(table){border-collapse:collapse;margin:1em 0;max-width:100%;overflow:auto;display:block;background:var(--responsive-scroll-gradient);background-repeat:no-repeat;background-size:40px 100%,40px 100%,14px 100%,14px 100%;background-position:0 0,100%,0 0,100%;background-attachment:local,local,scroll,scroll}action-table :where(th){border:var(--border);padding:0;text-align:left;background:var(--th-bg)}action-table :where(th[no-sort]){padding:.2em .5em}action-table :where(th button){cursor:pointer;font-weight:700;border:0;width:100%;height:100%;display:block;padding:.2em 1.5em .2em .5em;background-color:transparent;position:relative;text-align:left;font-size:inherit;line-height:inherit}action-table :where(th button:hover,th:has(button):hover,th button:focus,th:has(button):focus){background-color:var(--highlight)}action-table :where(th button):after{content:"";background-image:url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath d='M9 16.172l-6.071-6.071-1.414 1.414 8.485 8.485 8.485-8.485-1.414-1.414-6.071 6.071v-16.172h-2z'%3E%3C/path%3E%3C/svg%3E%0A");background-repeat:no-repeat;background-position:center right;background-size:.7em;height:.7em;width:.7em;display:block;opacity:.2;position:absolute;right:.4em;top:50%;transform:translateY(-50%);float:right;transition:transform .3s ease-in-out,opacity .3s ease-in-out}action-table :where(th[aria-sort$=ing] button,th[aria-sort$=ing]:has(button)){background-color:var(--th-sorted)}action-table :where(th[aria-sort$=ing] button):after{opacity:1}action-table :where(th[aria-sort=descending] button):after{opacity:1;transform:translateY(-50%) rotate(180deg)}action-table :where(td){border:var(--border);padding:.2em .4em}action-table .sorted{background-color:var(--col-sorted)}action-table :where(td span){background-color:var(--td-options-bg);padding:0 .4em .1em;margin:0 .2em}action-table:not(:defined),action-table-filters:not(:defined){visibility:hidden}action-table :where(select,input,button){font-size:inherit}action-table :where(input[type=search],select){border:var(--border);border-radius:var(--border-radius)}action-table .selected{background-color:var(--highlight);transition:color .2s ease}action-table .no-results :where(td){padding:1em;text-align:center}action-table :where(button){cursor:pointer}action-table-filter-menu,action-table-filter-switch label,action-table-filter-menu .filter-label{display:inline-flex;flex-wrap:wrap;align-items:center}action-table-filter-menu label,action-table-filter-menu .filter-label{margin-inline-end:.3em}action-table .switch label{display:inline-flex;align-items:center;margin:0}action-table .switch input{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;display:inline-block;background:var(--switch-unchecked);cursor:pointer;height:1.4em;width:2.75em;vertical-align:middle;border-radius:2em;box-shadow:0 1px 3px #0003 inset;transition:.25s linear background}action-table .switch input:before{content:"";display:block;width:1em;height:1em;background:#fff;border-radius:1em;position:absolute;top:.2em;left:.2em;box-shadow:0 1px 3px #0003;transition:.25s linear transform;transform:translate(0)}action-table .switch :checked{background:var(--switch-checked)}action-table .switch :checked:before{transform:translate(1.3em)}action-table .switch input:focus,action-table .star input:focus{outline:transparent}action-table .switch input:focus-visible,action-table .star input:focus-visible{outline:2px solid var(--focus);outline-offset:2px}action-table .star input{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;cursor:pointer;height:1.6em;width:1.6em;vertical-align:middle;border-radius:.3em}action-table .star input:before{content:"";background:var(--star-unchecked);cursor:pointer;position:absolute;height:1.6em;width:1.6em;vertical-align:middle;transition:.25s linear background;clip-path:polygon(50% 0%,62% 29%,98% 35%,74% 58%,79% 91%,50% 76%,21% 91%,26% 58%,2% 35%,34% 29%)}action-table .star input:checked:before{background:var(--star-checked)}action-table-pagination{display:flex;justify-content:start;align-items:center;flex-wrap:wrap;gap:.6em;max-width:100%;overflow:auto}action-table-pagination .pagination-buttons{display:flex;justify-content:start;align-items:center;gap:.3em}action-table-pagination button{cursor:pointer;font-size:inherit;background-color:var(--page-btn);border:0;border-radius:.3em;padding:.2em .5em}action-table-pagination button:hover{background-color:var(--highlight)}action-table-pagination .active{font-weight:700;background-color:var(--page-btn-active)}@keyframes fade-in{0%{opacity:0}}@keyframes fade-out{to{opacity:0}}@keyframes slide-from-bottom{0%{transform:translateY(50px)}}@keyframes slide-to-top{to{transform:translateY(-50px)}}::view-transition-old(row){animation:90ms cubic-bezier(.4,0,1,1) both fade-out,.3s cubic-bezier(.4,0,.2,1) both slide-to-top}::view-transition-new(row){animation:1.21s cubic-bezier(0,0,.2,1) 90ms both fade-in,.3s cubic-bezier(.4,0,.2,1) both slide-from-bottom}action-table-filter-range{--thumb-size: 1.3em;--thumb-bg: #fff;--thumb-border: solid 2px #9e9e9e;--thumb-shadow: 0 1px 4px .5px rgba(0, 0, 0, .25);--thumb-highlight: var(--highlight);--track-bg: lightgray;--track-shadow: inset 0 0 2px #00000099;--track-highlight: var(--highlight);--ticks-color: #b2b2b2;--ticks-width: 1}action-table-filter-range,action-table-filter-range>div,action-table-filter-range label{display:flex;align-items:center;flex-wrap:wrap;gap:.6em}action-table-filter-range .range-slider-group{display:grid}action-table-filter-range .range-slider-group>*{grid-column:1;grid-row:1;position:relative}action-table-filter-range .range-slider-group:after{content:"";width:100%;height:.5em;background-color:var(--track-bg);border-radius:50px;background-size:100% 2px;box-shadow:var(--track-shadow);grid-column:1;grid-row:1;justify-self:center}action-table-filter-range .range-slider-highlight{background-color:var(--track-highlight);width:100%;height:.35em}action-table-filter-range input{-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;width:100%;background:transparent;padding:.2em 0;pointer-events:none;cursor:-webkit-grab;cursor:grab}action-table-filter-range input::-webkit-slider-runnable-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent}action-table-filter-range input::-webkit-slider-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);border-radius:50%;background:var(--thumb-bg);border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-moz-range-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);background:var(--thumb-bg);border-radius:50%;border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-ms-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);background:var(--thumb-bg);border-radius:50%;border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-webkit-slider-thumb:hover{background:var(--thumb-highlight)}action-table-filter-range svg{color:var(--ticks-color);position:relative;top:-.6em;width:calc(100% - var(--thumb-size));justify-self:center;border-left:1px solid var(--ticks-color);border-right:1px solid var(--ticks-color);box-sizing:border-box} 2 | -------------------------------------------------------------------------------- /dist/action-table.js: -------------------------------------------------------------------------------- 1 | var g=Object.defineProperty;var p=(c,l,t)=>l in c?g(c,l,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[l]=t;var h=(c,l,t)=>(p(c,typeof l!="symbol"?l+"":l,t),t);class m extends HTMLElement{constructor(){super();const l=this.closest("action-table");this.style.display="none",this.addEventListener("click",t=>{t.target instanceof HTMLButtonElement&&t.target.type==="reset"&&(this.dispatchEvent(new CustomEvent("action-table-filter",{bubbles:!0})),this.dispatchEvent(new CustomEvent("action-table-filters-reset",{bubbles:!0})))}),l.addEventListener("action-table",t=>{const e=t.detail;(e==null?void 0:e.rowsVisible)===0?this.style.display="":this.style.display="none"})}}customElements.define("action-table-no-results",m);function y(c){return Object.keys(c).length>0}class C extends HTMLElement{constructor(){var t;super();h(this,"table");h(this,"tbody");h(this,"cols",[]);h(this,"rows",[]);h(this,"filters",{});h(this,"tableContent",new WeakMap);h(this,"rowsSet",new Set);h(this,"sortAndFilter",this.delayUntilNoLongerCalled(()=>{this.filterTable(),this.sortTable(),this.appendRows(),this.tbody.matches("[style*=none]")&&(this.rowsSet.size===0&&(this.setFilters(),this.dispatchEvent(new Event("action-table-filters-reset"))),this.tbody.style.display="")}));if(this.store){const e=this.getStore();e&&(this.sort=e.sort||this.sort,this.direction=e.direction||this.direction||"ascending",this.filters=e.filters||this.filters)}if(this.hasAttribute("urlparams")){const e=new URLSearchParams(window.location.search),s={};for(let[i,r]of e.entries())i=i.toLowerCase(),r=r.toLowerCase(),i!=="sort"&&i!=="direction"&&((t=s[i])!=null&&t.values?s[i].values.push(r):s[i]={values:[r]}),i==="sort"&&(this.sort=r),i==="direction"&&(r==="ascending"||r==="descending")&&(this.direction=r);Object.keys(s).length>0&&this.setFiltersObject(s)}this.addEventListeners()}get sort(){return this.getCleanAttr("sort")}set sort(t){this.setAttribute("sort",t)}get direction(){const t=this.getCleanAttr("direction");return t==="descending"?t:"ascending"}set direction(t){this.setAttribute("direction",t)}get store(){return this.hasAttribute("store")?this.getCleanAttr("store")||"action-table":""}get pagination(){return Number(this.getCleanAttr("pagination"))||0}set pagination(t){this.setAttribute("pagination",t.toString())}get page(){return Number(this.getCleanAttr("page"))||1}set page(t){t=this.checkPage(t),this.setAttribute("page",t.toString())}checkPage(t){return Math.max(1,Math.min(t,this.numberOfPages))}dispatch(t){this.dispatchEvent(new CustomEvent("action-table",{detail:t}))}getCleanAttr(t){var e;return((e=this.getAttribute(t))==null?void 0:e.trim().toLowerCase())||""}connectedCallback(){const t=this.querySelector("table");if(t&&t.querySelector("thead th")&&t.querySelector("tbody td"))this.table=t,this.tbody=t.querySelector("tbody"),this.rows=Array.from(this.tbody.querySelectorAll("tr")),this.rowsSet=new Set(this.rows);else throw new Error("Could not find table with thead and tbody");(this.sort||y(this.filters))&&(this.tbody.style.display="none",this.sortAndFilter()),this.getColumns(),this.addObserver()}static get observedAttributes(){return["sort","direction","pagination","page"]}attributeChangedCallback(t,e,s){e!==s&&this.rows.length>0&&((t==="sort"||t==="direction")&&this.sortTable(),t==="pagination"&&this.dispatch({pagination:this.pagination}),this.appendRows())}setFiltersObject(t={}){this.filters=t,this.store&&this.setStore({filters:this.filters})}setFilters(t={}){this.setFiltersObject(t),this.filterTable(),this.appendRows()}addEventListeners(){this.addEventListener("click",e=>{const s=e.target;if(s instanceof HTMLButtonElement&&s.dataset.col){const i=s.dataset.col;let r="ascending";this.sort===i&&this.direction==="ascending"&&(r="descending"),this.sort=i,this.direction=r,this.store&&this.setStore({sort:this.sort,direction:r})}},!1);const t=e=>e.matches("td")?e:e.closest("td");this.addEventListener("change",e=>{const s=e.target;s instanceof HTMLInputElement&&s.closest("td")&&s.type==="checkbox"&&this.updateCellValues(t(s))}),this.addEventListener("action-table-filter",e=>{if(e.detail){const s={...this.filters,...e.detail};Object.keys(s).forEach(i=>{s[i].values.every(r=>r==="")&&delete s[i]}),this.setFilters(s)}else this.setFilters()}),this.addEventListener("action-table-update",e=>{const s=e.target;if(s instanceof HTMLElement){let i={};typeof e.detail=="string"?i={sort:e.detail,filter:e.detail}:i=e.detail,this.updateCellValues(t(s),i)}})}getStore(){try{const t=localStorage.getItem(this.store),e=t&&JSON.parse(t);return typeof e=="object"&&e!==null&&["sort","direction","filters"].some(i=>i in e)?e:!1}catch{return!1}}setStore(t){const e=this.getStore()||{};e&&(t={...e,...t}),localStorage.setItem(this.store,JSON.stringify(t))}delayUntilNoLongerCalled(t){let e,s=!1;function i(){t(),s=!1}return function(){s?clearTimeout(e):s=!0,e=setTimeout(i,10)}}getColumns(){const t=this.table.querySelectorAll("thead th");if(t.forEach(e=>{const s=(e.dataset.col||this.getCellContent(e)).trim().toLowerCase(),i=e.dataset.order?e.dataset.order.split(","):void 0;if(this.cols.push({name:s,order:i}),!e.hasAttribute("no-sort")){const r=document.createElement("button");r.dataset.col=s,r.type="button",r.innerHTML=e.innerHTML,e.replaceChildren(r)}}),!this.table.querySelector("colgroup")){const e=document.createElement("colgroup");t.forEach(()=>{const s=document.createElement("col");e.appendChild(s)}),this.table.prepend(e)}}getCellContent(t){var s;let e=(t.textContent||"").trim();if(!e){const i=t.querySelector("svg");i instanceof SVGElement&&(e=((s=i.querySelector("title"))==null?void 0:s.textContent)||e);const r=t.querySelector("input[type=checkbox]");r instanceof HTMLInputElement&&r.checked&&(e=r.value);const n=t.querySelector(":defined");n!=null&&n.shadowRoot&&(e=n.shadowRoot.textContent||e)}return e.trim()}getCellValues(t){return this.tableContent.has(t)?this.tableContent.get(t):this.setCellValues(t)}setCellValues(t,e={}){const s=this.getCellContent(t),i={sort:t.dataset.sort||s,filter:t.dataset.filter||s,...e};return this.tableContent.set(t,i),i}updateCellValues(t,e={}){this.setCellValues(t,e),this.sortAndFilter()}addObserver(){new MutationObserver(e=>{e.forEach(s=>{let i=s.target;if(i.nodeType===3&&i.parentNode&&(i=i.parentNode),!(i instanceof HTMLElement))return;const r=i.closest("td");r instanceof HTMLTableCellElement&&(r.hasAttribute("contenteditable")&&r===document.activeElement?r.dataset.edit||(r.dataset.edit="true",r.addEventListener("blur",()=>{this.updateCellValues(r)})):this.updateCellValues(r))})}).observe(this.tbody,{childList:!0,subtree:!0,attributes:!0,characterData:!0,attributeFilter:["data-sort","data-filter"]})}filterTable(){const t=this.numberOfPages,e=this.rowsSet.size,s=this.filters["action-table"];this.rows.forEach(i=>{let r=!1;const n=i.querySelectorAll("td");if(s){const o=Array.from(n).filter((a,d)=>this.filters["action-table"].cols?this.filters["action-table"].cols.includes(this.cols[d].name.toLowerCase()):!0).map(a=>a.querySelector('input[type="checkbox"]')?"":this.getCellValues(a).filter).join(" ");this.shouldHide(s,o)&&(r=!0)}n.forEach((u,o)=>{const a=this.filters[this.cols[o].name];a&&this.shouldHide(a,this.getCellValues(u).filter)&&(r=!0)}),r?this.rowsSet.delete(i):this.rowsSet.add(i)}),this.numberOfPages!==t&&this.dispatch({numberOfPages:this.numberOfPages}),this.rowsSet.size!==e&&this.dispatch({rowsVisible:this.rowsSet.size})}shouldHide(t,e){if(t.values&&t.values.length>0){if(t.regex){let s=t.values.join("|");return t.exclusive&&(s=`${t.values.map(n=>`(?=.*${n})`).join("")}.*`),!new RegExp(s,"i").test(e)}if(t.range){const[s,i]=t.values;if(!isNaN(Number(s))&&!isNaN(Number(i)))return Number(e)Number(i)}return t.exclusive?!t.values.every(s=>e.toLowerCase().includes(s.toLowerCase())):t.exact?t.values.every(s=>s&&s!==e):!t.values.some(s=>e.toLowerCase().includes(s.toLowerCase()))}return!1}sortTable(t=this.sort,e=this.direction){if(!this.sort||!e)return;t=t.toLowerCase();const s=this.cols.findIndex(i=>i.name===t);if(s>=0&&this.rows.length>0){const i=this.cols[s].order,r=o=>i!=null&&i.includes(o)?i.indexOf(o).toString():o;this.rows.sort((o,a)=>{if(e==="descending"){const b=o;o=a,a=b}const d=r(this.getCellValues(o.children[s]).sort),f=r(this.getCellValues(a.children[s]).sort);return this.alphaNumSort(d,f)}),this.querySelectorAll("col").forEach((o,a)=>{a===s?o.classList.add("sorted"):o.classList.remove("sorted")}),this.table.querySelectorAll("thead th").forEach((o,a)=>{const d=a===s?e:"none";o.setAttribute("aria-sort",d)})}}alphaNumSort(t,e){function s(n){if(isNaN(Number(n))){if(!isNaN(Date.parse(n)))return Date.parse(n)}else return Number(n)}const i=s(t),r=s(e);return i&&r?i-r:t.localeCompare(e)}appendRows(){const t=i=>{const{pagination:r,page:n}=this;return r===0||i>=r*(n-1)+1&&i<=r*n},e=document.createDocumentFragment();let s=0;if(this.rows.forEach(i=>{let r="none";this.rowsSet.has(i)&&(s++,t(s)&&(r="",e.appendChild(i))),i.style.display=r}),this.tbody.prepend(e),this.pagination>0){const i=this.checkPage(this.page);i!==this.page&&(this.page=i,this.dispatch({page:i}))}}get numberOfPages(){return this.pagination>0?Math.ceil(this.rowsSet.size/this.pagination):1}}customElements.define("action-table",C); 2 | //# sourceMappingURL=action-table.js.map 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import"./action-table.js";import"./action-table-filters.js";import"./action-table-pagination.js"; 2 | //# sourceMappingURL=index.js.map 3 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""} -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | var m=Object.defineProperty;var a=(n,e,t)=>e in n?m(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var o=(n,e,t)=>(a(n,typeof e!="symbol"?e+"":e,t),t);import"./index.js";import"./action-table-switch.js";import"./action-table.js";import"./action-table-filters.js";import"./action-table-pagination.js";class i extends HTMLElement{constructor(){super();o(this,"randomNumber");const t=this.attachShadow({mode:"open"});this.randomNumber=Math.floor(Math.random()*10)+1,t.innerHTML=`${this.randomNumber}`,this.dispatch(this.randomNumber.toString()),this.addEventListener("click",()=>{this.randomNumber=Math.floor(Math.random()*10)+1,t.innerHTML=`${this.randomNumber}`,this.dispatch(this.randomNumber.toString())})}dispatch(t){const r=new CustomEvent("action-table-update",{detail:t,bubbles:!0});this.dispatchEvent(r)}}customElements.define("random-number",i); 2 | //# sourceMappingURL=main.js.map 3 | -------------------------------------------------------------------------------- /dist/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sources":["../src/random-number.ts"],"sourcesContent":["class RandomNumberComponent extends HTMLElement {\n\tprivate randomNumber: number;\n\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: \"open\" });\n\t\tthis.randomNumber = Math.floor(Math.random() * 10) + 1;\n\t\tshadow.innerHTML = `${this.randomNumber}`;\n\t\tthis.dispatch(this.randomNumber.toString());\n\n\t\t// click event that rerandomizes the number\n\t\tthis.addEventListener(\"click\", () => {\n\t\t\tthis.randomNumber = Math.floor(Math.random() * 10) + 1;\n\t\t\tshadow.innerHTML = `${this.randomNumber}`;\n\t\t\tthis.dispatch(this.randomNumber.toString());\n\t\t});\n\t}\n\n\tprivate dispatch(detail: { sort: string; filter: string } | string) {\n\t\t// console.log(\"dispatch\", detail);\n\t\tconst event = new CustomEvent<{ sort: string; filter: string } | string>(\"action-table-update\", {\n\t\t\tdetail,\n\t\t\tbubbles: true,\n\t\t});\n\t\tthis.dispatchEvent(event);\n\t}\n}\n\ncustomElements.define(\"random-number\", RandomNumberComponent);\n"],"names":["RandomNumberComponent","__publicField","shadow","detail","event"],"mappings":"6TAAA,MAAMA,UAA8B,WAAY,CAG/C,aAAc,CACP,QAHCC,EAAA,qBAIP,MAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,OAAQ,EACjD,KAAK,aAAe,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,EAC9CA,EAAA,UAAY,GAAG,KAAK,YAAY,GACvC,KAAK,SAAS,KAAK,aAAa,SAAU,CAAA,EAGrC,KAAA,iBAAiB,QAAS,IAAM,CACpC,KAAK,aAAe,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,EAC9CA,EAAA,UAAY,GAAG,KAAK,YAAY,GACvC,KAAK,SAAS,KAAK,aAAa,SAAU,CAAA,CAAA,CAC1C,CACF,CAEQ,SAASC,EAAmD,CAE7D,MAAAC,EAAQ,IAAI,YAAuD,sBAAuB,CAC/F,OAAAD,EACA,QAAS,EAAA,CACT,EACD,KAAK,cAAcC,CAAK,CACzB,CACD,CAEA,eAAe,OAAO,gBAAiBJ,CAAqB"} -------------------------------------------------------------------------------- /docs/action-table-filters.js: -------------------------------------------------------------------------------- 1 | var x=Object.defineProperty;var y=(g,p,e)=>p in g?x(g,p,{enumerable:!0,configurable:!0,writable:!0,value:e}):g[p]=e;var E=(g,p,e)=>(y(g,typeof p!="symbol"?p+"":p,e),e);class A extends HTMLElement{constructor(){super();E(this,"options",[])}findOptions(e){e=e.toLowerCase();const t=this.closest("action-table"),o=t.cols,n=t.tbody,s=o.findIndex(i=>i.name===e);if(s===-1)return;const h=`td:nth-child(${s+1})`,u=n.querySelectorAll(h);let a=[];Array.from(u).forEach(i=>{const l=i.querySelectorAll("span, ul > li");if(!i.dataset.filter&&(l==null?void 0:l.length)>0){const d=Array.from(l).map(f=>f.textContent||"");a=a.concat(d)}else a.push(t.getCellValues(i).filter)}),a=Array.from(new Set(a));const r=o[s].order;function c(i){return r!=null&&r.includes(i)?r.indexOf(i).toString():i}a.sort((i,l)=>(i=c(i),l=c(l),t.alphaNumSort(i,l))),this.hasAttribute("descending")&&a.reverse(),this.options=a}connectedCallback(){var t;const e=this.getAttribute("name");e&&(this.hasAttribute("options")?this.options=((t=this.getAttribute("options"))==null?void 0:t.split(","))||[]:this.findOptions(e),this.render(e))}render(e){if(this.options.length<1)return;const t=this.getAttribute("type")||"select";if(t!=="checkbox"&&this.options.length<2)return;const o=this.getAttribute("label")||e,n=this.hasAttribute("multiple")?"multiple":"",s=this.getAttribute("all")||"All";let h="",u="";const a=t==="select"?``:`${o}`;t==="select"&&(h=`"),t==="radio"&&(h=``);const r=`${a}${h}${this.options.map(c=>t==="select"?``:t==="radio"||t==="checkbox"?``:"").join("")}${u}`;this.innerHTML=`${r}`}}customElements.define("action-table-filter-menu",A);class L extends HTMLElement{connectedCallback(){const p=this.getAttribute("name");p&&(this.innerHTML=``)}}customElements.define("action-table-filter-switch",L);class T extends HTMLElement{constructor(){super();E(this,"min",0);E(this,"rangeTotal",0);this.render(),this.addEventListeners()}get name(){return this.getAttribute("name")||""}addEventListeners(){this.addEventListener("input",e=>{const t=this.querySelectorAll("input"),o=this.querySelector("output"),[n,s]=Array.from(t).map(r=>Number(r.value)),h=n.toString(),u=s.toString();o instanceof HTMLOutputElement&&(o.textContent=`${h}-${u}`);const a=this.querySelector(".range-slider-highlight");a instanceof HTMLSpanElement&&(a.style.marginLeft=`${(n-this.min)/this.rangeTotal*100}%`,a.style.width=`${(s-n)/this.rangeTotal*100}%`),n>s&&(e.stopPropagation(),t[0].value=u,t[1].value=h)})}findMinMax(){const e=this.getAttribute("min"),t=this.getAttribute("max");if(e&&t)return[Number(e),Number(t)];const o=this.closest("action-table"),n=o.cols,s=o.tbody,h=n.findIndex(r=>r.name===this.name.toLowerCase());if(h===-1)return[0,0];const u=`td:nth-child(${h+1})`,a=s.querySelectorAll(u);return Array.from(a).reduce((r,c)=>{const i=Number(o.getCellValues(c).filter);let l=r.length===2?r[0]:i,d=r.length===2?r[1]:i;return l=li?d:i,[l,d]},[])}render(){const[e,t]=this.findMinMax(),o=e.toString(),n=t.toString();this.rangeTotal=t-e,this.min=e;const s=this.getAttribute("label")||this.name,h=document.createElement("div");h.textContent=s;const u=document.createElement("div");u.classList.add("range-slider-group");const a=Math.pow(10,Math.round(Math.log10(this.rangeTotal)))/10;function r(m,b){for(const v in b)m.setAttribute(v,b[v])}const c=[e];for(let m=e+a;m<=t;m+=a)c.push(Math.round(m/a)*a);c.includes(t)||c.push(t);const i=document.createElementNS("http://www.w3.org/2000/svg","svg");r(i,{role:"presentation",width:"100%",height:"5"});const l=100/(c.length-1);for(let m=1;m0&&this.setFilterElements(e)}toggleHighlight(e){e.value?e.classList.add("selected"):e.classList.remove("selected")}addEventListeners(){var o;const e=(n,s)=>n.hasAttribute(s)||!!n.closest(`[${s}]`);this.addEventListener("input",n=>{const s=n.target;if(s instanceof HTMLSelectElement||s instanceof HTMLInputElement){const h=e(s,"exclusive"),u=e(s,"regex"),a=e(s,"exact"),r=s.dataset.cols?s.dataset.cols.toLowerCase().split(","):void 0,c=s.name.toLowerCase();if(s instanceof HTMLSelectElement){this.toggleHighlight(s);const i=Array.from(s.selectedOptions).map(l=>l.value);this.dispatch({[c]:{values:i,exclusive:h,regex:u,exact:a,cols:r}})}if(s instanceof HTMLInputElement){if(s.type==="checkbox"){const i=this.querySelectorAll("input[type=checkbox][name="+s.name+"]"),l=Array.from(i).filter(d=>d.checked).map(d=>d.value);this.dispatch({[c]:{values:l,exclusive:h,regex:u,exact:a,cols:r}})}if(s.type==="radio"&&this.dispatch({[c]:{values:[s.value],exclusive:h,regex:u,exact:a,cols:r}}),s.type==="range"){const i=this.querySelectorAll("input[type=range][name='"+s.name+"']");let l=[];const d=[];i.forEach(f=>{f.dataset.range==="min"&&(d[0]=f.min,l[0]=f.value),f.dataset.range==="max"&&(d[1]=f.max,l[1]=f.value)}),l.every((f,m)=>f===d[m])&&(l=[]),this.dispatch({[c]:{values:l,range:!0}})}}}}),this.querySelectorAll("input[type='search']").forEach(n=>{function s(u,a=300){let r;return(...c)=>{clearTimeout(r),r=setTimeout(()=>{u(...c)},a)}}const h=n.dataset.event||"input";n.addEventListener(h,()=>{this.toggleHighlight(n);const u=e(n,"exclusive"),a=e(n,"regex"),r=e(n,"exact"),c=n.dataset.cols?n.dataset.cols.toLowerCase().split(","):void 0;s(()=>this.dispatch({[n.name]:{values:[n.value],exclusive:u,regex:a,exact:r,cols:c}}))()})}),(o=this.resetButton)==null||o.addEventListener("click",()=>{this.resetAllFilterElements(),this.dispatch()}),this.actionTable.addEventListener("action-table-filters-reset",()=>{this.resetAllFilterElements()})}dispatchInput(e){e.dispatchEvent(new Event("input",{bubbles:!0}))}dispatch(e){if(this.resetButton)if(e){let t=this.actionTable.filters||{};t={...t,...e},this.enableReset(this.hasFilters(t))}else this.enableReset(!1);this.dispatchEvent(new CustomEvent("action-table-filter",{detail:e,bubbles:!0}))}enableReset(e=!0){this.resetButton&&(e?this.resetButton.removeAttribute("disabled"):this.resetButton.setAttribute("disabled",""))}hasFilters(e){return Object.keys(e).some(t=>e[t].values.some(o=>o!==""))}resetAllFilterElements(){this.querySelectorAll("select, input").forEach(t=>{t instanceof HTMLInputElement&&(t.type==="checkbox"||t.type==="radio")&&(t.value===""?t.checked=!0:t.checked=!1),(t instanceof HTMLSelectElement||t instanceof HTMLInputElement&&t.type==="search")&&(t.value="",this.toggleHighlight(t)),t instanceof HTMLInputElement&&t.type==="range"&&(t.value=t.dataset.range==="max"?t.max:t.min,this.dispatchInput(t))})}setFilterElements(e){this.hasFilters(e)?(this.enableReset(),Object.keys(e).forEach(t=>this.setFilterElement(t,e[t].values))):this.resetAllFilterElements()}setSelectValueIgnoringCase(e,t){t=t.toLowerCase(),Array.from(e.options).some(o=>(o.value.toLowerCase()||o.text.toLowerCase())===t?(o.selected=!0,this.toggleHighlight(e),!0):!1)}setFilterElement(e,t){if(t.length===0)return;this.querySelectorAll(`select[name="${e}" i], input[name="${e}" i]`).forEach(n=>{n instanceof HTMLSelectElement&&(n.value=t[0],this.setSelectValueIgnoringCase(n,t[0])),n instanceof HTMLInputElement&&(n.type==="checkbox"&&t.includes(n.value)&&(n.checked=!0),n.type==="radio"&&n.value===t[0]&&(n.checked=!0),n.type==="search"&&(n.value=t[0],this.toggleHighlight(n)),n.type==="range"&&(n.dataset.range==="min"&&(n.value=t[0]||n.min,this.dispatchInput(n)),n.dataset.range==="max"&&(n.value=t[1]||n.max,this.dispatchInput(n))))})}}customElements.define("action-table-filters",$); 5 | //# sourceMappingURL=action-table-filters.js.map 6 | -------------------------------------------------------------------------------- /docs/action-table-pagination.js: -------------------------------------------------------------------------------- 1 | var f=Object.defineProperty;var d=(r,a,s)=>a in r?f(r,a,{enumerable:!0,configurable:!0,writable:!0,value:s}):r[a]=s;var c=(r,a,s)=>(d(r,typeof a!="symbol"?a+"":a,s),s);class m extends HTMLElement{constructor(){super();const a=this.closest("action-table"),{pagination:s}=a,t=i=>i.map(e=>``).join(""),n=this.options.length>0?``:"";this.innerHTML=n,this.addEventListener("change",i=>{if(i.target instanceof HTMLSelectElement){const e=Number(i.target.value);isNaN(e)||(a.pagination=e)}})}get options(){const a=this.getAttribute("options");if(a){const s=a.split(",").map(t=>Number(t)).filter(t=>!isNaN(t));if(s.length>0)return s}return[]}}customElements.define("action-table-pagination-options",m);class $ extends HTMLElement{constructor(){super();c(this,"page",1);c(this,"numberOfPages",1);c(this,"group",1);c(this,"maxGroups",1);c(this,"actionTable",this.closest("action-table"));c(this,"rowsVisible",0);this.addEventListeners()}connectedCallback(){this.render()}render(){const{page:s,numberOfPages:t}=this.actionTable;this.numberOfPages=t,this.page=s;const n=Number(this.getAttribute("max-buttons"))||10,i=Math.ceil(t/n);let e=this.group;e>i?e=i:e<1&&(e=1);const l=(e-1)*n+1;function p(o,b="",h){return``}let g="";if(e>1&&(g+=`${p(1,"first")}${p(l-1,"prev","...")}`),t>0){for(let o=l;o<=t;o++)if(g+=p(o),o!==t&&o>=n*e){g+=`${p(o+1,"next","...")}${p(t,"last")}`;break}}const u=o=>` class="pagination-${o}"`;this.innerHTML=` ${g}`,this.changeLabel(s),this.group=e,this.maxGroups=i}changeLabel(s){const{pagination:t,rowsSet:n}=this.actionTable;this.rowsVisible=n.size;const e=(this.getAttribute("label")||"Showing {rows} of {total}:").replace("{rows}",`${s*t-t+1}-${s*t}`).replace("{total}",`${n.size}`),l=this.querySelector("span.pagination-label");l&&(l.textContent=e)}addEventListeners(){this.addEventListener("click",s=>{const t=s.target;if(t instanceof HTMLButtonElement){let n=1;t.dataset.page&&(n=Number(t.dataset.page),t.classList.add("active"),this.querySelectorAll("button").forEach(l=>{l!==t&&l.classList.remove("active")}));let i=this.group;const e=l=>t.classList.contains(l);e("next")&&i++,e("prev")&&i--,e("first")&&(i=1),e("last")&&(i=this.maxGroups),this.actionTable.page=this.page=n,this.changeLabel(n),this.group!==i&&(this.group=i,this.render())}}),this.actionTable.addEventListener("action-table",s=>{const{page:t,pagination:n,numberOfPages:i,rowsVisible:e}=s.detail;(t&&t!==this.page||i!==void 0&&i!==this.numberOfPages||n!==void 0||e!==this.rowsVisible)&&this.render()})}}customElements.define("action-table-pagination",$); 2 | //# sourceMappingURL=action-table-pagination.js.map 3 | -------------------------------------------------------------------------------- /docs/action-table-pagination.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action-table-pagination.js","sources":["../src/action-table-pagination-options.ts","../src/action-table-pagination.ts"],"sourcesContent":["import type { ActionTable } from \"./action-table\";\n\nexport class ActionTablePaginationOptions extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst actionTable = this.closest(\"action-table\") as ActionTable;\n\t\tconst { pagination } = actionTable;\n\t\tconst paginationOptions = (options: number[]) => options.map((opt) => ``).join(\"\");\n\n\t\tconst paginationSelect =\n\t\t\tthis.options.length > 0\n\t\t\t\t? ``\n\t\t\t\t: \"\";\n\n\t\tthis.innerHTML = paginationSelect;\n\t\tthis.addEventListener(\"change\", (e) => {\n\t\t\tif (e.target instanceof HTMLSelectElement) {\n\t\t\t\tconst value = Number(e.target.value);\n\t\t\t\tif (!isNaN(value)) {\n\t\t\t\t\tactionTable.pagination = value;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\tget options(): number[] {\n\t\tconst options = this.getAttribute(\"options\");\n\t\tif (options) {\n\t\t\tconst paginationArray = options\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((item) => Number(item))\n\t\t\t\t.filter((p) => !isNaN(p));\n\t\t\tif (paginationArray.length > 0) {\n\t\t\t\treturn paginationArray;\n\t\t\t}\n\t\t}\n\t\treturn [];\n\t}\n}\n\ncustomElements.define(\"action-table-pagination-options\", ActionTablePaginationOptions);\n","import type { ActionTable } from \"./action-table\";\nimport \"./action-table-pagination-options\";\n\nexport class ActionTablePagination extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.addEventListeners();\n\t}\n\n\tprivate page = 1;\n\tprivate numberOfPages = 1;\n\tprivate group = 1;\n\tprivate maxGroups = 1;\n\tprivate actionTable = this.closest(\"action-table\") as ActionTable;\n\tprivate rowsVisible = 0;\n\n\tpublic connectedCallback(): void {\n\t\tthis.render();\n\t}\n\n\tpublic render() {\n\t\tconsole.log(\"render pagination\");\n\n\t\tconst { page, numberOfPages } = this.actionTable;\n\t\t// reassign number of pages based on this.actionTable\n\t\tthis.numberOfPages = numberOfPages;\n\t\tthis.page = page;\n\t\t// temporarily local variables\n\t\tconst maxButtons = Number(this.getAttribute(\"max-buttons\")) || 10;\n\t\tconst maxGroups = Math.ceil(numberOfPages / maxButtons); // reassign to this at end of render\n\t\tlet group = this.group; // reassign to this at end of render\n\n\t\tif (group > maxGroups) {\n\t\t\tgroup = maxGroups;\n\t\t} else if (group < 1) {\n\t\t\tgroup = 1;\n\t\t}\n\n\t\tconst startIndex = (group - 1) * maxButtons + 1;\n\n\t\t/* -------------------------------------------------------------------------- */\n\t\t/* Render the buttons */\n\t\t/* -------------------------------------------------------------------------- */\n\n\t\t/* ----------------------------- Button strings ----------------------------- */\n\t\tfunction pageButton(i: number, className: string = \"\", text?: string): string {\n\t\t\treturn ``;\n\t\t}\n\n\t\t/* -------------------------- Start making buttons -------------------------- */\n\n\t\tlet paginatedButtons = \"\";\n\n\t\tif (group > 1) {\n\t\t\tpaginatedButtons += `${pageButton(1, \"first\")}${pageButton(startIndex - 1, \"prev\", \"...\")}`;\n\t\t}\n\n\t\tif (numberOfPages > 0) {\n\t\t\t// for looping through the number of pages\n\t\t\tfor (let i = startIndex; i <= numberOfPages; i++) {\n\t\t\t\t// code to handle each page\n\t\t\t\tpaginatedButtons += pageButton(i);\n\t\t\t\tif (i !== numberOfPages && i >= maxButtons * group) {\n\t\t\t\t\tpaginatedButtons += `${pageButton(i + 1, \"next\", \"...\")}${pageButton(numberOfPages, \"last\")}`;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst classAttr = (suffix: string) => ` class=\"pagination-${suffix}\"`;\n\n\t\tthis.innerHTML = ` ${paginatedButtons}`;\n\t\tthis.changeLabel(page);\n\n\t\t// assign temporary variables back to this\n\t\tthis.group = group;\n\t\tthis.maxGroups = maxGroups;\n\t}\n\n\tprivate changeLabel(page: number) {\n\t\tconst { pagination, rowsSet } = this.actionTable;\n\t\t// update rowsVisible from current set size\n\t\tthis.rowsVisible = rowsSet.size;\n\n\t\tconst label = this.getAttribute(\"label\") || \"Showing {rows} of {total}:\";\n\n\t\tconst labelStr = label.replace(\"{rows}\", `${page * pagination - pagination + 1}-${page * pagination}`).replace(\"{total}\", `${rowsSet.size}`);\n\n\t\tconst labelSpan = this.querySelector(\"span.pagination-label\");\n\t\tif (labelSpan) labelSpan.textContent = labelStr;\n\t}\n\n\tprivate addEventListeners(): void {\n\t\tthis.addEventListener(\"click\", (event) => {\n\t\t\tconst target = event.target;\n\t\t\tif (target instanceof HTMLButtonElement) {\n\t\t\t\t// temp variable\n\t\t\t\t// must trigger action-table page change if it changes\n\t\t\t\tlet page: number = 1;\n\n\t\t\t\tif (target.dataset.page) {\n\t\t\t\t\t// set the current page before setting the current page on the action table so that it doesn't rerender when setProps is returned\n\t\t\t\t\tpage = Number(target.dataset.page);\n\n\t\t\t\t\ttarget.classList.add(\"active\");\n\t\t\t\t\tthis.querySelectorAll(\"button\").forEach((button) => {\n\t\t\t\t\t\tif (button !== target) {\n\t\t\t\t\t\t\tbutton.classList.remove(\"active\");\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t// temp variables\n\t\t\t\t// Must rerender if the group changes\n\t\t\t\tlet group = this.group;\n\n\t\t\t\tconst hasClass = (className: string) => {\n\t\t\t\t\treturn target.classList.contains(className);\n\t\t\t\t};\n\t\t\t\tif (hasClass(\"next\")) {\n\t\t\t\t\tgroup++;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"prev\")) {\n\t\t\t\t\tgroup--;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"first\")) {\n\t\t\t\t\tgroup = 1;\n\t\t\t\t}\n\t\t\t\tif (hasClass(\"last\")) {\n\t\t\t\t\tgroup = this.maxGroups;\n\t\t\t\t}\n\n\t\t\t\tthis.actionTable.page = this.page = page;\n\t\t\t\tthis.changeLabel(page);\n\n\t\t\t\tif (this.group !== group) {\n\t\t\t\t\tthis.group = group;\n\t\t\t\t\tthis.render();\n\t\t\t\t}\n\t\t\t\t// }\n\t\t\t}\n\t\t});\n\n\t\tthis.actionTable.addEventListener(\"action-table\", (e) => {\n\t\t\tconst { page, pagination, numberOfPages, rowsVisible } = e.detail;\n\t\t\tconsole.log(\"action-table pagination\", e.detail);\n\t\t\tif (\n\t\t\t\t(page && page !== this.page) ||\n\t\t\t\t(numberOfPages !== undefined && numberOfPages !== this.numberOfPages) ||\n\t\t\t\tpagination !== undefined ||\n\t\t\t\trowsVisible !== this.rowsVisible\n\t\t\t) {\n\t\t\t\tconsole.log(\"action-table pagination render\", page, this.page, pagination, numberOfPages, this.numberOfPages);\n\t\t\t\tthis.render();\n\t\t\t}\n\t\t});\n\t}\n}\n\ncustomElements.define(\"action-table-pagination\", ActionTablePagination);\n"],"names":["ActionTablePaginationOptions","actionTable","pagination","paginationOptions","options","opt","paginationSelect","e","value","paginationArray","item","p","ActionTablePagination","__publicField","page","numberOfPages","maxButtons","maxGroups","group","startIndex","pageButton","i","className","text","paginatedButtons","classAttr","suffix","rowsSet","labelStr","labelSpan","event","target","button","hasClass","rowsVisible"],"mappings":"wKAEO,MAAMA,UAAqC,WAAY,CAC7D,aAAc,CACP,QACA,MAAAC,EAAc,KAAK,QAAQ,cAAc,EACzC,CAAE,WAAAC,CAAe,EAAAD,EACjBE,EAAqBC,GAAsBA,EAAQ,IAAKC,GAAQ,WAAWH,IAAeG,EAAM,WAAa,EAAE,IAAIA,CAAG,WAAW,EAAE,KAAK,EAAE,EAE1IC,EACL,KAAK,QAAQ,OAAS,EACnB,0CAA0C,KAAK,aAAa,OAAO,GAAK,WAAW,mBAAmBH,EAAkB,KAAK,OAAO,CAAC,oBACrI,GAEJ,KAAK,UAAYG,EACZ,KAAA,iBAAiB,SAAWC,GAAM,CAClC,GAAAA,EAAE,kBAAkB,kBAAmB,CAC1C,MAAMC,EAAQ,OAAOD,EAAE,OAAO,KAAK,EAC9B,MAAMC,CAAK,IACfP,EAAY,WAAaO,EAE3B,CAAA,CACA,CACF,CAEA,IAAI,SAAoB,CACjB,MAAAJ,EAAU,KAAK,aAAa,SAAS,EAC3C,GAAIA,EAAS,CACZ,MAAMK,EAAkBL,EACtB,MAAM,GAAG,EACT,IAAKM,GAAS,OAAOA,CAAI,CAAC,EAC1B,OAAQC,GAAM,CAAC,MAAMA,CAAC,CAAC,EACrB,GAAAF,EAAgB,OAAS,EACrB,OAAAA,CAET,CACA,MAAO,EACR,CACD,CAEA,eAAe,OAAO,kCAAmCT,CAA4B,ECrC9E,MAAMY,UAA8B,WAAY,CACtD,aAAc,CACP,QAICC,EAAA,YAAO,GACPA,EAAA,qBAAgB,GAChBA,EAAA,aAAQ,GACRA,EAAA,iBAAY,GACZA,EAAA,mBAAc,KAAK,QAAQ,cAAc,GACzCA,EAAA,mBAAc,GARrB,KAAK,kBAAkB,CACxB,CASO,mBAA0B,CAChC,KAAK,OAAO,CACb,CAEO,QAAS,CAGf,KAAM,CAAE,KAAAC,EAAM,cAAAC,GAAkB,KAAK,YAErC,KAAK,cAAgBA,EACrB,KAAK,KAAOD,EAEZ,MAAME,EAAa,OAAO,KAAK,aAAa,aAAa,CAAC,GAAK,GACzDC,EAAY,KAAK,KAAKF,EAAgBC,CAAU,EACtD,IAAIE,EAAQ,KAAK,MAEbA,EAAQD,EACHC,EAAAD,EACEC,EAAQ,IACVA,EAAA,GAGH,MAAAC,GAAcD,EAAQ,GAAKF,EAAa,EAO9C,SAASI,EAAWC,EAAWC,EAAoB,GAAIC,EAAuB,CAC7E,MAAO,gCAAgCT,IAASO,EAAI,UAAUC,CAAS,GAAK,GAAGA,CAAS,EAAE,gBAAgBD,CAAC,YAAYC,CAAS,KAAKC,GAAQF,CAAC,WAC/I,CAIA,IAAIG,EAAmB,GAMvB,GAJIN,EAAQ,IACSM,GAAA,GAAGJ,EAAW,EAAG,OAAO,CAAC,GAAGA,EAAWD,EAAa,EAAG,OAAQ,KAAK,CAAC,IAGtFJ,EAAgB,GAEnB,QAASM,EAAIF,EAAYE,GAAKN,EAAeM,IAG5C,GADAG,GAAoBJ,EAAWC,CAAC,EAC5BA,IAAMN,GAAiBM,GAAKL,EAAaE,EAAO,CAC/BM,GAAA,GAAGJ,EAAWC,EAAI,EAAG,OAAQ,KAAK,CAAC,GAAGD,EAAWL,EAAe,MAAM,CAAC,GAC3F,KACD,EAIF,MAAMU,EAAaC,GAAmB,sBAAsBA,CAAM,IAE7D,KAAA,UAAY,QAAQD,EAAU,OAAO,CAAC,iBAAiBA,EAAU,SAAS,CAAC,IAAID,CAAgB,UACpG,KAAK,YAAYV,CAAI,EAGrB,KAAK,MAAQI,EACb,KAAK,UAAYD,CAClB,CAEQ,YAAYH,EAAc,CACjC,KAAM,CAAE,WAAAZ,EAAY,QAAAyB,GAAY,KAAK,YAErC,KAAK,YAAcA,EAAQ,KAI3B,MAAMC,GAFQ,KAAK,aAAa,OAAO,GAAK,8BAErB,QAAQ,SAAU,GAAGd,EAAOZ,EAAaA,EAAa,CAAC,IAAIY,EAAOZ,CAAU,EAAE,EAAE,QAAQ,UAAW,GAAGyB,EAAQ,IAAI,EAAE,EAErIE,EAAY,KAAK,cAAc,uBAAuB,EACxDA,IAAWA,EAAU,YAAcD,EACxC,CAEQ,mBAA0B,CAC5B,KAAA,iBAAiB,QAAUE,GAAU,CACzC,MAAMC,EAASD,EAAM,OACrB,GAAIC,aAAkB,kBAAmB,CAGxC,IAAIjB,EAAe,EAEfiB,EAAO,QAAQ,OAEXjB,EAAA,OAAOiB,EAAO,QAAQ,IAAI,EAE1BA,EAAA,UAAU,IAAI,QAAQ,EAC7B,KAAK,iBAAiB,QAAQ,EAAE,QAASC,GAAW,CAC/CA,IAAWD,GACPC,EAAA,UAAU,OAAO,QAAQ,CACjC,CACA,GAIF,IAAId,EAAQ,KAAK,MAEX,MAAAe,EAAYX,GACVS,EAAO,UAAU,SAAST,CAAS,EAEvCW,EAAS,MAAM,GAClBf,IAEGe,EAAS,MAAM,GAClBf,IAEGe,EAAS,OAAO,IACXf,EAAA,GAELe,EAAS,MAAM,IAClBf,EAAQ,KAAK,WAGT,KAAA,YAAY,KAAO,KAAK,KAAOJ,EACpC,KAAK,YAAYA,CAAI,EAEjB,KAAK,QAAUI,IAClB,KAAK,MAAQA,EACb,KAAK,OAAO,EAGd,CAAA,CACA,EAED,KAAK,YAAY,iBAAiB,eAAiBX,GAAM,CACxD,KAAM,CAAE,KAAAO,EAAM,WAAAZ,EAAY,cAAAa,EAAe,YAAAmB,GAAgB3B,EAAE,QAGzDO,GAAQA,IAAS,KAAK,MACtBC,IAAkB,QAAaA,IAAkB,KAAK,eACvDb,IAAe,QACfgC,IAAgB,KAAK,cAGrB,KAAK,OAAO,CACb,CACA,CACF,CACD,CAEA,eAAe,OAAO,0BAA2BtB,CAAqB"} -------------------------------------------------------------------------------- /docs/action-table-switch.js: -------------------------------------------------------------------------------- 1 | class t extends HTMLElement{constructor(){super(),this.render(),this.addEventListeners()}get checked(){return this.hasAttribute("checked")}set checked(e){e?this.setAttribute("checked",""):this.removeAttribute("checked")}get label(){return this.getAttribute("label")||"switch"}get name(){return this.getAttribute("name")||""}get value(){return this.getAttribute("value")||"on"}addEventListeners(){const e=this.querySelector("input");e&&e.addEventListener("change",()=>{this.checked=e.checked,this.sendEvent()})}async sendEvent(){const e={checked:this.checked,id:this.id||this.dataset.id,name:this.name,value:this.value};this.dispatchEvent(new CustomEvent("action-table-switch",{detail:e,bubbles:!0}))}render(){const e=document.createElement("input");e.type="checkbox",e.name=this.name,e.value=this.value,e.checked=this.checked,e.setAttribute("aria-label",this.label),this.replaceChildren(e)}}customElements.define("action-table-switch",t); 2 | //# sourceMappingURL=action-table-switch.js.map 3 | -------------------------------------------------------------------------------- /docs/action-table-switch.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"action-table-switch.js","sources":["../src/action-table-switch.ts"],"sourcesContent":["/* -------------------------------------------------------------------------- */\n/* Action Table Switch */\n/* -------------------------------------------------------------------------- */\n/* ----- Optional element added as an example to be extended by the user ---- */\n\nexport class ActionTableSwitch extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.render();\n\t\tthis.addEventListeners();\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Attributes */\n\t/* -------------------------------------------------------------------------- */\n\n\tget checked(): boolean {\n\t\treturn this.hasAttribute(\"checked\");\n\t}\n\tset checked(value: boolean) {\n\t\tif (value) {\n\t\t\tthis.setAttribute(\"checked\", \"\");\n\t\t} else {\n\t\t\tthis.removeAttribute(\"checked\");\n\t\t}\n\t}\n\tget label(): string {\n\t\treturn this.getAttribute(\"label\") || \"switch\";\n\t}\n\n\tget name(): string {\n\t\treturn this.getAttribute(\"name\") || \"\";\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute(\"value\") || \"on\";\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Event Listeners */\n\t/* -------------------------------------------------------------------------- */\n\n\tprivate addEventListeners() {\n\t\tconst input = this.querySelector(\"input\");\n\t\tif (input) {\n\t\t\tinput.addEventListener(\"change\", () => {\n\t\t\t\tthis.checked = input.checked;\n\t\t\t\tthis.sendEvent();\n\t\t\t});\n\t\t}\n\t}\n\n\t/* -------------------------------------------------------------------------- */\n\t/* Private Methods */\n\t/* -------------------------------------------------------------------------- */\n\n\t/* ----------------- Send Event Triggered by checkbox change ---------------- */\n\n\tprivate async sendEvent() {\n\t\tconst detail = { checked: this.checked, id: this.id || this.dataset.id, name: this.name, value: this.value };\n\t\tthis.dispatchEvent(new CustomEvent(\"action-table-switch\", { detail, bubbles: true }));\n\t}\n\n\tprivate render(): void {\n\t\tconst checkbox = document.createElement(\"input\");\n\t\tcheckbox.type = \"checkbox\";\n\t\tcheckbox.name = this.name;\n\t\tcheckbox.value = this.value;\n\t\tcheckbox.checked = this.checked;\n\t\tcheckbox.setAttribute(\"aria-label\", this.label);\n\t\tthis.replaceChildren(checkbox);\n\t}\n}\n\ncustomElements.define(\"action-table-switch\", ActionTableSwitch);\n"],"names":["ActionTableSwitch","value","input","detail","checkbox"],"mappings":"AAKO,MAAMA,UAA0B,WAAY,CAClD,aAAc,CACP,QACN,KAAK,OAAO,EACZ,KAAK,kBAAkB,CACxB,CAMA,IAAI,SAAmB,CACf,OAAA,KAAK,aAAa,SAAS,CACnC,CACA,IAAI,QAAQC,EAAgB,CACvBA,EACE,KAAA,aAAa,UAAW,EAAE,EAE/B,KAAK,gBAAgB,SAAS,CAEhC,CACA,IAAI,OAAgB,CACZ,OAAA,KAAK,aAAa,OAAO,GAAK,QACtC,CAEA,IAAI,MAAe,CACX,OAAA,KAAK,aAAa,MAAM,GAAK,EACrC,CAEA,IAAI,OAAgB,CACZ,OAAA,KAAK,aAAa,OAAO,GAAK,IACtC,CAMQ,mBAAoB,CACrB,MAAAC,EAAQ,KAAK,cAAc,OAAO,EACpCA,GACGA,EAAA,iBAAiB,SAAU,IAAM,CACtC,KAAK,QAAUA,EAAM,QACrB,KAAK,UAAU,CAAA,CACf,CAEH,CAQA,MAAc,WAAY,CACzB,MAAMC,EAAS,CAAE,QAAS,KAAK,QAAS,GAAI,KAAK,IAAM,KAAK,QAAQ,GAAI,KAAM,KAAK,KAAM,MAAO,KAAK,OAChG,KAAA,cAAc,IAAI,YAAY,sBAAuB,CAAE,OAAAA,EAAQ,QAAS,EAAM,CAAA,CAAC,CACrF,CAEQ,QAAe,CAChB,MAAAC,EAAW,SAAS,cAAc,OAAO,EAC/CA,EAAS,KAAO,WAChBA,EAAS,KAAO,KAAK,KACrBA,EAAS,MAAQ,KAAK,MACtBA,EAAS,QAAU,KAAK,QACfA,EAAA,aAAa,aAAc,KAAK,KAAK,EAC9C,KAAK,gBAAgBA,CAAQ,CAC9B,CACD,CAEA,eAAe,OAAO,sBAAuBJ,CAAiB"} -------------------------------------------------------------------------------- /docs/action-table.css: -------------------------------------------------------------------------------- 1 | action-table{display:block;--highlight: paleturquoise;--focus: dodgerblue;--star-checked: orange;--star-unchecked: gray;--switch-checked: green;--switch-unchecked: lightgray;--border: 1px solid lightgray;--border-radius: .2em;--th-bg: whitesmoke;--th-sorted: rgb(244, 220, 188);--col-sorted: rgb(255, 253, 240);--td-options-bg: whitesmoke;--page-btn: whitesmoke;--page-btn-active: rgb(244, 220, 188);--responsive-scroll-gradient: linear-gradient(to right, #fff 30%, rgba(255, 255, 255, 0)), linear-gradient(to right, rgba(255, 255, 255, 0), #fff 70%) 0 100%, radial-gradient(farthest-side at 0% 50%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, .2), rgba(0, 0, 0, 0)) 0 100%}action-table :where(table){border-collapse:collapse;margin:1em 0;max-width:100%;overflow:auto;display:block;background:var(--responsive-scroll-gradient);background-repeat:no-repeat;background-size:40px 100%,40px 100%,14px 100%,14px 100%;background-position:0 0,100%,0 0,100%;background-attachment:local,local,scroll,scroll}action-table :where(th){border:var(--border);padding:0;text-align:left;background:var(--th-bg)}action-table :where(th[no-sort]){padding:.2em .5em}action-table :where(th button){cursor:pointer;font-weight:700;border:0;width:100%;height:100%;display:block;padding:.2em 1.5em .2em .5em;background-color:transparent;position:relative;text-align:left;font-size:inherit;line-height:inherit}action-table :where(th button:hover,th:has(button):hover,th button:focus,th:has(button):focus){background-color:var(--highlight)}action-table :where(th button):after{content:"";background-image:url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath d='M9 16.172l-6.071-6.071-1.414 1.414 8.485 8.485 8.485-8.485-1.414-1.414-6.071 6.071v-16.172h-2z'%3E%3C/path%3E%3C/svg%3E%0A");background-repeat:no-repeat;background-position:center right;background-size:.7em;height:.7em;width:.7em;display:block;opacity:.2;position:absolute;right:.4em;top:50%;transform:translateY(-50%);float:right;transition:transform .3s ease-in-out,opacity .3s ease-in-out}action-table :where(th[aria-sort$=ing] button,th[aria-sort$=ing]:has(button)){background-color:var(--th-sorted)}action-table :where(th[aria-sort$=ing] button):after{opacity:1}action-table :where(th[aria-sort=descending] button):after{opacity:1;transform:translateY(-50%) rotate(180deg)}action-table :where(td){border:var(--border);padding:.2em .4em}action-table .sorted{background-color:var(--col-sorted)}action-table :where(td span){background-color:var(--td-options-bg);padding:0 .4em .1em;margin:0 .2em}action-table:not(:defined),action-table-filters:not(:defined){visibility:hidden}action-table :where(select,input,button){font-size:inherit}action-table :where(input[type=search],select){border:var(--border);border-radius:var(--border-radius)}action-table .selected{background-color:var(--highlight);transition:color .2s ease}action-table .no-results :where(td){padding:1em;text-align:center}action-table :where(button){cursor:pointer}action-table-filter-menu,action-table-filter-switch label,action-table-filter-menu .filter-label{display:inline-flex;flex-wrap:wrap;align-items:center}action-table-filter-menu label,action-table-filter-menu .filter-label{margin-inline-end:.3em}action-table .switch label{display:inline-flex;align-items:center;margin:0}action-table .switch input{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;display:inline-block;background:var(--switch-unchecked);cursor:pointer;height:1.4em;width:2.75em;vertical-align:middle;border-radius:2em;box-shadow:0 1px 3px #0003 inset;transition:.25s linear background}action-table .switch input:before{content:"";display:block;width:1em;height:1em;background:#fff;border-radius:1em;position:absolute;top:.2em;left:.2em;box-shadow:0 1px 3px #0003;transition:.25s linear transform;transform:translate(0)}action-table .switch :checked{background:var(--switch-checked)}action-table .switch :checked:before{transform:translate(1.3em)}action-table .switch input:focus,action-table .star input:focus{outline:transparent}action-table .switch input:focus-visible,action-table .star input:focus-visible{outline:2px solid var(--focus);outline-offset:2px}action-table .star input{-webkit-appearance:none;-moz-appearance:none;appearance:none;position:relative;cursor:pointer;height:1.6em;width:1.6em;vertical-align:middle;border-radius:.3em}action-table .star input:before{content:"";background:var(--star-unchecked);cursor:pointer;position:absolute;height:1.6em;width:1.6em;vertical-align:middle;transition:.25s linear background;clip-path:polygon(50% 0%,62% 29%,98% 35%,74% 58%,79% 91%,50% 76%,21% 91%,26% 58%,2% 35%,34% 29%)}action-table .star input:checked:before{background:var(--star-checked)}action-table-pagination{display:flex;justify-content:start;align-items:center;flex-wrap:wrap;gap:.6em;max-width:100%;overflow:auto}action-table-pagination .pagination-buttons{display:flex;justify-content:start;align-items:center;gap:.3em}action-table-pagination button{cursor:pointer;font-size:inherit;background-color:var(--page-btn);border:0;border-radius:.3em;padding:.2em .5em}action-table-pagination button:hover{background-color:var(--highlight)}action-table-pagination .active{font-weight:700;background-color:var(--page-btn-active)}@keyframes fade-in{0%{opacity:0}}@keyframes fade-out{to{opacity:0}}@keyframes slide-from-bottom{0%{transform:translateY(50px)}}@keyframes slide-to-top{to{transform:translateY(-50px)}}::view-transition-old(row){animation:90ms cubic-bezier(.4,0,1,1) both fade-out,.3s cubic-bezier(.4,0,.2,1) both slide-to-top}::view-transition-new(row){animation:1.21s cubic-bezier(0,0,.2,1) 90ms both fade-in,.3s cubic-bezier(.4,0,.2,1) both slide-from-bottom}action-table-filter-range{--thumb-size: 1.3em;--thumb-bg: #fff;--thumb-border: solid 2px #9e9e9e;--thumb-shadow: 0 1px 4px .5px rgba(0, 0, 0, .25);--thumb-highlight: var(--highlight);--track-bg: lightgray;--track-shadow: inset 0 0 2px #00000099;--track-highlight: var(--highlight);--ticks-color: #b2b2b2;--ticks-width: 1}action-table-filter-range,action-table-filter-range>div,action-table-filter-range label{display:flex;align-items:center;flex-wrap:wrap;gap:.6em}action-table-filter-range .range-slider-group{display:grid}action-table-filter-range .range-slider-group>*{grid-column:1;grid-row:1;position:relative}action-table-filter-range .range-slider-group:after{content:"";width:100%;height:.5em;background-color:var(--track-bg);border-radius:50px;background-size:100% 2px;box-shadow:var(--track-shadow);grid-column:1;grid-row:1;justify-self:center}action-table-filter-range .range-slider-highlight{background-color:var(--track-highlight);width:100%;height:.35em}action-table-filter-range input{-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;width:100%;background:transparent;padding:.2em 0;pointer-events:none;cursor:-webkit-grab;cursor:grab}action-table-filter-range input::-webkit-slider-runnable-track{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent}action-table-filter-range input::-webkit-slider-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);border-radius:50%;background:var(--thumb-bg);border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-moz-range-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);background:var(--thumb-bg);border-radius:50%;border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-ms-thumb{-webkit-appearance:none;height:var(--thumb-size);width:var(--thumb-size);background:var(--thumb-bg);border-radius:50%;border:var(--thumb-border);box-shadow:var(--thumb-shadow);pointer-events:auto}action-table-filter-range input::-webkit-slider-thumb:hover{background:var(--thumb-highlight)}action-table-filter-range svg{color:var(--ticks-color);position:relative;top:-.6em;width:calc(100% - var(--thumb-size));justify-self:center;border-left:1px solid var(--ticks-color);border-right:1px solid var(--ticks-color);box-sizing:border-box} 2 | -------------------------------------------------------------------------------- /docs/action-table.js: -------------------------------------------------------------------------------- 1 | var g=Object.defineProperty;var p=(c,l,t)=>l in c?g(c,l,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[l]=t;var h=(c,l,t)=>(p(c,typeof l!="symbol"?l+"":l,t),t);class m extends HTMLElement{constructor(){super();const l=this.closest("action-table");this.style.display="none",this.addEventListener("click",t=>{t.target instanceof HTMLButtonElement&&t.target.type==="reset"&&(this.dispatchEvent(new CustomEvent("action-table-filter",{bubbles:!0})),this.dispatchEvent(new CustomEvent("action-table-filters-reset",{bubbles:!0})))}),l.addEventListener("action-table",t=>{const e=t.detail;(e==null?void 0:e.rowsVisible)===0?this.style.display="":this.style.display="none"})}}customElements.define("action-table-no-results",m);function y(c){return Object.keys(c).length>0}class C extends HTMLElement{constructor(){var t;super();h(this,"table");h(this,"tbody");h(this,"cols",[]);h(this,"rows",[]);h(this,"filters",{});h(this,"tableContent",new WeakMap);h(this,"rowsSet",new Set);h(this,"sortAndFilter",this.delayUntilNoLongerCalled(()=>{this.filterTable(),this.sortTable(),this.appendRows(),this.tbody.matches("[style*=none]")&&(this.rowsSet.size===0&&(this.setFilters(),this.dispatchEvent(new Event("action-table-filters-reset"))),this.tbody.style.display="")}));if(this.store){const e=this.getStore();e&&(this.sort=e.sort||this.sort,this.direction=e.direction||this.direction||"ascending",this.filters=e.filters||this.filters)}if(this.hasAttribute("urlparams")){const e=new URLSearchParams(window.location.search),s={};for(let[i,r]of e.entries())i=i.toLowerCase(),r=r.toLowerCase(),i!=="sort"&&i!=="direction"&&((t=s[i])!=null&&t.values?s[i].values.push(r):s[i]={values:[r]}),i==="sort"&&(this.sort=r),i==="direction"&&(r==="ascending"||r==="descending")&&(this.direction=r);Object.keys(s).length>0&&this.setFiltersObject(s)}this.addEventListeners()}get sort(){return this.getCleanAttr("sort")}set sort(t){this.setAttribute("sort",t)}get direction(){const t=this.getCleanAttr("direction");return t==="descending"?t:"ascending"}set direction(t){this.setAttribute("direction",t)}get store(){return this.hasAttribute("store")?this.getCleanAttr("store")||"action-table":""}get pagination(){return Number(this.getCleanAttr("pagination"))||0}set pagination(t){this.setAttribute("pagination",t.toString())}get page(){return Number(this.getCleanAttr("page"))||1}set page(t){t=this.checkPage(t),this.setAttribute("page",t.toString())}checkPage(t){return Math.max(1,Math.min(t,this.numberOfPages))}dispatch(t){this.dispatchEvent(new CustomEvent("action-table",{detail:t}))}getCleanAttr(t){var e;return((e=this.getAttribute(t))==null?void 0:e.trim().toLowerCase())||""}connectedCallback(){const t=this.querySelector("table");if(t&&t.querySelector("thead th")&&t.querySelector("tbody td"))this.table=t,this.tbody=t.querySelector("tbody"),this.rows=Array.from(this.tbody.querySelectorAll("tr")),this.rowsSet=new Set(this.rows);else throw new Error("Could not find table with thead and tbody");(this.sort||y(this.filters))&&(this.tbody.style.display="none",this.sortAndFilter()),this.getColumns(),this.addObserver()}static get observedAttributes(){return["sort","direction","pagination","page"]}attributeChangedCallback(t,e,s){e!==s&&this.rows.length>0&&((t==="sort"||t==="direction")&&this.sortTable(),t==="pagination"&&this.dispatch({pagination:this.pagination}),this.appendRows())}setFiltersObject(t={}){this.filters=t,this.store&&this.setStore({filters:this.filters})}setFilters(t={}){this.setFiltersObject(t),this.filterTable(),this.appendRows()}addEventListeners(){this.addEventListener("click",e=>{const s=e.target;if(s instanceof HTMLButtonElement&&s.dataset.col){const i=s.dataset.col;let r="ascending";this.sort===i&&this.direction==="ascending"&&(r="descending"),this.sort=i,this.direction=r,this.store&&this.setStore({sort:this.sort,direction:r})}},!1);const t=e=>e.matches("td")?e:e.closest("td");this.addEventListener("change",e=>{const s=e.target;s instanceof HTMLInputElement&&s.closest("td")&&s.type==="checkbox"&&this.updateCellValues(t(s))}),this.addEventListener("action-table-filter",e=>{if(e.detail){const s={...this.filters,...e.detail};Object.keys(s).forEach(i=>{s[i].values.every(r=>r==="")&&delete s[i]}),this.setFilters(s)}else this.setFilters()}),this.addEventListener("action-table-update",e=>{const s=e.target;if(s instanceof HTMLElement){let i={};typeof e.detail=="string"?i={sort:e.detail,filter:e.detail}:i=e.detail,this.updateCellValues(t(s),i)}})}getStore(){try{const t=localStorage.getItem(this.store),e=t&&JSON.parse(t);return typeof e=="object"&&e!==null&&["sort","direction","filters"].some(i=>i in e)?e:!1}catch{return!1}}setStore(t){const e=this.getStore()||{};e&&(t={...e,...t}),localStorage.setItem(this.store,JSON.stringify(t))}delayUntilNoLongerCalled(t){let e,s=!1;function i(){t(),s=!1}return function(){s?clearTimeout(e):s=!0,e=setTimeout(i,10)}}getColumns(){const t=this.table.querySelectorAll("thead th");if(t.forEach(e=>{const s=(e.dataset.col||this.getCellContent(e)).trim().toLowerCase(),i=e.dataset.order?e.dataset.order.split(","):void 0;if(this.cols.push({name:s,order:i}),!e.hasAttribute("no-sort")){const r=document.createElement("button");r.dataset.col=s,r.type="button",r.innerHTML=e.innerHTML,e.replaceChildren(r)}}),!this.table.querySelector("colgroup")){const e=document.createElement("colgroup");t.forEach(()=>{const s=document.createElement("col");e.appendChild(s)}),this.table.prepend(e)}}getCellContent(t){var s;let e=(t.textContent||"").trim();if(!e){const i=t.querySelector("svg");i instanceof SVGElement&&(e=((s=i.querySelector("title"))==null?void 0:s.textContent)||e);const r=t.querySelector("input[type=checkbox]");r instanceof HTMLInputElement&&r.checked&&(e=r.value);const n=t.querySelector(":defined");n!=null&&n.shadowRoot&&(e=n.shadowRoot.textContent||e)}return e.trim()}getCellValues(t){return this.tableContent.has(t)?this.tableContent.get(t):this.setCellValues(t)}setCellValues(t,e={}){const s=this.getCellContent(t),i={sort:t.dataset.sort||s,filter:t.dataset.filter||s,...e};return this.tableContent.set(t,i),i}updateCellValues(t,e={}){this.setCellValues(t,e),this.sortAndFilter()}addObserver(){new MutationObserver(e=>{e.forEach(s=>{let i=s.target;if(i.nodeType===3&&i.parentNode&&(i=i.parentNode),!(i instanceof HTMLElement))return;const r=i.closest("td");r instanceof HTMLTableCellElement&&(r.hasAttribute("contenteditable")&&r===document.activeElement?r.dataset.edit||(r.dataset.edit="true",r.addEventListener("blur",()=>{this.updateCellValues(r)})):this.updateCellValues(r))})}).observe(this.tbody,{childList:!0,subtree:!0,attributes:!0,characterData:!0,attributeFilter:["data-sort","data-filter"]})}filterTable(){const t=this.numberOfPages,e=this.rowsSet.size,s=this.filters["action-table"];this.rows.forEach(i=>{let r=!1;const n=i.querySelectorAll("td");if(s){const o=Array.from(n).filter((a,d)=>this.filters["action-table"].cols?this.filters["action-table"].cols.includes(this.cols[d].name.toLowerCase()):!0).map(a=>a.querySelector('input[type="checkbox"]')?"":this.getCellValues(a).filter).join(" ");this.shouldHide(s,o)&&(r=!0)}n.forEach((u,o)=>{const a=this.filters[this.cols[o].name];a&&this.shouldHide(a,this.getCellValues(u).filter)&&(r=!0)}),r?this.rowsSet.delete(i):this.rowsSet.add(i)}),this.numberOfPages!==t&&this.dispatch({numberOfPages:this.numberOfPages}),this.rowsSet.size!==e&&this.dispatch({rowsVisible:this.rowsSet.size})}shouldHide(t,e){if(t.values&&t.values.length>0){if(t.regex){let s=t.values.join("|");return t.exclusive&&(s=`${t.values.map(n=>`(?=.*${n})`).join("")}.*`),!new RegExp(s,"i").test(e)}if(t.range){const[s,i]=t.values;if(!isNaN(Number(s))&&!isNaN(Number(i)))return Number(e)Number(i)}return t.exclusive?!t.values.every(s=>e.toLowerCase().includes(s.toLowerCase())):t.exact?t.values.every(s=>s&&s!==e):!t.values.some(s=>e.toLowerCase().includes(s.toLowerCase()))}return!1}sortTable(t=this.sort,e=this.direction){if(!this.sort||!e)return;t=t.toLowerCase();const s=this.cols.findIndex(i=>i.name===t);if(s>=0&&this.rows.length>0){const i=this.cols[s].order,r=o=>i!=null&&i.includes(o)?i.indexOf(o).toString():o;this.rows.sort((o,a)=>{if(e==="descending"){const b=o;o=a,a=b}const d=r(this.getCellValues(o.children[s]).sort),f=r(this.getCellValues(a.children[s]).sort);return this.alphaNumSort(d,f)}),this.querySelectorAll("col").forEach((o,a)=>{a===s?o.classList.add("sorted"):o.classList.remove("sorted")}),this.table.querySelectorAll("thead th").forEach((o,a)=>{const d=a===s?e:"none";o.setAttribute("aria-sort",d)})}}alphaNumSort(t,e){function s(n){if(isNaN(Number(n))){if(!isNaN(Date.parse(n)))return Date.parse(n)}else return Number(n)}const i=s(t),r=s(e);return i&&r?i-r:t.localeCompare(e)}appendRows(){const t=i=>{const{pagination:r,page:n}=this;return r===0||i>=r*(n-1)+1&&i<=r*n},e=document.createDocumentFragment();let s=0;if(this.rows.forEach(i=>{let r="none";this.rowsSet.has(i)&&(s++,t(s)&&(r="",e.appendChild(i))),i.style.display=r}),this.tbody.prepend(e),this.pagination>0){const i=this.checkPage(this.page);i!==this.page&&(this.page=i,this.dispatch({page:i}))}}get numberOfPages(){return this.pagination>0?Math.ceil(this.rowsSet.size/this.pagination):1}}customElements.define("action-table",C); 2 | //# sourceMappingURL=action-table.js.map 3 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import"./action-table.js";import"./action-table-filters.js";import"./action-table-pagination.js"; 2 | //# sourceMappingURL=index.js.map 3 | -------------------------------------------------------------------------------- /docs/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""} -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | var m=Object.defineProperty;var a=(n,e,t)=>e in n?m(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var o=(n,e,t)=>(a(n,typeof e!="symbol"?e+"":e,t),t);import"./index.js";import"./action-table-switch.js";import"./action-table.js";import"./action-table-filters.js";import"./action-table-pagination.js";class i extends HTMLElement{constructor(){super();o(this,"randomNumber");const t=this.attachShadow({mode:"open"});this.randomNumber=Math.floor(Math.random()*10)+1,t.innerHTML=`${this.randomNumber}`,this.dispatch(this.randomNumber.toString()),this.addEventListener("click",()=>{this.randomNumber=Math.floor(Math.random()*10)+1,t.innerHTML=`${this.randomNumber}`,this.dispatch(this.randomNumber.toString())})}dispatch(t){const r=new CustomEvent("action-table-update",{detail:t,bubbles:!0});this.dispatchEvent(r)}}customElements.define("random-number",i); 2 | //# sourceMappingURL=main.js.map 3 | -------------------------------------------------------------------------------- /docs/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sources":["../src/random-number.ts"],"sourcesContent":["class RandomNumberComponent extends HTMLElement {\n\tprivate randomNumber: number;\n\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: \"open\" });\n\t\tthis.randomNumber = Math.floor(Math.random() * 10) + 1;\n\t\tshadow.innerHTML = `${this.randomNumber}`;\n\t\tthis.dispatch(this.randomNumber.toString());\n\n\t\t// click event that rerandomizes the number\n\t\tthis.addEventListener(\"click\", () => {\n\t\t\tthis.randomNumber = Math.floor(Math.random() * 10) + 1;\n\t\t\tshadow.innerHTML = `${this.randomNumber}`;\n\t\t\tthis.dispatch(this.randomNumber.toString());\n\t\t});\n\t}\n\n\tprivate dispatch(detail: { sort: string; filter: string } | string) {\n\t\t// console.log(\"dispatch\", detail);\n\t\tconst event = new CustomEvent<{ sort: string; filter: string } | string>(\"action-table-update\", {\n\t\t\tdetail,\n\t\t\tbubbles: true,\n\t\t});\n\t\tthis.dispatchEvent(event);\n\t}\n}\n\ncustomElements.define(\"random-number\", RandomNumberComponent);\n"],"names":["RandomNumberComponent","__publicField","shadow","detail","event"],"mappings":"6TAAA,MAAMA,UAA8B,WAAY,CAG/C,aAAc,CACP,QAHCC,EAAA,qBAIP,MAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,OAAQ,EACjD,KAAK,aAAe,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,EAC9CA,EAAA,UAAY,GAAG,KAAK,YAAY,GACvC,KAAK,SAAS,KAAK,aAAa,SAAU,CAAA,EAGrC,KAAA,iBAAiB,QAAS,IAAM,CACpC,KAAK,aAAe,KAAK,MAAM,KAAK,OAAO,EAAI,EAAE,EAAI,EAC9CA,EAAA,UAAY,GAAG,KAAK,YAAY,GACvC,KAAK,SAAS,KAAK,aAAa,SAAU,CAAA,CAAA,CAC1C,CACF,CAEQ,SAASC,EAAmD,CAE7D,MAAAC,EAAQ,IAAI,YAAuD,sBAAuB,CAC/F,OAAAD,EACA,QAAS,EAAA,CACT,EACD,KAAK,cAAcC,CAAK,CACzB,CACD,CAEA,eAAe,OAAO,gBAAiBJ,CAAqB"} -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## Refactor how parent child reactivity works 4 | 5 | ### State communications: 6 | 7 | 1. Sort state: column and direction 8 | 1. Sort state is set and reflected via observed attributes on action-table. 9 | 2. Headers are turned into sort buttons which trigger sort state. 10 | 2. Filter state: column and filter values 11 | 1. Set with a filtersObj property which is an object (see FiltersObject in types.ts) 12 | 2. To change filter state, call filterTable() method with or without arguments. Arguments will update filtersObj 13 | 3. Right now, when filtersObj is changed nothing happens reactively. Ideally what should happen is: 14 | 1. It should trigger filterTable() method. 15 | 2. If filtersObj changed by actions-table-filter it should trigger filterTable() method but not update actions-table-filters elements. 16 | 3. If however it is updated by something else like an auto reset then it should update actions-table-filters elements 17 | 3. Pagination state: page number, number of rows per page, number of pages 18 | 1. the actions-table-pagination needs to be render updated based on page number and number of pages 19 | 2. actions-table needs to be updated based on page number and number of rows per page 20 | 3. Right now it's all method calls 21 | 4. actions-table-pagination buttons update page number 22 | 5. actions-table filter updates number of pages based on number of displayed rows and can update page number if the current page is higher than the max page 23 | 6. Ideal is a reactive property which would need trigger the above on changes. Might need a local state to test against the main state so that it doesn't trigger render when not needed. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@colinaut/action-table", 3 | "version": "2.4.21", 4 | "description": "", 5 | "keywords": [ 6 | "web components", 7 | "custom elements", 8 | "components", 9 | "table elements", 10 | "typescript" 11 | ], 12 | "homepage": "https://colinaut.github.io/action-table/", 13 | "repository": "https://github.com/colinaut/action-table", 14 | "license": "MIT", 15 | "author": "Colin Fahrion", 16 | "type": "module", 17 | "exports": { 18 | ".": { 19 | "import": "./dist/index.js", 20 | "types": "./src/types.ts" 21 | }, 22 | "./dist/index.js": "./dist/index.js", 23 | "./dist/action-table.css": "./dist/action-table.css", 24 | "./dist/action-table.js": "./dist/action-table.js", 25 | "./dist/action-table-filters.js": "./dist/action-table-filters.js", 26 | "./dist/action-table-switch.js": "./dist/action-table-switch.js", 27 | "./dist/action-table-pagination.js": "./dist/action-table-pagination.js" 28 | }, 29 | "main": "dist/index.js", 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "build": "tsc && vite build && rm -rf docs && cp -r dist docs", 35 | "dev": "vite", 36 | "preview": "vite preview", 37 | "start": "vite", 38 | "docs": "npx serve docs", 39 | "lint": "eslint .", 40 | "test": "echo \"Error: no test specified\" && exit 1" 41 | }, 42 | "browserslist": [ 43 | "> 1%", 44 | "last 2 versions", 45 | "not dead" 46 | ], 47 | "devDependencies": { 48 | "@typescript-eslint/eslint-plugin": "^6.14.0", 49 | "@typescript-eslint/parser": "^6.14.0", 50 | "eslint": "^8.55.0", 51 | "rollup-plugin-minify-html-literals": "^1.2.6", 52 | "typescript": "^5.3.2", 53 | "vite": "^5.0.2", 54 | "vite-plugin-eslint": "^1.8.1" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /small.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Action Table: HTML Web Component 7 | 8 | 9 | 10 | 11 | 12 | 65 | 66 | 67 |
68 |

Action Table

69 |
70 |

71 | Native HTML web component for adding sort functionality and filtering to html tables. This component does not use the Shadow DOM. Instead it includes a custom 72 | css stylesheet you can use, which you can override or customize. For instructions check out the 73 | action-table github repo. 74 |

75 |
76 |

Example

77 | 78 |
79 | 80 |

Filters

81 |
82 |
83 |

Action Table Filter Elements: Select Menu

84 | 85 | 86 |
Day options based on td data-filter
87 | 88 |
Team options ordered via th data-order
89 |
90 |
91 |

Action Table Filter Elements: Checkbox/Radio

92 | 93 | 94 | 95 |
96 |
97 |

Action Table Filter Elements: Switch

98 |
99 |
100 |
101 |
102 |

Action Table Filter Search

103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 |
113 |

114 |

Test

115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |

123 |
124 | 125 |
126 |
127 | 128 |
129 |
130 |

Table

131 |
Age is using a random-number custom element for some cells for testing purposes.
132 |
Day is sorted via custom td data-sort
133 |
Team is sorted via custom th data-order
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 258 | 259 | 260 | 261 | 262 | 263 | 264 |
AnimalNameColorAgeActionsFavsSwitchDayTeamTest
rabbitAttilawhiteN/Ajumpeat 157 | 158 | Mon, 10am5A1
small dogSophiablack3runeatsnuggle 171 | 172 | Tue, 11am8 man2
rabbitCindybrownN/Ajumprun 186 | 187 | Sat, 1pm6 man3
raccoonSewergrayruneathide 200 | 201 | Sun, 2pm8 man4
dogZiggygrayrunsnuggle 214 | 215 | Mon, 3pm4A5
raccoonWrapperbrowneat 228 | 229 | Sun, 10am2A6
catTommybrownrunexplore 242 | 243 | Tue, 11am3A7
catSteveblackrunsnuggle 256 | 257 | Wed, 12pm1A8
265 | 266 |

267 | No results found 268 |

269 |
270 |
271 |
272 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /src/action-table-filter-menu.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTable } from "./action-table"; 2 | export class ActionTableFilterMenu extends HTMLElement { 3 | constructor() { 4 | super(); 5 | } 6 | 7 | private options: string[] = []; 8 | 9 | private findOptions(columnName: string): void { 10 | // 1. Set column name to lowercase 11 | columnName = columnName.toLowerCase(); 12 | 13 | // 2. Get action table; of not found, return 14 | const actionTable = this.closest("action-table") as ActionTable; 15 | 16 | // 3. Get cols and tbody from actionTable 17 | const cols = actionTable.cols; 18 | const tbody = actionTable.tbody; 19 | 20 | // 4. Find column index based on column name in header data-col attribute; if not found, return 21 | const columnIndex = cols.findIndex((col) => col.name === columnName); 22 | if (columnIndex === -1) { 23 | return; 24 | } 25 | 26 | // 6. Get all cells in column 27 | const columnTDs = `td:nth-child(${columnIndex + 1})`; 28 | const cells = tbody.querySelectorAll(columnTDs) as NodeListOf; 29 | // 7. Create array of options 30 | let options: string[] = []; 31 | 32 | // 8. Review all cells for filter values 33 | Array.from(cells).forEach((cell) => { 34 | const subItems = cell.querySelectorAll(`span, ul > li`) as NodeListOf; 35 | if (!cell.dataset.filter && subItems?.length > 0) { 36 | // 8.3 If subitems exist, get all options in subitems 37 | const subOptions = Array.from(subItems).map((item) => item.textContent || ""); 38 | options = options.concat(subOptions); 39 | } else { 40 | // 8.4 If subitems do not exist, get filter value of cell 41 | options.push(actionTable.getCellValues(cell).filter); 42 | } 43 | }); 44 | 45 | // 8. Make array of all unique options 46 | options = Array.from(new Set(options)); 47 | 48 | const sortOrder = cols[columnIndex].order; 49 | function checkSortOrder(value: string) { 50 | return sortOrder?.includes(value) ? sortOrder.indexOf(value).toString() : value; 51 | } 52 | 53 | // 9. Sort options using action table alpha numeric sort 54 | options.sort((a, b) => { 55 | a = checkSortOrder(a); 56 | b = checkSortOrder(b); 57 | return actionTable.alphaNumSort(a, b); 58 | }); 59 | // 10. reverse order if descending 60 | if (this.hasAttribute("descending")) options.reverse(); 61 | // 11. Set options 62 | this.options = options; 63 | } 64 | 65 | // Using connectedCallback because options may need to be rerendered when added to the DOM 66 | public connectedCallback(): void { 67 | const columnName = this.getAttribute("name"); 68 | // name is required 69 | if (!columnName) return; 70 | // If options are not specified then find them 71 | if (this.hasAttribute("options")) { 72 | this.options = this.getAttribute("options")?.split(",") || []; 73 | } else { 74 | this.findOptions(columnName); 75 | } 76 | this.render(columnName); 77 | } 78 | 79 | private render(columnName: string): void { 80 | if (this.options.length < 1) return; 81 | // Get options from custom element attributes 82 | const type = (this.getAttribute("type") as "select" | "checkbox" | "radio") || "select"; 83 | if (type !== "checkbox" && this.options.length < 2) return; 84 | const label = this.getAttribute("label") || columnName; 85 | const multiple = this.hasAttribute("multiple") ? "multiple" : ""; 86 | const all = this.getAttribute("all") || "All"; 87 | 88 | // Build element 89 | let start = ""; 90 | let end = ""; 91 | const mainLabel = type === "select" ? `` : `${label}`; 92 | // is this is a select menu then add start and end wrapper and an All option 93 | if (type === "select") { 94 | start = ``; 96 | } 97 | // If this is a radio button then add an all option 98 | if (type === "radio") { 99 | start = ``; 100 | } 101 | // add select options, radio buttons, or checkboxes from options 102 | const html = `${mainLabel}${start}${this.options 103 | .map((option) => { 104 | if (type === "select") return ``; 105 | if (type === "radio" || type === "checkbox") return ``; 106 | return ""; 107 | }) 108 | .join("")}${end}`; 109 | 110 | // Add to inner HTML 111 | this.innerHTML = `${html}`; 112 | } 113 | } 114 | 115 | customElements.define("action-table-filter-menu", ActionTableFilterMenu); 116 | -------------------------------------------------------------------------------- /src/action-table-filter-range.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTable } from "./action-table"; 2 | export class ActionTableFilterRange extends HTMLElement { 3 | // private shadow: ShadowRoot; 4 | constructor() { 5 | super(); 6 | // this.shadow = this.attachShadow({ mode: "open" }); 7 | this.render(); 8 | this.addEventListeners(); 9 | } 10 | 11 | get name() { 12 | return this.getAttribute("name") || ""; 13 | } 14 | 15 | private min = 0; 16 | private rangeTotal = 0; 17 | 18 | addEventListeners() { 19 | this.addEventListener("input", (e) => { 20 | const inputs = this.querySelectorAll("input"); 21 | const output = this.querySelector("output"); 22 | const [min, max] = Array.from(inputs).map((input) => Number(input.value)); 23 | const minStr = min.toString(); 24 | const maxStr = max.toString(); 25 | // set content of outputs 26 | if (output instanceof HTMLOutputElement) { 27 | output.textContent = `${minStr}-${maxStr}`; 28 | } 29 | // set side margin of color 30 | const rangeColor = this.querySelector(".range-slider-highlight"); 31 | if (rangeColor instanceof HTMLSpanElement) { 32 | // margin left is percentage of range based on total range distance of current value and min 33 | rangeColor.style.marginLeft = `${((min - this.min) / this.rangeTotal) * 100}%`; 34 | rangeColor.style.width = `${((max - min) / this.rangeTotal) * 100}%`; 35 | } 36 | // reset input values if goes out of range 37 | if (min > max) { 38 | // stop propagation so that it doesn't trigger the action-table-filter event 39 | e.stopPropagation(); 40 | inputs[0].value = maxStr; 41 | inputs[1].value = minStr; 42 | } 43 | }); 44 | } 45 | 46 | findMinMax(): number[] { 47 | const min = this.getAttribute("min"); 48 | const max = this.getAttribute("max"); 49 | if (min && max) { 50 | return [Number(min), Number(max)]; 51 | } 52 | const actionTable = this.closest("action-table") as ActionTable; 53 | // 3. Get cols and tbody from actionTable 54 | const cols = actionTable.cols; 55 | const tbody = actionTable.tbody; 56 | 57 | // 4. Find column index based on column name in header data-col attribute; if not found, return 58 | const columnIndex = cols.findIndex((col) => col.name === this.name.toLowerCase()); 59 | if (columnIndex === -1) { 60 | return [0, 0]; 61 | } 62 | 63 | // 6. Get all cells in column 64 | const columnTDs = `td:nth-child(${columnIndex + 1})`; 65 | const cells = tbody.querySelectorAll(columnTDs) as NodeListOf; 66 | 67 | return Array.from(cells).reduce((total: number[], current) => { 68 | const num = Number(actionTable.getCellValues(current).filter); 69 | let min = total.length === 2 ? total[0] : num; 70 | let max = total.length === 2 ? total[1] : num; 71 | min = min < num ? min : num; 72 | max = max > num ? max : num; 73 | return [min, max]; 74 | }, []); 75 | } 76 | 77 | render() { 78 | const [min, max] = this.findMinMax(); 79 | 80 | const minStr = min.toString(); 81 | const maxStr = max.toString(); 82 | this.rangeTotal = max - min; 83 | this.min = min; 84 | const label = this.getAttribute("label") || this.name; 85 | const labelDiv = document.createElement("div"); 86 | labelDiv.textContent = label; 87 | const group = document.createElement("div"); 88 | group.classList.add("range-slider-group"); 89 | 90 | // TODO: make this variable so number of steps can be adjusted with attribute? 91 | // Each step is rounded to the nearest power of ten ( 10, 100, 1000, etc. ) 92 | const step = Math.pow(10, Math.round(Math.log10(this.rangeTotal))) / 10; 93 | 94 | // Helper function 95 | function setAttributes(element: Element, attributes: Record) { 96 | for (const key in attributes) { 97 | element.setAttribute(key, attributes[key]); 98 | } 99 | } 100 | 101 | // make array of values starting with the min and ending with the max with the steps in between 102 | const values = [min]; 103 | for (let i = min + step; i <= max; i += step) { 104 | values.push(Math.round(i / step) * step); 105 | } 106 | if (!values.includes(max)) values.push(max); 107 | 108 | // Add svg ticks 109 | const svgTicks = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 110 | setAttributes(svgTicks, { 111 | role: "presentation", 112 | width: "100%", 113 | height: "5", 114 | }); 115 | const gaps = 100 / (values.length - 1); 116 | 117 | for (let i = 1; i < values.length - 1; i++) { 118 | const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); 119 | const gap = `${i * gaps}%`; 120 | setAttributes(line, { 121 | x1: gap, 122 | x2: gap, 123 | y1: "0", 124 | y2: "5", 125 | stroke: "currentColor", 126 | "stroke-width": "1", 127 | }); 128 | svgTicks.append(line); 129 | } 130 | 131 | const slideHighlight = document.createElement("span"); 132 | slideHighlight.classList.add("range-slider-highlight"); 133 | group.append(svgTicks, slideHighlight); 134 | 135 | // Add input ranges 136 | for (let i = 0; i <= 1; i++) { 137 | const input = document.createElement("input"); 138 | setAttributes(input, { 139 | type: "range", 140 | name: this.name, 141 | min: minStr, 142 | max: maxStr, 143 | "data-range": i === 0 ? "min" : "max", 144 | "aria-label": i === 0 ? "Min" : "Max", 145 | value: i === 0 ? minStr : maxStr, 146 | }); 147 | group.append(input); 148 | } 149 | 150 | // add output 151 | const output = document.createElement("output"); 152 | output.innerHTML = `${min}–${max}`; 153 | this.append(labelDiv, group, output); 154 | } 155 | } 156 | 157 | customElements.define("action-table-filter-range", ActionTableFilterRange); 158 | -------------------------------------------------------------------------------- /src/action-table-filter-switch.ts: -------------------------------------------------------------------------------- 1 | export class ActionTableFilterSwitch extends HTMLElement { 2 | connectedCallback(): void { 3 | const name = this.getAttribute("name"); 4 | if (name) { 5 | this.innerHTML = ``; 9 | } 10 | } 11 | } 12 | 13 | customElements.define("action-table-filter-switch", ActionTableFilterSwitch); 14 | -------------------------------------------------------------------------------- /src/action-table-filters.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTable } from "./action-table"; 2 | import { FiltersObject } from "./types"; 3 | 4 | export class ActionTableFilters extends HTMLElement { 5 | constructor() { 6 | super(); 7 | this.addEventListeners(); 8 | } 9 | 10 | private actionTable = this.closest("action-table") as ActionTable; 11 | private resetButton = this.querySelector("button[type=reset]") as HTMLButtonElement; 12 | 13 | /* -------------------------------------------------------------------------- */ 14 | /* Connected Callback */ 15 | /* -------------------------------------------------------------------------- */ 16 | public connectedCallback(): void { 17 | // Grab current filters from action-table 18 | const filters: FiltersObject = this.actionTable.filters; 19 | 20 | // 4.1 If filters are not empty, set the select/checkbox/radio elements 21 | if (Object.keys(filters).length > 0) { 22 | this.setFilterElements(filters); 23 | } 24 | } 25 | 26 | /* -------------------------------------------------------------------------- */ 27 | /* Private Method: toggle highlight for select menu */ 28 | /* -------------------------------------------------------------------------- */ 29 | 30 | private toggleHighlight(el: HTMLInputElement | HTMLSelectElement): void { 31 | if (el.value) { 32 | el.classList.add("selected"); 33 | } else { 34 | el.classList.remove("selected"); 35 | } 36 | } 37 | 38 | /* -------------------------------------------------------------------------- */ 39 | /* Private: add event listeners */ 40 | /* -------------------------------------------------------------------------- */ 41 | 42 | private addEventListeners(): void { 43 | const hasAttr = (el: HTMLElement, attr: string) => { 44 | return el.hasAttribute(attr) || !!el.closest(`[${attr}]`); 45 | }; 46 | /* ------------ Event Listeners for select/checkbox/radio ------------ */ 47 | this.addEventListener("input", (e) => { 48 | const el = e.target; 49 | if (el instanceof HTMLSelectElement || el instanceof HTMLInputElement) { 50 | const exclusive = hasAttr(el, "exclusive"); 51 | const regex = hasAttr(el, "regex"); 52 | const exact = hasAttr(el, "exact"); 53 | const cols = el.dataset.cols ? el.dataset.cols.toLowerCase().split(",") : undefined; 54 | const columnName = el.name.toLowerCase(); 55 | if (el instanceof HTMLSelectElement) { 56 | this.toggleHighlight(el); 57 | const selectedOptions = Array.from(el.selectedOptions).map((option) => option.value); 58 | this.dispatch({ [columnName]: { values: selectedOptions, exclusive, regex, exact, cols } }); 59 | } 60 | if (el instanceof HTMLInputElement) { 61 | if (el.type === "checkbox") { 62 | // Casting to HTMLInputElement because we know it's a checkbox from selector 63 | const checkboxes = this.querySelectorAll("input[type=checkbox][name=" + el.name + "]") as NodeListOf; 64 | const checkboxValues = Array.from(checkboxes) 65 | .filter((e) => { 66 | return e.checked; 67 | }) 68 | .map((checkbox) => checkbox.value); 69 | this.dispatch({ [columnName]: { values: checkboxValues, exclusive, regex, exact, cols } }); 70 | } 71 | if (el.type === "radio") { 72 | this.dispatch({ [columnName]: { values: [el.value], exclusive, regex, exact, cols } }); 73 | } 74 | if (el.type === "range") { 75 | const sliders = this.querySelectorAll("input[type=range][name='" + el.name + "']") as NodeListOf; 76 | let minMax: string[] = []; 77 | const defaultMinMax: string[] = []; 78 | sliders.forEach((slider) => { 79 | if (slider.dataset.range === "min") { 80 | defaultMinMax[0] = slider.min; 81 | minMax[0] = slider.value; 82 | } 83 | if (slider.dataset.range === "max") { 84 | defaultMinMax[1] = slider.max; 85 | minMax[1] = slider.value; 86 | } 87 | }); 88 | if (minMax.every((item, i) => item === defaultMinMax[i])) { 89 | minMax = []; 90 | } 91 | this.dispatch({ [columnName]: { values: minMax, range: true } }); 92 | } 93 | } 94 | } 95 | }); 96 | 97 | const searchInputs = this.querySelectorAll("input[type='search']") as NodeListOf; 98 | 99 | searchInputs.forEach((el) => { 100 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 | function debounce any>(func: T, timeout = 300) { 102 | let timer: ReturnType; 103 | return (...args: Parameters) => { 104 | clearTimeout(timer); 105 | timer = setTimeout(() => { 106 | func(...args); 107 | }, timeout); 108 | }; 109 | } 110 | 111 | const event = el.dataset.event || "input"; 112 | el.addEventListener(event, () => { 113 | this.toggleHighlight(el); 114 | const exclusive = hasAttr(el, "exclusive"); 115 | const regex = hasAttr(el, "regex"); 116 | const exact = hasAttr(el, "exact"); 117 | const cols = el.dataset.cols ? el.dataset.cols.toLowerCase().split(",") : undefined; 118 | const debouncedFilter = debounce(() => this.dispatch({ [el.name]: { values: [el.value], exclusive, regex, exact, cols } })); 119 | debouncedFilter(); 120 | }); 121 | }); 122 | 123 | /* ------------------------------- Text Input ------------------------------- */ 124 | 125 | /* ------------------------------ Reset Button ------------------------------ */ 126 | this.resetButton?.addEventListener("click", () => { 127 | this.resetAllFilterElements(); 128 | this.dispatch(); 129 | }); 130 | 131 | /* ----------------- Reset Event Filters from action-table ----------------- */ 132 | // This is fired when the reset button is clicked in the tfoot section 133 | this.actionTable.addEventListener("action-table-filters-reset", () => { 134 | this.resetAllFilterElements(); 135 | }); 136 | } 137 | 138 | private dispatchInput(el: HTMLInputElement) { 139 | el.dispatchEvent( 140 | new Event("input", { 141 | bubbles: true, 142 | }) 143 | ); 144 | } 145 | 146 | private dispatch(detail?: FiltersObject) { 147 | // return no detail to reset filters on table 148 | console.log("dispatch", detail); 149 | 150 | // 1. If reset button exists then check if it should be enabled 151 | if (this.resetButton) { 152 | if (detail) { 153 | // 2. Create temp filters object to check how it will change when details is added 154 | let filters = this.actionTable.filters || {}; 155 | filters = { ...filters, ...detail }; 156 | 157 | // 3. If has filter then enable reset button; else disable 158 | this.enableReset(this.hasFilters(filters)); 159 | } else { 160 | // if no detail than this is a reset so disable reset button 161 | this.enableReset(false); 162 | } 163 | } 164 | 165 | this.dispatchEvent( 166 | new CustomEvent("action-table-filter", { 167 | detail, 168 | bubbles: true, 169 | }) 170 | ); 171 | } 172 | 173 | private enableReset(enable = true) { 174 | if (this.resetButton) { 175 | if (enable) { 176 | this.resetButton.removeAttribute("disabled"); 177 | } else { 178 | this.resetButton.setAttribute("disabled", ""); 179 | } 180 | } 181 | } 182 | 183 | private hasFilters(filters: FiltersObject) { 184 | return Object.keys(filters).some((key) => filters[key].values.some((value) => value !== "")); 185 | } 186 | 187 | /* -------------------------------------------------------------------------- */ 188 | /* Public Method: reset all filter elements */ 189 | /* -------------------------------------------------------------------------- */ 190 | 191 | public resetAllFilterElements() { 192 | console.log("resetAllFilterElements"); 193 | 194 | // Casting to types as we know what it is from selector 195 | const filterElements = this.querySelectorAll("select, input") as NodeListOf; 196 | 197 | filterElements.forEach((el) => { 198 | if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) { 199 | if (el.value === "") { 200 | el.checked = true; 201 | } else { 202 | el.checked = false; 203 | } 204 | } 205 | if (el instanceof HTMLSelectElement || (el instanceof HTMLInputElement && el.type === "search")) { 206 | el.value = ""; 207 | this.toggleHighlight(el); 208 | } 209 | if (el instanceof HTMLInputElement && el.type === "range") { 210 | el.value = el.dataset.range === "max" ? el.max : el.min; 211 | // dispatch input event to trigger change for range slider 212 | this.dispatchInput(el); 213 | } 214 | }); 215 | } 216 | 217 | /* -------------------------------------------------------------------------- */ 218 | /* Public Method: set filter elements */ 219 | /* -------------------------------------------------------------------------- */ 220 | /* ------------------ If no args are passed then it resets ------------------ */ 221 | 222 | public setFilterElements(filters: FiltersObject) { 223 | // 1. if there are filters then set the filters on all the elements 224 | if (this.hasFilters(filters)) { 225 | // enable reset button 226 | this.enableReset(); 227 | // set filter elements 228 | Object.keys(filters).forEach((key) => this.setFilterElement(key, filters[key].values)); 229 | } else { 230 | // else reset all filters 231 | this.resetAllFilterElements(); 232 | } 233 | } 234 | 235 | /* --------------------------- Set Filter element --------------------------- */ 236 | 237 | /** 238 | * Sets the value of a select element, ignoring case, to match the provided value. 239 | * 240 | * @param {HTMLSelectElement} selectElement - The select element to set the value for. 241 | * @param {string} value - The value to set, case insensitive. 242 | */ 243 | private setSelectValueIgnoringCase(selectElement: HTMLSelectElement, value: string) { 244 | value = value.toLowerCase(); 245 | Array.from(selectElement.options).some((option) => { 246 | const optionValue = option.value.toLowerCase() || option.text.toLowerCase(); 247 | 248 | if (optionValue === value) { 249 | option.selected = true; 250 | this.toggleHighlight(selectElement); 251 | return true; 252 | } else return false; 253 | }); 254 | } 255 | 256 | public setFilterElement(columnName: string, values: string[]) { 257 | if (values.length === 0) return; 258 | 259 | // Find matching fields based on name 260 | // Casting to types as we know what it is from selector 261 | 262 | const filterElements = this.querySelectorAll(`select[name="${columnName}" i], input[name="${columnName}" i]`) as NodeListOf; 263 | 264 | filterElements.forEach((el) => { 265 | if (el instanceof HTMLSelectElement) { 266 | el.value = values[0]; 267 | this.setSelectValueIgnoringCase(el, values[0]); 268 | } 269 | if (el instanceof HTMLInputElement) { 270 | if (el.type === "checkbox") { 271 | if (values.includes(el.value)) { 272 | el.checked = true; 273 | } 274 | } 275 | if (el.type === "radio") { 276 | if (el.value === values[0]) { 277 | el.checked = true; 278 | } 279 | } 280 | if (el.type === "search") { 281 | el.value = values[0]; 282 | this.toggleHighlight(el); 283 | } 284 | if (el.type === "range") { 285 | if (el.dataset.range === "min") { 286 | el.value = values[0] || el.min; 287 | // trigger input event so range slider updates 288 | this.dispatchInput(el); 289 | } 290 | if (el.dataset.range === "max") { 291 | el.value = values[1] || el.max; 292 | // trigger input event so range slider updates 293 | this.dispatchInput(el); 294 | } 295 | } 296 | } 297 | }); 298 | } 299 | } 300 | 301 | customElements.define("action-table-filters", ActionTableFilters); 302 | 303 | // Import filter components 304 | import "./action-table-filter-menu"; 305 | import "./action-table-filter-switch"; 306 | import "./action-table-filter-range"; 307 | -------------------------------------------------------------------------------- /src/action-table-no-results.ts: -------------------------------------------------------------------------------- 1 | import { FiltersObject } from "./types"; 2 | import type { ActionTable } from "./action-table"; 3 | 4 | /* -------------------------------------------------------------------------- */ 5 | /* Action Table No Results */ 6 | /* -------------------------------------------------------------------------- */ 7 | /* ---------- Simple HTML element wrapper to allow reset of filters --------- */ 8 | 9 | export class ActionTableNoResults extends HTMLElement { 10 | constructor() { 11 | super(); 12 | const actionTable = this.closest("action-table") as ActionTable; 13 | // Hide on loading 14 | this.style.display = "none"; 15 | this.addEventListener("click", (e) => { 16 | // if target is reset button 17 | if (e.target instanceof HTMLButtonElement && e.target.type === "reset") { 18 | // reset the filters on the table element 19 | this.dispatchEvent( 20 | new CustomEvent("action-table-filter", { 21 | bubbles: true, 22 | }) 23 | ); 24 | // reset the filters on the action-table-filters 25 | this.dispatchEvent(new CustomEvent("action-table-filters-reset", { bubbles: true })); 26 | } 27 | }); 28 | 29 | actionTable.addEventListener("action-table", (e) => { 30 | const detail = e.detail; 31 | // console.log("action-table", detail); 32 | 33 | if (detail?.rowsVisible === 0) { 34 | this.style.display = ""; 35 | } else { 36 | this.style.display = "none"; 37 | } 38 | }); 39 | } 40 | 41 | /* -------------------------------------------------------------------------- */ 42 | /* Private Methods */ 43 | /* -------------------------------------------------------------------------- */ 44 | 45 | /* ----------------- Send Event Triggered by checkbox change ---------------- */ 46 | } 47 | 48 | customElements.define("action-table-no-results", ActionTableNoResults); 49 | -------------------------------------------------------------------------------- /src/action-table-pagination-options.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTable } from "./action-table"; 2 | 3 | export class ActionTablePaginationOptions extends HTMLElement { 4 | constructor() { 5 | super(); 6 | const actionTable = this.closest("action-table") as ActionTable; 7 | const { pagination } = actionTable; 8 | const paginationOptions = (options: number[]) => options.map((opt) => ``).join(""); 9 | 10 | const paginationSelect = 11 | this.options.length > 0 12 | ? `` 13 | : ""; 14 | 15 | this.innerHTML = paginationSelect; 16 | this.addEventListener("change", (e) => { 17 | if (e.target instanceof HTMLSelectElement) { 18 | const value = Number(e.target.value); 19 | if (!isNaN(value)) { 20 | actionTable.pagination = value; 21 | } 22 | } 23 | }); 24 | } 25 | 26 | get options(): number[] { 27 | const options = this.getAttribute("options"); 28 | if (options) { 29 | const paginationArray = options 30 | .split(",") 31 | .map((item) => Number(item)) 32 | .filter((p) => !isNaN(p)); 33 | if (paginationArray.length > 0) { 34 | return paginationArray; 35 | } 36 | } 37 | return []; 38 | } 39 | } 40 | 41 | customElements.define("action-table-pagination-options", ActionTablePaginationOptions); 42 | -------------------------------------------------------------------------------- /src/action-table-pagination.ts: -------------------------------------------------------------------------------- 1 | import type { ActionTable } from "./action-table"; 2 | import "./action-table-pagination-options"; 3 | 4 | export class ActionTablePagination extends HTMLElement { 5 | constructor() { 6 | super(); 7 | this.addEventListeners(); 8 | } 9 | 10 | private page = 1; 11 | private numberOfPages = 1; 12 | private group = 1; 13 | private maxGroups = 1; 14 | private actionTable = this.closest("action-table") as ActionTable; 15 | private rowsVisible = 0; 16 | 17 | public connectedCallback(): void { 18 | this.render(); 19 | } 20 | 21 | public render() { 22 | console.log("render pagination"); 23 | 24 | const { page, numberOfPages } = this.actionTable; 25 | // reassign number of pages based on this.actionTable 26 | this.numberOfPages = numberOfPages; 27 | this.page = page; 28 | // temporarily local variables 29 | const maxButtons = Number(this.getAttribute("max-buttons")) || 10; 30 | const maxGroups = Math.ceil(numberOfPages / maxButtons); // reassign to this at end of render 31 | let group = this.group; // reassign to this at end of render 32 | 33 | if (group > maxGroups) { 34 | group = maxGroups; 35 | } else if (group < 1) { 36 | group = 1; 37 | } 38 | 39 | const startIndex = (group - 1) * maxButtons + 1; 40 | 41 | /* -------------------------------------------------------------------------- */ 42 | /* Render the buttons */ 43 | /* -------------------------------------------------------------------------- */ 44 | 45 | /* ----------------------------- Button strings ----------------------------- */ 46 | function pageButton(i: number, className: string = "", text?: string): string { 47 | return ``; 48 | } 49 | 50 | /* -------------------------- Start making buttons -------------------------- */ 51 | 52 | let paginatedButtons = ""; 53 | 54 | if (group > 1) { 55 | paginatedButtons += `${pageButton(1, "first")}${pageButton(startIndex - 1, "prev", "...")}`; 56 | } 57 | 58 | if (numberOfPages > 0) { 59 | // for looping through the number of pages 60 | for (let i = startIndex; i <= numberOfPages; i++) { 61 | // code to handle each page 62 | paginatedButtons += pageButton(i); 63 | if (i !== numberOfPages && i >= maxButtons * group) { 64 | paginatedButtons += `${pageButton(i + 1, "next", "...")}${pageButton(numberOfPages, "last")}`; 65 | break; 66 | } 67 | } 68 | } 69 | 70 | const classAttr = (suffix: string) => ` class="pagination-${suffix}"`; 71 | 72 | this.innerHTML = ` ${paginatedButtons}`; 73 | this.changeLabel(page); 74 | 75 | // assign temporary variables back to this 76 | this.group = group; 77 | this.maxGroups = maxGroups; 78 | } 79 | 80 | private changeLabel(page: number) { 81 | const { pagination, rowsSet } = this.actionTable; 82 | // update rowsVisible from current set size 83 | this.rowsVisible = rowsSet.size; 84 | 85 | const label = this.getAttribute("label") || "Showing {rows} of {total}:"; 86 | 87 | const labelStr = label.replace("{rows}", `${page * pagination - pagination + 1}-${page * pagination}`).replace("{total}", `${rowsSet.size}`); 88 | 89 | const labelSpan = this.querySelector("span.pagination-label"); 90 | if (labelSpan) labelSpan.textContent = labelStr; 91 | } 92 | 93 | private addEventListeners(): void { 94 | this.addEventListener("click", (event) => { 95 | const target = event.target; 96 | if (target instanceof HTMLButtonElement) { 97 | // temp variable 98 | // must trigger action-table page change if it changes 99 | let page: number = 1; 100 | 101 | if (target.dataset.page) { 102 | // set the current page before setting the current page on the action table so that it doesn't rerender when setProps is returned 103 | page = Number(target.dataset.page); 104 | 105 | target.classList.add("active"); 106 | this.querySelectorAll("button").forEach((button) => { 107 | if (button !== target) { 108 | button.classList.remove("active"); 109 | } 110 | }); 111 | } 112 | // temp variables 113 | // Must rerender if the group changes 114 | let group = this.group; 115 | 116 | const hasClass = (className: string) => { 117 | return target.classList.contains(className); 118 | }; 119 | if (hasClass("next")) { 120 | group++; 121 | } 122 | if (hasClass("prev")) { 123 | group--; 124 | } 125 | if (hasClass("first")) { 126 | group = 1; 127 | } 128 | if (hasClass("last")) { 129 | group = this.maxGroups; 130 | } 131 | 132 | this.actionTable.page = this.page = page; 133 | this.changeLabel(page); 134 | 135 | if (this.group !== group) { 136 | this.group = group; 137 | this.render(); 138 | } 139 | // } 140 | } 141 | }); 142 | 143 | this.actionTable.addEventListener("action-table", (e) => { 144 | const { page, pagination, numberOfPages, rowsVisible } = e.detail; 145 | console.log("action-table pagination", e.detail); 146 | if ( 147 | (page && page !== this.page) || 148 | (numberOfPages !== undefined && numberOfPages !== this.numberOfPages) || 149 | pagination !== undefined || 150 | rowsVisible !== this.rowsVisible 151 | ) { 152 | console.log("action-table pagination render", page, this.page, pagination, numberOfPages, this.numberOfPages); 153 | this.render(); 154 | } 155 | }); 156 | } 157 | } 158 | 159 | customElements.define("action-table-pagination", ActionTablePagination); 160 | -------------------------------------------------------------------------------- /src/action-table-switch.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------- */ 2 | /* Action Table Switch */ 3 | /* -------------------------------------------------------------------------- */ 4 | /* ----- Optional element added as an example to be extended by the user ---- */ 5 | 6 | export class ActionTableSwitch extends HTMLElement { 7 | constructor() { 8 | super(); 9 | this.render(); 10 | this.addEventListeners(); 11 | } 12 | 13 | /* -------------------------------------------------------------------------- */ 14 | /* Attributes */ 15 | /* -------------------------------------------------------------------------- */ 16 | 17 | get checked(): boolean { 18 | return this.hasAttribute("checked"); 19 | } 20 | set checked(value: boolean) { 21 | if (value) { 22 | this.setAttribute("checked", ""); 23 | } else { 24 | this.removeAttribute("checked"); 25 | } 26 | } 27 | get label(): string { 28 | return this.getAttribute("label") || "switch"; 29 | } 30 | 31 | get name(): string { 32 | return this.getAttribute("name") || ""; 33 | } 34 | 35 | get value(): string { 36 | return this.getAttribute("value") || "on"; 37 | } 38 | 39 | /* -------------------------------------------------------------------------- */ 40 | /* Event Listeners */ 41 | /* -------------------------------------------------------------------------- */ 42 | 43 | private addEventListeners() { 44 | const input = this.querySelector("input"); 45 | if (input) { 46 | input.addEventListener("change", () => { 47 | this.checked = input.checked; 48 | this.sendEvent(); 49 | }); 50 | } 51 | } 52 | 53 | /* -------------------------------------------------------------------------- */ 54 | /* Private Methods */ 55 | /* -------------------------------------------------------------------------- */ 56 | 57 | /* ----------------- Send Event Triggered by checkbox change ---------------- */ 58 | 59 | private async sendEvent() { 60 | const detail = { checked: this.checked, id: this.id || this.dataset.id, name: this.name, value: this.value }; 61 | this.dispatchEvent(new CustomEvent("action-table-switch", { detail, bubbles: true })); 62 | } 63 | 64 | private render(): void { 65 | const checkbox = document.createElement("input"); 66 | checkbox.type = "checkbox"; 67 | checkbox.name = this.name; 68 | checkbox.value = this.value; 69 | checkbox.checked = this.checked; 70 | checkbox.setAttribute("aria-label", this.label); 71 | this.replaceChildren(checkbox); 72 | } 73 | } 74 | 75 | customElements.define("action-table-switch", ActionTableSwitch); 76 | -------------------------------------------------------------------------------- /src/action-table.ts: -------------------------------------------------------------------------------- 1 | import { ColsArray, FiltersObject, SingleFilterObject, ActionTableCellData, ActionTableEventDetail, Direction, ActionTableStore } from "./types"; 2 | import "./action-table-no-results"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | function hasKeys(obj: any): boolean { 6 | return Object.keys(obj).length > 0; 7 | } 8 | 9 | export class ActionTable extends HTMLElement { 10 | constructor() { 11 | super(); 12 | 13 | /* -------------------------------------------------------------------------- */ 14 | /* Init */ 15 | /* -------------------------------------------------------------------------- */ 16 | /* ------------------- Only fires once on js initial load ------------------- */ 17 | /* --------------- Does not require the inner DOM to be ready --------------- */ 18 | 19 | // 1. Get sort and direction and filters from local storage 20 | if (this.store) { 21 | // 1. Get sort and direction and filters from local storage 22 | const lsActionTable = this.getStore(); 23 | if (lsActionTable) { 24 | this.sort = lsActionTable.sort || this.sort; 25 | this.direction = lsActionTable.direction || this.direction || "ascending"; 26 | this.filters = lsActionTable.filters || this.filters; 27 | } 28 | } 29 | 30 | // 2. Get sort and direction and filters from URL (overrides local storage) 31 | if (this.hasAttribute("urlparams")) { 32 | const params = new URLSearchParams(window.location.search); 33 | 34 | // sort through remaining params for filters to create a filters object 35 | const filters: FiltersObject = {}; 36 | for (let [key, value] of params.entries()) { 37 | key = key.toLowerCase(); 38 | value = value.toLowerCase(); 39 | // Only add key if it's not sort or direction 40 | if (key !== "sort" && key !== "direction") { 41 | // check for value and if so add it to the array. 42 | if (filters[key]?.values) { 43 | filters[key].values.push(value); 44 | } else { 45 | // if not, create it 46 | filters[key] = { values: [value] }; 47 | } 48 | } 49 | if (key === "sort") { 50 | this.sort = value; 51 | } 52 | if (key === "direction" && (value === "ascending" || value === "descending")) { 53 | this.direction = value; 54 | } 55 | } 56 | 57 | // if filters object is not empty, set this.filters 58 | if (Object.keys(filters).length > 0) { 59 | this.setFiltersObject(filters); 60 | } 61 | } 62 | 63 | this.addEventListeners(); 64 | } 65 | 66 | public table!: HTMLTableElement; 67 | public tbody!: HTMLTableSectionElement; 68 | public cols: ColsArray = []; 69 | public rows: Array = []; 70 | public filters: FiltersObject = {}; 71 | 72 | /* -------------------------------------------------------------------------- */ 73 | /* Attributes */ 74 | /* -------------------------------------------------------------------------- */ 75 | 76 | // sort attribute to set the sort column 77 | 78 | get sort(): string { 79 | return this.getCleanAttr("sort"); 80 | } 81 | set sort(value: string) { 82 | this.setAttribute("sort", value); 83 | } 84 | 85 | // direction attribute to set the sort direction 86 | get direction(): Direction { 87 | const direction = this.getCleanAttr("direction"); 88 | return direction === "descending" ? direction : "ascending"; 89 | } 90 | set direction(value: Direction) { 91 | this.setAttribute("direction", value); 92 | } 93 | 94 | // store attribute to trigger loading and saving to sort and filters localStorage 95 | get store(): string { 96 | return this.hasAttribute("store") ? this.getCleanAttr("store") || "action-table" : ""; 97 | } 98 | 99 | get pagination(): number { 100 | return Number(this.getCleanAttr("pagination")) || 0; 101 | } 102 | 103 | set pagination(value: number) { 104 | this.setAttribute("pagination", value.toString()); 105 | } 106 | 107 | get page(): number { 108 | return Number(this.getCleanAttr("page")) || 1; 109 | } 110 | 111 | set page(value: number) { 112 | value = this.checkPage(value); 113 | this.setAttribute("page", value.toString()); 114 | } 115 | 116 | private checkPage(page: number): number { 117 | return Math.max(1, Math.min(page, this.numberOfPages)); 118 | } 119 | 120 | private dispatch(detail: ActionTableEventDetail) { 121 | // console.log("dispatch", detail); 122 | this.dispatchEvent( 123 | new CustomEvent("action-table", { 124 | detail, 125 | }) 126 | ); 127 | } 128 | 129 | public tableContent = new WeakMap(); 130 | // set of rows that are shown in table based on filters 131 | public rowsSet = new Set(); 132 | 133 | private getCleanAttr(attr: string): string { 134 | return this.getAttribute(attr)?.trim().toLowerCase() || ""; 135 | } 136 | 137 | /* -------------------------------------------------------------------------- */ 138 | /* Connected Callback */ 139 | /* -------------------------------------------------------------------------- */ 140 | /* ------------- Fires every time the event is added to the DOM ------------- */ 141 | 142 | public connectedCallback(): void { 143 | /* -------------- Init code which requires DOM to be ready -------------- */ 144 | console.time("Connected Callback"); 145 | 146 | // 1. Get table, tbody, rows, and column names in this.cols 147 | const table = this.querySelector("table"); 148 | // make sure table with thead and tbody exists 149 | if (table && table.querySelector("thead th") && table.querySelector("tbody td")) { 150 | this.table = table; 151 | // casting type as we know it exists due to querySelector above 152 | this.tbody = table.querySelector("tbody") as HTMLTableSectionElement; 153 | this.rows = Array.from(this.tbody.querySelectorAll("tr")) as Array; 154 | // add each row to rowsSet 155 | this.rowsSet = new Set(this.rows); 156 | } else { 157 | throw new Error("Could not find table with thead and tbody"); 158 | } 159 | // 2. Hide tbody if there is sort or filters; then sort and filter 160 | 161 | if (this.sort || hasKeys(this.filters)) { 162 | this.tbody.style.display = "none"; 163 | this.sortAndFilter(); 164 | } 165 | 166 | this.getColumns(); 167 | 168 | this.addObserver(); 169 | 170 | console.timeEnd("Connected Callback"); 171 | console.log("store:", this.store); 172 | } 173 | 174 | /* -------------------------------------------------------------------------- */ 175 | /* Attribute Changed Callback */ 176 | /* -------------------------------------------------------------------------- */ 177 | 178 | static get observedAttributes(): string[] { 179 | return ["sort", "direction", "pagination", "page"]; 180 | } 181 | public attributeChangedCallback(name: string, oldValue: string, newValue: string) { 182 | // only fires if the value actually changes and if the rows is not empty, which means it has grabbed the cellContent 183 | if (oldValue !== newValue && this.rows.length > 0) { 184 | if (name === "sort" || name === "direction") { 185 | console.log("attributeChangedCallback: sort", oldValue, newValue); 186 | this.sortTable(); 187 | } 188 | if (name === "pagination") { 189 | // console.log("attributeChangedCallback: pagination", oldValue, newValue); 190 | this.dispatch({ pagination: this.pagination }); 191 | } 192 | this.appendRows(); 193 | } 194 | } 195 | 196 | /* -------------------------------------------------------------------------- */ 197 | /* PRIVATE METHODS */ 198 | /* -------------------------------------------------------------------------- */ 199 | 200 | /* -------------------------------------------------------------------------- */ 201 | /* Filter variable methods */ 202 | /* -------------------------------------------------------------------------- */ 203 | 204 | private setFiltersObject(filters: FiltersObject = {}): void { 205 | // If set empty it resets filters to default 206 | 207 | this.filters = filters; 208 | 209 | if (this.store) this.setStore({ filters: this.filters }); 210 | } 211 | 212 | /* ----------- Used by reset button and action-table-filter event ----------- */ 213 | private setFilters(filters: FiltersObject = {}) { 214 | this.setFiltersObject(filters); 215 | this.filterTable(); 216 | this.appendRows(); 217 | } 218 | 219 | /* -------------------------------------------------------------------------- */ 220 | /* Private Method: add event listeners */ 221 | /* -------------------------------------------------------------------------- */ 222 | 223 | private addEventListeners(): void { 224 | // Sort buttons 225 | this.addEventListener( 226 | "click", 227 | (event) => { 228 | const el = event.target; 229 | // only fire if event target is a button with data-col 230 | if (el instanceof HTMLButtonElement && el.dataset.col) { 231 | const name = el.dataset.col; 232 | let direction: Direction = "ascending"; 233 | if (this.sort === name && this.direction === "ascending") { 234 | direction = "descending"; 235 | } 236 | 237 | this.sort = name; 238 | this.direction = direction; 239 | if (this.store) this.setStore({ sort: this.sort, direction: direction }); 240 | } 241 | }, 242 | false 243 | ); 244 | 245 | const findCell = (el: HTMLElement) => { 246 | return (el.matches("td") ? el : el.closest("td")) as HTMLTableCellElement; 247 | }; 248 | 249 | // Listens for checkboxes in the table since mutation observer does not support checkbox changes 250 | this.addEventListener("change", (event) => { 251 | const el = event.target; 252 | // only fire if event target is a checkbox in a td; this stops it firing for filters 253 | if (el instanceof HTMLInputElement && el.closest("td") && el.type === "checkbox") { 254 | // get new content, sort and filter. This works for checkboxes and action-table-switch 255 | console.log("event change", el); 256 | this.updateCellValues(findCell(el)); 257 | } 258 | }); 259 | 260 | // Listens for action-table-filter event from action-table-filters 261 | this.addEventListener(`action-table-filter`, (event) => { 262 | if (event.detail) { 263 | // 1. If detail is defined then add it to the filters object 264 | const filters = { ...this.filters, ...event.detail }; 265 | // 2. Remove empty filters 266 | Object.keys(filters).forEach((key) => { 267 | if (filters[key].values.every((value) => value === "")) { 268 | delete filters[key]; 269 | } 270 | }); 271 | // 3. Set filters with new filters object 272 | this.setFilters(filters); 273 | } else { 274 | // 3. if no detail than reset filters by calling setFilters with empty object 275 | this.setFilters(); 276 | } 277 | }); 278 | 279 | // Listens for action-table-update event used by custom elements that want to announce content changes 280 | this.addEventListener(`action-table-update`, (event) => { 281 | const target = event.target; 282 | if (target instanceof HTMLElement) { 283 | let values: Partial = {}; 284 | if (typeof event.detail === "string") { 285 | values = { sort: event.detail, filter: event.detail }; 286 | } else values = event.detail; 287 | this.updateCellValues(findCell(target), values); 288 | } 289 | }); 290 | } 291 | 292 | /* -------------------------------------------------------------------------- */ 293 | /* Private Method: Get localStorage */ 294 | /* -------------------------------------------------------------------------- */ 295 | 296 | private getStore() { 297 | try { 298 | const ls = localStorage.getItem(this.store); 299 | const data = ls && JSON.parse(ls); 300 | if (typeof data === "object" && data !== null) { 301 | const hasKeys = ["sort", "direction", "filters"].some((key) => key in data); 302 | if (hasKeys) return data as ActionTableStore; 303 | } 304 | return false; 305 | } catch (e) { 306 | return false; 307 | } 308 | } 309 | 310 | /* -------------------------------------------------------------------------- */ 311 | /* Private Method: Set localStorage */ 312 | /* -------------------------------------------------------------------------- */ 313 | 314 | private setStore(data: ActionTableStore) { 315 | const lsData = this.getStore() || {}; 316 | if (lsData) { 317 | data = { ...lsData, ...data }; 318 | } 319 | localStorage.setItem(this.store, JSON.stringify(data)); 320 | } 321 | 322 | /* -------------------------------------------------------------------------- */ 323 | /* Private Method delaying sortAndFilter until it's no longer called */ 324 | /* -------------------------------------------------------------------------- */ 325 | 326 | private delayUntilNoLongerCalled(callback: () => void) { 327 | let timeoutId: number; 328 | let isCalling = false; 329 | 330 | function delayedCallback() { 331 | // Execute the callback 332 | callback(); 333 | 334 | // Reset the flag variable to false 335 | isCalling = false; 336 | } 337 | 338 | return function () { 339 | // If the function is already being called, clear the previous timeout 340 | if (isCalling) { 341 | clearTimeout(timeoutId); 342 | } else { 343 | // Set the flag variable to true if the function is not already being called 344 | isCalling = true; 345 | } 346 | 347 | // Set a new timeout to execute the delayed callback after 10ms 348 | timeoutId = setTimeout(delayedCallback, 10); 349 | }; 350 | } 351 | 352 | /* ------------------------- Delayed Sort and Filter ------------------------ */ 353 | 354 | private sortAndFilter = this.delayUntilNoLongerCalled(() => { 355 | console.log("🎲 sortAndFilter"); 356 | this.filterTable(); 357 | this.sortTable(); 358 | this.appendRows(); 359 | // If tbody is hidden then this is the initial render 360 | if (this.tbody.matches("[style*=none]")) { 361 | // if there are no rows then automatically reset filters 362 | if (this.rowsSet.size === 0) { 363 | console.error("no results found on initial render"); 364 | this.setFilters(); 365 | this.dispatchEvent(new Event(`action-table-filters-reset`)); 366 | } 367 | // show tbody 368 | this.tbody.style.display = ""; 369 | } 370 | }); 371 | 372 | /* -------------------------------------------------------------------------- */ 373 | /* Private Method: get columns from table */ 374 | /* -------------------------------------------------------------------------- */ 375 | 376 | private getColumns(): void { 377 | // console.time("getColumns"); 378 | // 1. Get column headers 379 | // casting type as we know what it is from selector 380 | const ths = this.table.querySelectorAll("thead th") as NodeListOf; 381 | 382 | ths.forEach((th) => { 383 | // 2. Column name is based on data-col attribute or results of getCellContent() function 384 | const name = (th.dataset.col || this.getCellContent(th)).trim().toLowerCase(); 385 | const order = th.dataset.order ? th.dataset.order.split(",") : undefined; 386 | 387 | // 4. Add column name to cols array 388 | this.cols.push({ name, order }); 389 | 390 | // 5. if the column is sortable then wrap it in a button, and add aria 391 | if (!th.hasAttribute("no-sort")) { 392 | const button = document.createElement("button"); 393 | button.dataset.col = name; 394 | button.type = "button"; 395 | button.innerHTML = th.innerHTML; 396 | th.replaceChildren(button); 397 | } 398 | }); 399 | 400 | // 7. add colGroup unless it already exists 401 | if (!this.table.querySelector("colgroup")) { 402 | // 7.1 create colgroup 403 | const colGroup = document.createElement("colgroup"); 404 | // 7.2 add col for each column 405 | ths.forEach(() => { 406 | const col = document.createElement("col"); 407 | colGroup.appendChild(col); 408 | }); 409 | // 7.3 prepend colgroup 410 | this.table.prepend(colGroup); 411 | } 412 | // console.log("action-table cols", this.cols); 413 | // 8. Return cols array 414 | // console.timeEnd("getColumns"); 415 | } 416 | 417 | /* -------------------------------------------------------------------------- */ 418 | /* Private Method: get cell content */ 419 | /* -------------------------------------------------------------------------- */ 420 | private getCellContent(cell: HTMLTableCellElement): string { 421 | // 1. get cell content with innerText; set to empty string if null 422 | let cellContent: string = (cell.textContent || "").trim(); 423 | 424 | // 3. if there is no cell content then check... 425 | if (!cellContent) { 426 | // 3.1 if there is an svg then get title; otherwise return empty string 427 | const svg = cell.querySelector("svg"); 428 | if (svg instanceof SVGElement) { 429 | cellContent = svg.querySelector("title")?.textContent || cellContent; 430 | } 431 | // 3.2 if checkbox element then get value if checked 432 | const checkbox = cell.querySelector("input[type=checkbox]"); 433 | if (checkbox instanceof HTMLInputElement && checkbox.checked) { 434 | cellContent = checkbox.value; 435 | } 436 | // 3.3 if custom element with shadowRoot then get text content from shadowRoot 437 | const customElement = cell.querySelector(":defined"); 438 | if (customElement?.shadowRoot) { 439 | cellContent = customElement.shadowRoot.textContent || cellContent; 440 | } 441 | } 442 | 443 | return cellContent.trim(); 444 | } 445 | 446 | /* ------------------------------------------------------------------------- */ 447 | /* Private Method: Set Cell Content in td attribute */ 448 | /* -------------------------------------------------------------------------- */ 449 | 450 | public getCellValues(cell: HTMLTableCellElement): ActionTableCellData { 451 | // 1. If data exists return it; else get it 452 | if (this.tableContent.has(cell)) { 453 | // console.log("getCellValues: Cached"); 454 | // @ts-expect-error has checks for data 455 | return this.tableContent.get(cell); 456 | } else { 457 | const cellValues = this.setCellValues(cell); 458 | // console.log("getCellValues: Set", cellValues); 459 | return cellValues; 460 | } 461 | } 462 | 463 | private setCellValues(cell: HTMLTableCellElement, values: Partial = {}) { 464 | const cellContent = this.getCellContent(cell); 465 | const cellValues = { sort: cell.dataset.sort || cellContent, filter: cell.dataset.filter || cellContent, ...values }; 466 | this.tableContent.set(cell, cellValues); 467 | return cellValues; 468 | } 469 | 470 | private updateCellValues(cell: HTMLTableCellElement, values: Partial = {}) { 471 | this.setCellValues(cell, values); 472 | this.sortAndFilter(); 473 | } 474 | 475 | /* -------------------------------------------------------------------------- */ 476 | /* Private Method: Add Observer */ 477 | /* -------------------------------------------------------------------------- */ 478 | 479 | private addObserver() { 480 | // Good reference for MutationObserver: https://davidwalsh.name/mutationobserver-api 481 | // 1. Create an observer instance 482 | const observer = new MutationObserver((mutations) => { 483 | // Make sure it only gets content once if there are several changes at the same time 484 | // 1.1 sort through all mutations 485 | mutations.forEach((mutation) => { 486 | let target = mutation.target; 487 | // If target is a text node, get its parentNode 488 | if (target.nodeType === 3 && target.parentNode) target = target.parentNode; 489 | // ignore if this is not an HTMLElement 490 | if (!(target instanceof HTMLElement)) return; 491 | // Get parent td 492 | const td = target.closest("td"); 493 | // Only act on HTMLTableCellElements 494 | if (td instanceof HTMLTableCellElement) { 495 | // If this is a contenteditable element that is focused then only update on blur 496 | if (td.hasAttribute("contenteditable") && td === document.activeElement) { 497 | // add function for event listener 498 | // Make sure that the event listener is only added once 499 | if (!td.dataset.edit) { 500 | td.dataset.edit = "true"; 501 | td.addEventListener("blur", () => { 502 | this.updateCellValues(td); 503 | }); 504 | } 505 | } else { 506 | // else update 507 | this.updateCellValues(td); 508 | } 509 | } 510 | 511 | // Ignore tbody changes which happens whenever a new row is added with sort 512 | }); 513 | }); 514 | observer.observe(this.tbody, { childList: true, subtree: true, attributes: true, characterData: true, attributeFilter: ["data-sort", "data-filter"] }); 515 | } 516 | 517 | /* -------------------------------------------------------------------------- */ 518 | /* Private Method: filter table on column name and value */ 519 | /* -------------------------------------------------------------------------- */ 520 | /* ------------- Used by filters in action-table-filter element ------------- */ 521 | /* ------------- Also triggered by local storage and URL params ------------- */ 522 | 523 | private filterTable(): void { 524 | console.log("filterTable", this.filters); 525 | 526 | // eslint-disable-next-line no-console 527 | // console.time("filterTable"); 528 | 529 | // 1. Save current state of numberOfPages 530 | const currentNumberOfPages = this.numberOfPages; 531 | const currentRowsVisible = this.rowsSet.size; 532 | 533 | // 2. get filter value for whole row based on special reserved name "action-table" 534 | const filterForWholeRow = this.filters["action-table"]; 535 | 536 | this.rows.forEach((row) => { 537 | // 3.1 set base display value as "" 538 | let hide = false; 539 | // 3.2 get td cells 540 | const cells = row.querySelectorAll("td") as NodeListOf; 541 | // 3.3 if filter value for whole row exists then run filter against innerText of entire row content 542 | if (filterForWholeRow) { 543 | // 3.3.1 build string of all td data-filter values, ignoring checkboxes 544 | // console.log("filterForWholeRow"); 545 | // TODO: add ability to only filter some columns data-ignore with name or index or data-only attribute 546 | const cellsFiltered = Array.from(cells).filter((_c, i) => { 547 | console.log("🚀 ~ ActionTable ~ this.cols[i].name:", this.filters["action-table"].cols, this.cols[i].name); 548 | return this.filters["action-table"].cols ? this.filters["action-table"].cols.includes(this.cols[i].name.toLowerCase()) : true; 549 | }); 550 | console.log("🚀 ~ ActionTable ~ this.rows.forEach ~ cellsFiltered:", cellsFiltered); 551 | 552 | const content = cellsFiltered.map((cell) => (cell.querySelector('input[type="checkbox"]') ? "" : this.getCellValues(cell).filter)).join(" "); 553 | 554 | if (this.shouldHide(filterForWholeRow, content)) { 555 | hide = true; 556 | } 557 | } 558 | // 3.4 if columnName is not action-table then run filter against td cell content 559 | cells.forEach((cell, i) => { 560 | const filter = this.filters[this.cols[i].name]; 561 | if (!filter) return; 562 | // console.log("filter cell", filter); 563 | 564 | if (this.shouldHide(filter, this.getCellValues(cell).filter)) { 565 | hide = true; 566 | } 567 | }); 568 | 569 | // 3.5 set display 570 | if (hide) { 571 | this.rowsSet.delete(row); 572 | } else { 573 | this.rowsSet.add(row); 574 | } 575 | }); 576 | 577 | // 4. If number of pages changed, update pagination 578 | console.log("currentNumberOfPages", currentNumberOfPages, this.numberOfPages); 579 | 580 | if (this.numberOfPages !== currentNumberOfPages) { 581 | this.dispatch({ numberOfPages: this.numberOfPages }); 582 | } 583 | if (this.rowsSet.size !== currentRowsVisible) { 584 | this.dispatch({ rowsVisible: this.rowsSet.size }); 585 | } 586 | // console.timeEnd("filterTable"); 587 | } 588 | 589 | private shouldHide(filter: SingleFilterObject, content: string): boolean { 590 | // console.log("shouldHide", filter, content); 591 | if (filter.values && filter.values.length > 0) { 592 | // 1. build regex from filterValues array (checkboxes and select menus send arrays) 593 | if (filter.regex) { 594 | let regexPattern = filter.values.join("|"); 595 | if (filter.exclusive) { 596 | const regexParts = filter.values.map((str) => `(?=.*${str})`); 597 | regexPattern = `${regexParts.join("")}.*`; 598 | } 599 | const regex = new RegExp(regexPattern, "i"); 600 | 601 | // 2. check if content matches 602 | return !regex.test(content); 603 | } 604 | if (filter.range) { 605 | const [min, max] = filter.values; 606 | // TODO: Maybe allow for alphabetical ranges? 607 | if (!isNaN(Number(min)) && !isNaN(Number(max))) return Number(content) < Number(min) || Number(content) > Number(max); 608 | } 609 | // console.log("show", columnName, content); 610 | if (filter.exclusive) { 611 | return !filter.values.every((v) => content.toLowerCase().includes(v.toLowerCase())); 612 | } 613 | if (filter.exact) { 614 | return filter.values.every((v) => v && v !== content); 615 | } 616 | return !filter.values.some((v) => content.toLowerCase().includes(v.toLowerCase())); 617 | } 618 | return false; 619 | } 620 | 621 | /* -------------------------------------------------------------------------- */ 622 | /* Private Method: sort table based on column name and direction */ 623 | /* -------------------------------------------------------------------------- */ 624 | /* ----------- Used by sort header buttons and attributes callback ----------- */ 625 | /* ------------- Also triggered by local storage and URL params ------------- */ 626 | 627 | private sortTable(columnName = this.sort, direction = this.direction) { 628 | if (!this.sort || !direction) return; 629 | // eslint-disable-next-line no-console 630 | // console.time("sortTable"); 631 | columnName = columnName.toLowerCase(); 632 | // 1. Get column index from column name 633 | const columnIndex = this.cols.findIndex((col) => col.name === columnName); 634 | 635 | // 2. If column exists and there are rows then sort 636 | if (columnIndex >= 0 && this.rows.length > 0) { 637 | console.log(`sort by ${columnName} ${direction}`); 638 | 639 | // 1 Get sort order for column if it exists 640 | const sortOrder = this.cols[columnIndex].order; 641 | // helper function to return sort order index for row sort 642 | const checkSortOrder = (value: string) => { 643 | return sortOrder?.includes(value) ? sortOrder.indexOf(value).toString() : value; 644 | }; 645 | 646 | // 2. Sort rows 647 | this.rows.sort((r1, r2) => { 648 | // 1. If descending sort, swap rows 649 | if (direction === "descending") { 650 | const temp = r1; 651 | r1 = r2; 652 | r2 = temp; 653 | } 654 | 655 | // 2. Get content from stored actionTable.sort; If it matches value in sort order exists then return index 656 | const a: string = checkSortOrder(this.getCellValues(r1.children[columnIndex] as HTMLTableCellElement).sort); 657 | const b: string = checkSortOrder(this.getCellValues(r2.children[columnIndex] as HTMLTableCellElement).sort); 658 | 659 | // console.log("a", a, "b", b); 660 | 661 | return this.alphaNumSort(a, b); 662 | }); 663 | 664 | // 3. Add sorted class to columns 665 | const colGroupCols = this.querySelectorAll("col"); 666 | colGroupCols.forEach((colGroupCol, i) => { 667 | if (i === columnIndex) { 668 | colGroupCol.classList.add("sorted"); 669 | } else { 670 | colGroupCol.classList.remove("sorted"); 671 | } 672 | }); 673 | 674 | // 3. set aria sorting direction 675 | const ths = this.table.querySelectorAll("thead th"); 676 | ths.forEach((th, i) => { 677 | const ariaSort = i === columnIndex ? direction : "none"; 678 | th.setAttribute("aria-sort", ariaSort); 679 | }); 680 | } 681 | // eslint-disable-next-line no-console 682 | // console.timeEnd("sortTable"); 683 | } 684 | 685 | /* --------------------------- Public Sort Method --------------------------- */ 686 | // Also used by action-table-filter-menu.js when building options menu 687 | 688 | public alphaNumSort(a: string, b: string): number { 689 | function isNumberOrDate(value: string): number | void { 690 | if (!isNaN(Number(value))) { 691 | return Number(value); 692 | } else if (!isNaN(Date.parse(value))) { 693 | return Date.parse(value); 694 | } 695 | } 696 | 697 | const aSort = isNumberOrDate(a); 698 | const bSort = isNumberOrDate(b); 699 | 700 | if (aSort && bSort) { 701 | return aSort - bSort; 702 | } 703 | return a.localeCompare(b); 704 | } 705 | 706 | /* -------------------------------------------------------------------------- */ 707 | /* Private Method: Append Rows */ 708 | /* -------------------------------------------------------------------------- */ 709 | /* --------- Sets row visibility based on sort,filter and pagination -------- */ 710 | 711 | private appendRows(): void { 712 | console.time("appendRows"); 713 | 714 | // Helper function for hiding rows based on pagination 715 | const isActivePage = (i: number): boolean => { 716 | // returns if pagination is enabled (> 0) and row is on current page. 717 | // For instance if the current page is 2 and pagination is 10 then is greater than 10 and less than or equal to 20 718 | const { pagination, page } = this; 719 | return pagination === 0 || (i >= pagination * (page - 1) + 1 && i <= pagination * page); 720 | }; 721 | 722 | // fragment for holding rows 723 | const fragment = document.createDocumentFragment(); 724 | // This includes both rows hidden by filter and by pagination 725 | let currentRowsVisible = 0; 726 | 727 | // loop through rows to set hide or show 728 | this.rows.forEach((row) => { 729 | let display = "none"; 730 | // if row not hidden by filter 731 | if (this.rowsSet.has(row)) { 732 | // increment current rows 733 | currentRowsVisible++; 734 | // if row not hidden by pagination 735 | if (isActivePage(currentRowsVisible)) { 736 | // set display to show and add row to fragment 737 | display = ""; 738 | fragment.appendChild(row); 739 | } 740 | } 741 | row.style.display = display; 742 | }); 743 | 744 | // prepend fragment to tbody 745 | 746 | this.tbody.prepend(fragment); 747 | 748 | console.timeEnd("appendRows"); 749 | 750 | if (this.pagination > 0) { 751 | // If page is greater than number of pages, set page to number of pages 752 | const page = this.checkPage(this.page); 753 | if (page !== this.page) { 754 | // update this.page 755 | this.page = page; 756 | // Dispatch current page 757 | this.dispatch({ page: page }); 758 | } 759 | } 760 | } 761 | 762 | get numberOfPages(): number { 763 | return this.pagination > 0 ? Math.ceil(this.rowsSet.size / this.pagination) : 1; 764 | } 765 | } 766 | 767 | customElements.define("action-table", ActionTable); 768 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- 1 | import { ActionTableEventDetail, FiltersObject } from "./types"; 2 | declare global { 3 | interface GlobalEventHandlersEventMap { 4 | "action-table": CustomEvent; 5 | "action-table-filter": CustomEvent; 6 | "action-table-update": CustomEvent<{ sort: string; filter: string } | string>; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "./action-table"; 2 | import "./action-table-filters"; 3 | import "./action-table-pagination"; 4 | -------------------------------------------------------------------------------- /src/random-number.ts: -------------------------------------------------------------------------------- 1 | class RandomNumberComponent extends HTMLElement { 2 | private randomNumber: number; 3 | 4 | constructor() { 5 | super(); 6 | const shadow = this.attachShadow({ mode: "open" }); 7 | this.randomNumber = Math.floor(Math.random() * 10) + 1; 8 | shadow.innerHTML = `${this.randomNumber}`; 9 | this.dispatch(this.randomNumber.toString()); 10 | 11 | // click event that rerandomizes the number 12 | this.addEventListener("click", () => { 13 | this.randomNumber = Math.floor(Math.random() * 10) + 1; 14 | shadow.innerHTML = `${this.randomNumber}`; 15 | this.dispatch(this.randomNumber.toString()); 16 | }); 17 | } 18 | 19 | private dispatch(detail: { sort: string; filter: string } | string) { 20 | // console.log("dispatch", detail); 21 | const event = new CustomEvent<{ sort: string; filter: string } | string>("action-table-update", { 22 | detail, 23 | bubbles: true, 24 | }); 25 | this.dispatchEvent(event); 26 | } 27 | } 28 | 29 | customElements.define("random-number", RandomNumberComponent); 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ColsArray = { name: string; order?: string[] }[]; 2 | 3 | export type SingleFilterObject = { 4 | values: string[]; 5 | exclusive?: boolean; 6 | regex?: boolean; 7 | exact?: boolean; 8 | range?: boolean; 9 | cols?: string[]; 10 | }; 11 | 12 | export type FiltersObject = { 13 | [key: string]: SingleFilterObject; 14 | }; 15 | 16 | export type ActionTableCellData = { 17 | sort: string; 18 | filter: string; 19 | }; 20 | 21 | export type PaginationProps = { page?: number; pagination?: number; rowsShown?: number }; 22 | 23 | export type ActionTableEventDetail = { 24 | page?: number; 25 | pagination?: number; 26 | numberOfPages?: number; 27 | rowsVisible?: number; 28 | }; 29 | 30 | // export type ActionTableSortStore = { sort: string; direction: "ascending" | "descending" }; 31 | 32 | export type UpdateContentDetail = { sort?: string; filter?: string } | string; 33 | 34 | export type Direction = "ascending" | "descending"; 35 | 36 | export type ActionTableStore = { 37 | filters?: FiltersObject; 38 | sort?: string; 39 | direction?: Direction; 40 | }; 41 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Action 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
AB
24
15
36
47
58
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM","DOM.Iterable"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import minifyHTML from "rollup-plugin-minify-html-literals"; 4 | import eslint from "vite-plugin-eslint"; 5 | // https://vitejs.dev/config/ 6 | 7 | export default defineConfig(({ mode }) => { 8 | return { 9 | esbuild: { 10 | drop: mode === "production" ? ["console", "debugger"] : [], 11 | }, 12 | build: { 13 | sourcemap: true, 14 | modulePreload: { 15 | polyfill: false, 16 | }, 17 | rollupOptions: { 18 | input: { 19 | main: resolve(__dirname, "index.html"), 20 | index: resolve(__dirname, "src/main.ts"), 21 | "action-table": resolve(__dirname, "src/action-table.ts"), 22 | "action-table-filters": resolve(__dirname, "src/action-table-filters.ts"), 23 | "action-table-pagination": resolve(__dirname, "src/action-table-pagination.ts"), 24 | "action-table-switch": resolve(__dirname, "src/action-table-switch.ts"), 25 | }, 26 | output: [ 27 | { 28 | entryFileNames: `[name].js`, 29 | assetFileNames: `action-table.[ext]`, 30 | dir: "dist", 31 | }, 32 | ], 33 | plugins: [ 34 | eslint(), 35 | // @ts-expect-error types are missing 36 | minifyHTML.default({ 37 | options: { 38 | shouldMinify(template) { 39 | return template.parts.some((part) => { 40 | // Matches Polymer templates that are not tagged 41 | return part.text.includes("