├── LICENSE ├── README.md ├── example.gif ├── pagination.gif ├── src ├── download.svg ├── neptune.css ├── neptune.js ├── sort-down.svg ├── sort-up.svg └── sort.svg └── validations.gif /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Alejandro Torres Hernández 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neptune 2 | 3 | Neptune is a table component that allows you to build tables faster. Just write a small html and observe the results: 4 | ```html 5 |
7 | n-url="/your/data" 8 | n-theme="normal" 9 | n-title 10 | n-update="/your/data/update" 11 | n-key="id" 12 | n-show-key="false" 13 | n-delete="/your/data/delete/{key}" 14 | n-pagination="/your/data/next-page/{page}" 15 | n-load-spinner="normal_spinner" 16 | n-progress-spinner="small_spinner" 17 | > 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ``` 27 | ![](example.gif) 28 | 29 | Neptune can also show **validation errors**. 30 | 31 | # Table of Contents 32 | 1. [Installation](#installation) 33 | 2. [Basic example](#basic-example) 34 | 3. [Update and n-key](#update) 35 | 4. [Delete](#delete) 36 | 5. [Server side Pagination](#server-pagination) 37 | 6. [Client Side Pagination](#client-pagination) 38 | 7. [Sorting](#sorting) 39 | 8. [Download as CSV](#csv-download) 40 | 9. [Full example](#full-example) 41 | 10. [Validation errors](#validation-errors) 42 | 11. [Themes](#themes) 43 | 44 | ## Installation 45 | 46 | Just include the library using the CDN and you are good to go. 47 | ```html 48 | 49 | 50 | 51 | ``` 52 | 53 | Also, you can download the files and use them locally 54 | ```js 55 | 56 | 57 | 58 | ``` 59 | 60 | ## Usage 61 | 62 | ### Basic example 63 | 64 | The `n-url` attribute defines the endpoint from where to get the data. The library expects the data in a `rows` property. 65 | 66 | Endpoint example 67 | ```js 68 | router.get("/your/data", async function(req, res) { 69 | const data = await query("SELECT * FROM table") 70 | res.json({ rows: data }) 71 | }) 72 | ``` 73 | 74 | Table component 75 | ```html 76 |
78 | n-url="/your/data" 79 | n-theme="normal" 80 | n-title 81 | > 82 |
83 | ``` 84 | 85 | ### Update and n-key 86 | 87 | You can pass the `n-update` attribute to update the information using a modal. To display the modal you double click the row. It's important to provide the `n-key` attribute to define your row key, in this way, the library will pass it to your endpoint. The data to be updated and the key value will be send using the POST method, in the format of a json object. 88 | 89 | For example, let's say you have the next endpoint that returns your data: 90 | 91 | ```js 92 | router.get("/your/data", async function(req, res) { 93 | const data = await query("SELECT id, email, name, lastName FROM table") 94 | res.json({ rows: data }) 95 | }) 96 | ``` 97 | 98 | Then Neptune will use the table fields to construct the json object: 99 | 100 | ```js 101 | const data = { 102 | id: keyValue, 103 | email: emailValue, 104 | name: nameValue, 105 | lastName: lastNameValue 106 | } 107 | ``` 108 | You can use the next table component: 109 | 110 | ```html 111 |
113 | n-url="/your/data" 114 | n-theme="normal" 115 | n-title 116 | n-show-key="false" 117 | n-key="id" 118 | n-update="/your/data/update" 119 | n-load-spinner="normal_spinner" 120 | n-progress-spinner="small_spinner" 121 | > 122 | 123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | ``` 131 | 132 | You can use the `n-load-spinner` and `n-progress-spinner` attributes to define the ids of yours spinners. It's very important to put the spinners inside the `div` 133 | 134 | ### Delete 135 | 136 | The `n-delete` attribute will display a delete button in every row of the table. This attribute must have the pattern: `/your/data/delete/{key}`. You can put the `{key}` substring anywhere you want, this substring gets replaced by the actual key value. 137 | 138 | ```html 139 |
141 | n-url="/your/data" 142 | n-theme="normal" 143 | n-title 144 | n-show-key="false" 145 | n-key="id" 146 | n-delete="/your/data/delete/{key}" 147 | n-load-spinner="normal_spinner" 148 | n-progress-spinner="small_spinner" 149 | > 150 | 151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | ``` 159 | 160 | ### Server Side Pagination 161 | 162 | Use `n-pagination` attribute to show pagination buttons and define your endpoint to get more data. In order to show the pagination buttons you must provide the property `pageNumbers` in your `n-url` endpoint 163 | 164 | Endpoint example 165 | ```js 166 | router.get("/your/data", async function(req, res) { 167 | const pages = await query("SELECT COUNT(*) FROM table") 168 | const data = await query("SELECT * FROM table LIMIT 20") 169 | res.json({ rows: data, pageNumbers: pages }) 170 | }) 171 | ``` 172 | 173 | The library will use `pageNumbers` to show the buttons. 174 | 175 | ```html 176 |
178 | n-url="/your/data" 179 | n-theme="normal" 180 | n-title 181 | n-show-key="false" 182 | n-key="id" 183 | n-pagination="/your/data/next-page/{page}" 184 | n-load-spinner="normal_spinner" 185 | > 186 | 187 |
188 |
189 |
190 |
191 | ``` 192 | 193 | Also, notice the way you define the `n-pagination` attribute, is identical to `n-delete`. So you can put the `{page}` substring anywhere you want, this substring gets replaced by the actual page value. 194 | 195 | ![](pagination.gif) 196 | 197 | ### Client Side Pagination 198 | 199 | With client side pagination you can pull all the data you need and add page buttons to better organize the information. 200 | 201 | ```html 202 |
204 | n-url="/your/data" 205 | n-theme="normal" 206 | n-title 207 | n-show-key="false" 208 | n-key="id" 209 | n-update="/neptune/table/update" 210 | n-client-side="20" 211 | n-delete="/neptune/table/delete/{key}" 212 | n-load-spinner="normal_spinner" 213 | n-progress-spinner="small_spinner" 214 | > 215 | 216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | ``` 224 | 225 | With `n-client-side` you define the number of rows per page, and based on that Neptune will organize your data.} 226 | 227 | ### Sorting 228 | 229 | Sorting only works when you have pull all your data or when you are using client side pagination. The sorting functionality is by default. 230 | 231 | ### CSV download 232 | 233 | You can add a button to download the data as csv using `n-csv="true"`. However, for the moment this functionality only works when you have pull all your data 234 | 235 | ```html 236 |
238 | n-url="/your/data" 239 | n-theme="normal" 240 | n-title 241 | n-show-key="false" 242 | n-key="id" 243 | n-client-side="20" 244 | n-load-spinner="normal_spinner" 245 | n-csv="true" 246 | > 247 | 248 |
249 |
250 |
251 |
252 | ``` 253 | 254 | It is not necessary to use `n-client-side` for this to work. As long as you have pull all your data you will be ok. 255 | 256 | ### Full example 257 | 258 | ```html 259 |
261 | n-url="/your/data" 262 | n-theme="normal" 263 | n-title 264 | n-update="/your/data/update" 265 | n-key="id" 266 | n-show-key="false" 267 | n-delete="/your/data/delete/{key}" 268 | n-pagination="/your/data/next-page/{page}" 269 | n-load-spinner="normal_spinner" 270 | n-progress-spinner="small_spinner" 271 | > 272 | 273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 | ``` 281 | 282 | ### Validation errors 283 | 284 | You can show the validation errors returned by your endpoint. Neptune expects this errors to be in the `messages` property. This property must be an array. 285 | 286 | ![](validations.gif) 287 | 288 | ### Themes 289 | 290 | - normal 291 | - gray 292 | - black 293 | - green 294 | 295 | ### Font and css customization 296 | 297 | Donlowad the css file and modify the `:root` to change the font 298 | 299 | ```css 300 | :root { 301 | --font: "Roboto"; 302 | --font-weight: 500; 303 | } 304 | ``` 305 | 306 | Neptune uses the nexts properties to style the table 307 | 308 | ```js 309 | const themeExample = { 310 | table: "table-class", 311 | tr: "tr-class", 312 | th: "th-class", 313 | td: "td-class", 314 | delete: "delete-class", 315 | pages_theme: "pagination-class", 316 | page_active: "page-active-class" // active button 317 | } 318 | ``` 319 | 320 | For the moment the only way to add more themes is to go directly to the `js` file and call the function `nepAddTheme` with your theme object. 321 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolfVector/neptune-component/fd07562fe659e280c6ae7e56e214e13278fff527/example.gif -------------------------------------------------------------------------------- /pagination.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolfVector/neptune-component/fd07562fe659e280c6ae7e56e214e13278fff527/pagination.gif -------------------------------------------------------------------------------- /src/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/neptune.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap"); 2 | 3 | :root { 4 | --font: "Roboto"; 5 | --font-weight: 500; 6 | } 7 | 8 | .n-table { 9 | font-family: var(--font); 10 | font-size: 16px; 11 | border-collapse: collapse; 12 | width: 100%; 13 | } 14 | 15 | .n-th { 16 | text-align: left; 17 | padding: 15px 6px; 18 | font-weight: var(--font-weight); 19 | } 20 | 21 | .n-th-cursor:hover { 22 | cursor: pointer; 23 | } 24 | 25 | th div { 26 | display: flex; 27 | justify-content: space-between; 28 | } 29 | 30 | .n-th-normal { 31 | background: #3D3D3D; 32 | color: white; 33 | } 34 | 35 | .n-th:first-child { 36 | border-top-left-radius: 7px; 37 | } 38 | 39 | .n-th:last-child { 40 | border-top-right-radius: 7px; 41 | } 42 | 43 | /* Animation when deleting row */ 44 | .n-tr { 45 | transition: transform 0.5s linear; 46 | } 47 | 48 | .n-tr-remove { 49 | transform: translateX(100vw); 50 | } 51 | /*******************************/ 52 | 53 | .n-tr-normal:nth-child(even) { 54 | background-color: #f2f2f2; 55 | } 56 | 57 | .n-td { 58 | padding: 12px; 59 | } 60 | 61 | .n-update-cursor:hover { 62 | cursor: pointer; 63 | } 64 | 65 | .n-delete { 66 | padding: 7px; 67 | color: white; 68 | border: 0px; 69 | border-radius: 5px; 70 | font-family: var(--font); 71 | font-weight: var(--font-weight); 72 | } 73 | 74 | .n-delete-normal { 75 | background: rgb(227, 60, 60); 76 | } 77 | 78 | .n-delete:hover { 79 | cursor: pointer; 80 | } 81 | 82 | .n-pages-div { 83 | margin-top: 15px; 84 | text-align: center; 85 | } 86 | 87 | .n-pages { 88 | padding: 12px; 89 | font-size: 14px; 90 | font-family: var(--font); 91 | } 92 | 93 | .n-pages:first-child { 94 | border-top-left-radius: 6px; 95 | border-bottom-left-radius: 6px; 96 | } 97 | 98 | .n-pages:hover { 99 | cursor: pointer; 100 | } 101 | 102 | .n-pages-normal { 103 | background: white; 104 | color: #0d6efd; 105 | border: 1px solid #dee2e6; 106 | border-right: 0px; 107 | } 108 | 109 | .n-pages-normal-active { 110 | background: #0d6efd; 111 | color: white; 112 | } 113 | 114 | .n-pages-normal:not(:last-child) { 115 | border-right: 1px solid #ebedef; 116 | } 117 | 118 | .n-pages-normal:last-child { 119 | border-right: 1px solid #dee2e6; 120 | border-top-right-radius: 6px; 121 | border-bottom-right-radius: 6px; 122 | } 123 | 124 | .n-pages-green { 125 | color: #1dd5b0; 126 | } 127 | 128 | .n-pages-green-active { 129 | background: #1dd5b0; 130 | color: white; 131 | } 132 | 133 | .n-pages-black { 134 | background: rgb(87, 87, 87); 135 | color: white; 136 | border: 1px solid #626161; 137 | border-right: 0px; 138 | } 139 | 140 | .n-pages-black-active { 141 | background: rgb(120, 119, 119); 142 | color: white; 143 | } 144 | 145 | .n-pages-black:not(:last-child) { 146 | border-right: 1px solid #626161; 147 | } 148 | 149 | .n-pages-black:last-child { 150 | border-right: 1px solid #626161; 151 | border-top-right-radius: 6px; 152 | border-bottom-right-radius: 6px; 153 | } 154 | 155 | 156 | .n-th-black { 157 | background: rgb(62, 62, 62); 158 | color: rgba(255,255,255,0.9); 159 | } 160 | 161 | .n-tr-black { 162 | background: rgb(30, 30, 30); 163 | } 164 | 165 | .n-tr-black:nth-child(even) { 166 | background-color: rgb(42, 41, 41); 167 | } 168 | 169 | .n-td-black { 170 | color: rgba(255,255,255,0.9); 171 | } 172 | 173 | .n-delete-black { 174 | background: rgb(228, 85, 85); 175 | } 176 | 177 | 178 | .n-th-gray { 179 | color: rgb(84, 84, 84); 180 | background: #ededed; 181 | } 182 | 183 | .n-tr-gray:nth-child(even) { 184 | background: #f6f6f6; 185 | } 186 | 187 | .n-tr-gray { 188 | background: #fafafa; 189 | } 190 | 191 | .n-delete-gray { 192 | background: rgb(228, 85, 85); 193 | } 194 | 195 | 196 | .n-th-green { 197 | color: white; 198 | background: #009879; 199 | } 200 | 201 | .n-tr-green:nth-child(even) { 202 | background: #f3f3f3; 203 | } 204 | 205 | 206 | /*********** 207 | DIALOG 208 | ************/ 209 | 210 | .n-dialog-styles { 211 | border: 0px; 212 | border-radius: 6px; 213 | -webkit-box-shadow: 0px 0px 7px 1px rgba(0,0,0,0.58); 214 | -moz-box-shadow: 0px 0px 7px 1px rgba(0,0,0,0.58); 215 | box-shadow: 0px 0px 7px 1px rgba(0,0,0,0.58); 216 | } 217 | 218 | .n-dialog-label { 219 | display: block; 220 | margin-bottom: 6px; 221 | font-size: 14px; 222 | font-family: var(--font); 223 | font-weight: var(--font-weight); 224 | } 225 | 226 | .n-dialog-field { 227 | margin-top: 16px; 228 | } 229 | 230 | .n-dialog-input { 231 | font-family: var(--font); 232 | background: #f9fafb; 233 | border: 1px solid #c0c4c9; 234 | border-bottom: 2px solid #c0c4c9; 235 | border-radius: 4px; 236 | width: 50%; 237 | color: #20242f; 238 | padding: 10px; 239 | -webkit-transition: 0.5s; 240 | transition: 0.5s; 241 | outline: none; 242 | } 243 | 244 | .n-dialog-input:focus { 245 | border: 1px solid #3b4ce2; 246 | border-bottom: 2px solid #3b4ce2; 247 | } 248 | 249 | .n-div-btn { 250 | display: flex; 251 | justify-content: space-between; 252 | margin-top: 40px; 253 | } 254 | 255 | .n-div-delete-btn { 256 | margin-top: 40px; 257 | } 258 | 259 | .n-dialog-btn { 260 | font-family: var(--font); 261 | font-weight: var(--font-weight); 262 | border: 0px; 263 | border-radius: 4px; 264 | color: white; 265 | padding: 8px; 266 | } 267 | 268 | .n-dialog-btn:hover { 269 | cursor: pointer; 270 | } 271 | 272 | .n-dialog-btn-primary { 273 | background: rgb(54, 148, 242); 274 | } 275 | 276 | .n-dialog-btn-warning { 277 | background: #ffc107; 278 | } 279 | 280 | .n-dialog-title { 281 | font-size: 20px; 282 | font-family: var(--font); 283 | font-weight: var(--font-weight); 284 | } 285 | 286 | /********* 287 | SPINNER 288 | **********/ 289 | .n-div-spinner { 290 | width: 100%; 291 | display: none; 292 | text-align: center; 293 | margin-top: 7px; 294 | } 295 | 296 | .basic-loader { 297 | width: 48px; 298 | height: 48px; 299 | border: 5px solid #b0afaf; 300 | border-bottom-color: transparent; 301 | border-radius: 50%; 302 | display: inline-block; 303 | box-sizing: border-box; 304 | animation: rotation 1s linear infinite; 305 | } 306 | 307 | @keyframes rotation { 308 | 0% { 309 | transform: rotate(0deg); 310 | } 311 | 100% { 312 | transform: rotate(360deg); 313 | } 314 | } 315 | 316 | .small-loader { 317 | width: 28px; 318 | height: 28px; 319 | border: 5px solid #b0afaf; 320 | border-bottom-color: transparent; 321 | border-radius: 50%; 322 | display: inline-block; 323 | box-sizing: border-box; 324 | animation: rotation 1s linear infinite; 325 | } 326 | 327 | /************ 328 | ALERTS 329 | *************/ 330 | 331 | .n-alert { 332 | padding-top: 6px; 333 | margin: auto; 334 | width: 60%; 335 | font-size: 14px; 336 | font-family: var(--font); 337 | font-weight: var(--font-weight); 338 | border-radius: 4px; 339 | } 340 | 341 | .n-alert button { 342 | font-family: var(--font); 343 | font-weight: var(--font-weight); 344 | } 345 | 346 | .n-error-data { 347 | border: 1px solid #f69a9a; 348 | background-color: rgba(255, 56, 56, 0.05); 349 | color: #ff3838; 350 | } 351 | 352 | .n-try-again { 353 | background: white; 354 | font-size: 16px; 355 | margin-top: 12px; 356 | margin-bottom: 12px; 357 | border: 1px solid rgb(219, 217, 217); 358 | border-radius: 6px; 359 | padding: 10px; 360 | } 361 | 362 | .n-try-again:hover { 363 | cursor: pointer; 364 | } 365 | 366 | .n-success-data { 367 | border: 1px solid #6ac878; 368 | background-color: rgba(46, 201, 70, 0.05); 369 | color: #2ec946; 370 | padding-bottom: 6px; 371 | } 372 | 373 | .n-errors { 374 | padding-left: 8px; 375 | padding-bottom: 8px; 376 | text-align: start; 377 | } 378 | 379 | /*******CSV BUTTON**********/ 380 | 381 | .csv-button { 382 | display: flex; 383 | justify-content: end; 384 | margin-bottom: 6px; 385 | } 386 | 387 | .csv-button button { 388 | font-family: var(--font); 389 | font-weight: var(--font-weight); 390 | font-size: 10px; 391 | border: 0; 392 | border-radius: 4px; 393 | padding: 8px; 394 | } 395 | 396 | .csv-button button:hover { 397 | cursor: pointer; 398 | } 399 | 400 | .csv-button img { 401 | width: 25px; 402 | height: 20px; 403 | } 404 | 405 | .csv-button-normal button { 406 | background-color: #3f3f3f; 407 | } 408 | 409 | .csv-img-normal img { 410 | filter: brightness(0) saturate(100%) invert(85%) sepia(0%) saturate(30%) hue-rotate(298deg) brightness(110%) contrast(97%); 411 | } 412 | 413 | .csv-button-black button { 414 | background-color: #3c3c3c; 415 | } 416 | 417 | .csv-img-black img { 418 | filter: brightness(0) saturate(100%) invert(85%) sepia(0%) saturate(30%) hue-rotate(298deg) brightness(110%) contrast(97%); 419 | } 420 | 421 | .csv-button-gray button { 422 | background-color: #e6e4e4; 423 | } 424 | 425 | .csv-img-gray img { 426 | filter: brightness(0) saturate(100%) invert(15%) sepia(0%) saturate(0%) hue-rotate(136deg) brightness(103%) contrast(72%); 427 | } 428 | 429 | .csv-button-green button { 430 | background-color: #32b89d; 431 | } 432 | 433 | .csv-img-green img { 434 | filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(258%) hue-rotate(26deg) brightness(115%) contrast(98%); 435 | } 436 | 437 | /*****************/ 438 | 439 | /********** SORT ICON ***********/ 440 | 441 | .sort-normal { 442 | filter: brightness(0) saturate(100%) invert(64%) sepia(6%) saturate(12%) hue-rotate(314deg) brightness(91%) contrast(76%); 443 | } 444 | 445 | .sort-black { 446 | filter: brightness(0) saturate(100%) invert(53%) sepia(0%) saturate(972%) hue-rotate(274deg) brightness(85%) contrast(58%); 447 | } 448 | 449 | .sort-gray { 450 | filter: brightness(0) saturate(100%) invert(78%) sepia(9%) saturate(11%) hue-rotate(324deg) brightness(102%) contrast(73%); 451 | } 452 | 453 | .sort-green { 454 | filter: brightness(0) saturate(100%) invert(88%) sepia(91%) saturate(74%) hue-rotate(189deg) brightness(116%) contrast(86%); 455 | opacity: 0.7; 456 | } 457 | 458 | /******************************/ -------------------------------------------------------------------------------- /src/neptune.js: -------------------------------------------------------------------------------- 1 | async function nepHandleAsyncReq(url, body) { 2 | let res = await fetch(url, body); 3 | return ((res.ok == false) ? false : await res.json()); 4 | } 5 | 6 | let nepTableIds = 0 7 | let nepTableMetaData = {} 8 | let nepThemes = {} 9 | let nepEmptyTheme = { 10 | table: "", 11 | tr: "", 12 | th: "", 13 | td: "", 14 | delete: "", 15 | pages_theme: "", 16 | page_active: "" 17 | } 18 | 19 | function nepAddTheme(themeName, theme) { 20 | nepThemes[themeName] = theme 21 | } 22 | 23 | nepAddTheme("normal", { 24 | table: "n-table", 25 | tr: "n-tr n-tr-normal", 26 | th: "n-th n-th-normal", 27 | td: "n-td", 28 | delete: "n-delete n-delete-normal", 29 | pages_theme: "n-pages n-pages-normal", 30 | page_active: "n-pages-normal-active", 31 | sort: "sort-normal", 32 | csv: "csv-button csv-button-normal csv-img-normal" 33 | }) 34 | 35 | nepAddTheme("black", { 36 | table: "n-table", 37 | tr: "n-tr n-tr-black", 38 | th: "n-th n-th-black", 39 | td: "n-td n-td-black", 40 | delete: "n-delete n-delete-black", 41 | pages_theme: "n-pages n-pages-normal n-pages-black", 42 | page_active: "n-pages-black-active", 43 | sort: "sort-black", 44 | csv: "csv-button csv-button-black csv-img-black" 45 | }) 46 | 47 | nepAddTheme("gray", { 48 | table: "n-table", 49 | tr: "n-tr n-tr-gray", 50 | th: "n-th n-th-gray", 51 | td: "n-td", 52 | delete: "n-delete n-delete-gray", 53 | pages_theme: "n-pages n-pages-normal", 54 | page_active: "n-pages-normal-active", 55 | sort: "sort-gray", 56 | csv: "csv-button csv-button-gray csv-img-gray" 57 | }) 58 | 59 | nepAddTheme("green", { 60 | table: "n-table", 61 | tr: "n-tr n-tr-green", 62 | th: "n-th n-th-green", 63 | td: "n-td", 64 | delete: "n-delete n-delete-gray", 65 | pages_theme: "n-pages n-pages-normal n-pages-green", 66 | page_active: "n-pages-green-active", 67 | sort: "sort-green", 68 | csv: "csv-button csv-button-green csv-img-green" 69 | }) 70 | 71 | /** Get all elements with n-table attribute and show the tables */ 72 | async function nepTableComponent() { 73 | const elements = document.querySelectorAll('[n-table]') 74 | elements.forEach(async (element) => { 75 | await nepGetComponent(element) // Show table 76 | }) 77 | 78 | // Add dialog for update and delete rows 79 | const dialog = document.createElement("dialog") 80 | dialog.id = "n_dialog" 81 | dialog.classList.add("n-dialog-styles") 82 | document.body.append(dialog) 83 | } 84 | 85 | /** Show table */ 86 | async function nepGetComponent(element) { 87 | // Get attributes and save them in the metadata object 88 | const urlTable = element.getAttribute("n-url") 89 | let temp = element.getAttribute("n-theme") 90 | const theme = (nepThemes.hasOwnProperty(temp)) ? nepThemes[temp] : nepThemes["normal"] 91 | const n_key = element.getAttribute("n-key") 92 | const n_title = element.getAttribute("n-title") 93 | temp = element.getAttribute("n-show-key") 94 | const n_show_key = (temp) ? temp : "true" 95 | const n_update = element.getAttribute("n-update") 96 | const n_delete = element.getAttribute("n-delete") 97 | const n_pagination = element.getAttribute("n-pagination") 98 | const n_load_spinner = element.getAttribute("n-load-spinner") 99 | const n_progress_spinner = element.getAttribute("n-progress-spinner") 100 | const n_client_side = element.getAttribute("n-client-side") 101 | const n_csv = element.getAttribute("n-csv") 102 | const tableId = `n_table${nepTableIds++}` 103 | nepTableMetaData[tableId] = { 104 | titles: [], 105 | keys: [], 106 | n_title, 107 | urlUpdate: n_update, 108 | urlDelete: n_delete, 109 | urlPagination: n_pagination, 110 | n_key, 111 | n_show_key, 112 | theme, 113 | n_load_spinner, 114 | n_progress_spinner, 115 | n_client_side: parseInt(n_client_side), 116 | n_csv 117 | } 118 | 119 | const spinner = nepLoadSpinner(n_load_spinner) 120 | const res = await nepHandleAsyncReq(urlTable) 121 | 122 | nepHideSpinner(spinner) 123 | if(res === false) { 124 | nepShowDataError(element, "There was an error while getting the data" , () => nepGetComponent(element)) 125 | return 126 | } 127 | 128 | let title = '' 129 | let sortIcon = "" 130 | let applySorting = n_client_side || n_pagination === null 131 | 132 | if(applySorting) 133 | sortIcon = `` 134 | 135 | /* Get columns title */ 136 | if(res.rows.length) { 137 | const keys = Object.keys(res.rows[0]) 138 | for(let i=0;i < keys.length;i++) { 139 | if(n_show_key === "false" && keys[i] == n_key) continue 140 | 141 | let title_column = nepGetTitle(keys[i], n_title) 142 | nepTableMetaData[tableId].titles.push(title_column) 143 | nepTableMetaData[tableId].keys.push(keys[i]) 144 | title += `
${title_column}${sortIcon}
` 145 | } 146 | } 147 | 148 | if(n_delete) 149 | title += `Delete` 150 | 151 | //Pagination type 152 | const { currentRows, pageNumbers } = nepPaginationType(res, nepTableMetaData[tableId]) 153 | 154 | // Get rows 155 | let rowsTable = nepGetRows(currentRows, nepTableMetaData[tableId]) 156 | let pageNumbersElement = null 157 | 158 | // If client side was defined 159 | if(n_client_side) 160 | pageNumbersElement = nepAddPageNumbers("client", pageNumbers, tableId) 161 | else if(n_pagination) // else, check if server pagination was defined 162 | pageNumbersElement = nepAddPageNumbers("server", pageNumbers, tableId) 163 | 164 | let csvButton = "" 165 | if(n_csv === "true" && (n_client_side || n_pagination === null)) 166 | csvButton = `
` 167 | else if(n_csv === "true") 168 | console.log("NEPTUNE: for the moment the CSV download only works when you have pull all your data") 169 | 170 | element.classList.remove("div-parent") 171 | element.innerHTML += ` 172 | ${csvButton} 173 | 174 | 175 | 176 | ${title} 177 | 178 | 179 | 180 | ${rowsTable} 181 | 182 |
183 | ` 184 | // Add page numbers if necessary 185 | if(pageNumbersElement) 186 | element.append(pageNumbersElement) 187 | 188 | nepOnDelete(n_delete, element) // Add delete button if reuired 189 | nepOnUpdate(n_update, element) // Add update functionality if reuired 190 | if(applySorting) 191 | nepAddSorting(tableId) 192 | } 193 | 194 | function nepLoadSpinner(n_load_spinner) { 195 | let spinner = null 196 | if(n_load_spinner) { 197 | spinner = document.getElementById(n_load_spinner) 198 | spinner.style.display = "block" 199 | } 200 | 201 | return spinner 202 | } 203 | 204 | function nepHideSpinner(spinner) { 205 | if(spinner) 206 | spinner.style.display = "none" 207 | } 208 | 209 | function nepProgressSpinner(n_progress_spinner, dialog) { 210 | if(n_progress_spinner) { 211 | let spinner = document.getElementById(n_progress_spinner).cloneNode(true) 212 | spinner.style.display = "block" 213 | dialog.append(spinner) 214 | } 215 | } 216 | 217 | function nepHideProgressSpinner(n_progress_spinner, dialog) { 218 | if(n_progress_spinner) 219 | dialog.removeChild(dialog.lastChild) 220 | } 221 | 222 | function nepShowDataError(element, msg, callback) { 223 | const errorElement = document.createElement("div") 224 | errorElement.setAttribute("n-error-container", "") 225 | errorElement.innerHTML = ` 226 |
227 |
228 |
${msg}
229 | 230 |
231 |
232 | ` 233 | 234 | element.append(errorElement) 235 | element.lastChild.addEventListener("click", (e) => { 236 | if(e.target.tagName === "BUTTON" && e.target.innerText === "Try again") { 237 | element.removeChild(element.lastChild) // Remove the error message 238 | callback() 239 | } 240 | }) 241 | } 242 | 243 | function nepShowSuccess(element, msg) { 244 | element.innerHTML = ` 245 |
246 |
247 | ${msg} 248 |
249 |
250 | 251 |
252 |
253 | ` 254 | } 255 | 256 | function nepValidationErrors(dialog, messages) { 257 | let errors = "" 258 | messages.forEach(message => { 259 | errors += `
  • ${message}
  • ` 260 | }) 261 | 262 | const errorElement = document.createElement("div") 263 | errorElement.setAttribute("n-validation-errors", "") 264 | errorElement.style = "text-align: center; margin-top: 10px;" 265 | errorElement.innerHTML = ` 266 |
    267 | ${errors} 268 |
    ` 269 | 270 | dialog.append(errorElement) 271 | } 272 | 273 | function nepPaginationType(res, metaData) { 274 | let currentRows = res.rows 275 | let pageNumbers = res.pageNumbers 276 | const n_client_side = metaData.n_client_side 277 | 278 | if(n_client_side) { 279 | pageNumbers = Math.ceil(res.rows.length / n_client_side) 280 | metaData.rows = res.rows 281 | currentRows = res.rows.slice(0, n_client_side) 282 | } 283 | else if(metaData.urlPagination === null && metaData.n_csv === "true") 284 | metaData.rows = res.rows 285 | 286 | return { currentRows, pageNumbers } 287 | } 288 | 289 | function nepGetRows(rows, metaData) { 290 | let rowsTable = '' 291 | const n_key = metaData.n_key 292 | const theme = metaData.theme 293 | const n_show_key = metaData.n_show_key 294 | const n_delete = metaData.urlDelete 295 | const n_update = metaData.urlUpdate 296 | const n_client_side = metaData.n_client_side 297 | 298 | let updateCursor = "" 299 | if(n_update) 300 | updateCursor = "n-update-cursor" 301 | 302 | let rowsLimit = rows.length 303 | if(n_client_side) //ppppp 304 | rowsLimit = n_client_side 305 | 306 | // Construct the tbody 307 | for(let i=0;i < rowsLimit;i++) { 308 | rowsTable += `` 309 | const row = rows[i] 310 | 311 | for(const [key, value] of Object.entries(row)) { 312 | if(n_show_key === "false" && n_key == key) continue 313 | rowsTable += `${value}` 314 | } 315 | 316 | if(n_delete) 317 | rowsTable += `` 318 | 319 | rowsTable += "" 320 | } 321 | 322 | return rowsTable 323 | } 324 | 325 | /** Convert from fieldName or field_name to Field Name */ 326 | function nepGetTitle(title_column, n_title) { 327 | if(n_title !== null && /^[a-z][A-Za-z]*$/.test(title_column)) 328 | return title_column.replace(/([A-Z])/g, ' $1').replace(/^./, function(str){ return str.toUpperCase(); }) 329 | else if(n_title !== null && /^[a-zA-Z]+(_[a-zA-Z]+)*$/.test(title_column)) { 330 | title_column = title_column.toLowerCase() 331 | let words = title_column.replace(/_/g, " ").split(" ") 332 | for (var i = 0; i < words.length; i++) { 333 | words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1); 334 | } 335 | 336 | return words.join(" ") 337 | } 338 | 339 | return title_column 340 | } 341 | 342 | /** Delete button */ 343 | function nepOnDelete(n_delete, element) { 344 | if(n_delete) { 345 | element.addEventListener("click", (e) => { 346 | if(e.target.tagName === "BUTTON" && e.target.innerText === "Delete") { 347 | const dialog = document.getElementById("n_dialog") 348 | const parentNode = e.target.parentElement.parentElement 349 | const key = parentNode.getAttribute("n-key") 350 | const tableId = parentNode.parentElement.parentElement.id 351 | 352 | dialog.style.width = "30%" 353 | 354 | dialog.innerHTML = ` 355 |
    356 |
    Are you sure?
    357 | 358 | 359 |
    360 | 361 | 362 |
    363 |
    364 | ` 365 | 366 | dialog.showModal() 367 | } 368 | }) 369 | } 370 | } 371 | 372 | /** Delete request */ 373 | async function nepDeleteRow() { 374 | const rowKey = document.getElementById("n_key_row").value 375 | const tableId = document.getElementById("n_table_id").value 376 | const rowTr = document.getElementById(tableId).querySelector(`[n-key="${rowKey}"]`) 377 | const dialog = document.getElementById("n_dialog") 378 | 379 | const n_progress_spinner = nepTableMetaData[tableId].n_progress_spinner 380 | let urlDelete = nepTableMetaData[tableId].urlDelete 381 | if(!urlDelete.includes("{key}")) { 382 | console.log("NEPTUNE: the 'n-delete' attribute must include '{key}'") 383 | return 384 | } 385 | urlDelete = urlDelete.replace(/\{key\}/, rowKey) 386 | 387 | /* If the error element exists, then remove it */ 388 | const errorElement = dialog.querySelector("[n-error-container]") 389 | if(errorElement) 390 | errorElement.remove() 391 | 392 | nepProgressSpinner(n_progress_spinner, dialog) 393 | 394 | const res = await nepHandleAsyncReq(urlDelete, { 395 | method: "DELETE" 396 | }) 397 | 398 | nepHideProgressSpinner(n_progress_spinner, dialog) 399 | if(res === false) 400 | nepShowDataError(dialog, "There was an error while deleting the row", () => nepDeleteRow()) 401 | else { 402 | if(nepTableMetaData[tableId].rows) { 403 | let key = nepTableMetaData[tableId].n_key 404 | nepTableMetaData[tableId].rows = nepTableMetaData[tableId].rows.filter(row => row[key] != rowKey) 405 | } 406 | rowTr.classList.add("n-tr-remove") 407 | rowTr.addEventListener("transitionend", () => { 408 | 409 | rowTr.remove() 410 | }) 411 | nepShowSuccess(dialog, "The row was deleted") 412 | } 413 | } 414 | 415 | /** Update functionality */ 416 | function nepOnUpdate(n_update, element) { 417 | if(n_update) { 418 | element.addEventListener("dblclick", (e) => { 419 | if(e.target.tagName === "TD") { 420 | const dialog = document.getElementById("n_dialog") 421 | const parentNode = e.target.parentElement 422 | const key = parentNode.getAttribute("n-key") 423 | const tableId = parentNode.parentElement.parentElement.id 424 | 425 | const cellValues = Array.from(parentNode.children).map((child) => child.innerText) 426 | let formUpdate = "" 427 | nepTableMetaData[tableId].titles.forEach((element, index) => { 428 | formUpdate += ` 429 |
    430 | 431 | 432 |
    433 | ` 434 | }) 435 | 436 | dialog.style.width = "40%" 437 | 438 | dialog.innerHTML = formUpdate + ` 439 | 440 | 441 |
    442 | 443 | 444 |
    445 | ` 446 | dialog.showModal() 447 | } 448 | }) 449 | } 450 | } 451 | 452 | /** Update request */ 453 | async function nepUpdateRow() { 454 | const rowKey = document.getElementById("n_key_row").value 455 | const tableId = document.getElementById("n_table_id").value 456 | const urlUpdate = nepTableMetaData[tableId].urlUpdate 457 | const fields = nepTableMetaData[tableId].keys 458 | const dialog = document.getElementById("n_dialog") 459 | const inputs = dialog.querySelectorAll("input[type=text]") 460 | const n_progress_spinner = nepTableMetaData[tableId].n_progress_spinner 461 | const keyName = nepTableMetaData[tableId].n_key 462 | 463 | const body = {} 464 | body[keyName] = rowKey 465 | 466 | Array.from(inputs).forEach((element, index) => { 467 | body[fields[index]] = element.value 468 | }) 469 | 470 | 471 | /* If the error element exists, then remove it */ 472 | /*const errorElement = dialog.querySelector("[n-error-container]") 473 | if(errorElement) 474 | errorElement.remove()*/ 475 | 476 | /* If the validation element exists, then remove it */ 477 | const validation = dialog.querySelector("[n-validation-errors]") 478 | if(validation) 479 | validation.remove() 480 | 481 | nepProgressSpinner(n_progress_spinner, dialog) 482 | 483 | const res = await nepHandleAsyncReq(urlUpdate, { 484 | method: "POST", 485 | body: JSON.stringify(body), 486 | headers: { 487 | "Content-Type": "application/json", 488 | }, 489 | }) 490 | 491 | nepHideProgressSpinner(n_progress_spinner, dialog) 492 | 493 | /*if(res === false) 494 | nepShowDataError(dialog, "There was an error while updating the data", () => nepUpdateRow())*/ 495 | if(res?.messages?.length) 496 | nepValidationErrors(dialog, res.messages) 497 | else { 498 | let trRow = document.getElementById(tableId).querySelector(`[n-key="${rowKey}"]`) 499 | trRow = Array.from(trRow.children).filter((element) => element.firstChild.nodeName == "#text" ) 500 | trRow.forEach((element, index) => { 501 | element.innerHTML = inputs[index].value 502 | }) 503 | 504 | if(nepTableMetaData[tableId].rows) { 505 | let rows = nepTableMetaData[tableId].rows 506 | let key = nepTableMetaData[tableId].n_key 507 | 508 | for(i in rows) { 509 | let row = rows[i] 510 | if(row[key] == rowKey) { 511 | Array.from(inputs).forEach((element, index) => row[fields[index]] = element.value) 512 | break 513 | } 514 | } 515 | } 516 | nepShowSuccess(dialog, "The row was updated") 517 | } 518 | } 519 | 520 | function nepCloseDialog() { 521 | const dialog = document.getElementById("n_dialog") 522 | dialog.close() 523 | } 524 | 525 | /** Add pages */ 526 | function nepAddPageNumbers(renderingType, pageNumbers, tableId) { 527 | let pageNumbersElement = document.createElement("div") 528 | pageNumbersElement.classList.add("n-pages-div") 529 | 530 | if(renderingType == "server" && pageNumbers === null) { 531 | //TO CONSIDER: cursor pagination 532 | console.log("NEPTUNE: you are using 'n-pagination' but your endpoint does not return 'pageNumbers'") 533 | return 534 | } 535 | 536 | for(let i=0;i < pageNumbers;i++) { 537 | let pageTheme = nepTableMetaData[tableId].theme.pages_theme 538 | let pageActive = (i == 0) ? nepTableMetaData[tableId].theme.page_active : "" // Add active class 539 | pageNumbersElement.innerHTML += `` 540 | } 541 | 542 | if(renderingType === "server") { 543 | pageNumbersElement.addEventListener("click", (e) => { 544 | if(e.target.tagName === "BUTTON") { 545 | switchActivePage(e, tableId) 546 | 547 | const page = e.target.getAttribute("n-page-number") 548 | nepNextServerPage(page, tableId) 549 | } 550 | }) 551 | } 552 | else if(renderingType === "client") { 553 | pageNumbersElement.addEventListener("click", (e) => { 554 | if(e.target.tagName === "BUTTON") { 555 | switchActivePage(e, tableId) 556 | const page = e.target.getAttribute("n-page-number") 557 | nepNextClientPage(page, tableId) 558 | } 559 | }) 560 | } 561 | 562 | return pageNumbersElement 563 | } 564 | 565 | function switchActivePage(e, tableId) { 566 | const pageActive = nepTableMetaData[tableId].theme.page_active 567 | if(pageActive && pageActive != "") { 568 | /* Remove active class and add it to the new button */ 569 | const active = e.target.parentElement.querySelector("." + pageActive) 570 | active.classList.remove(pageActive) 571 | 572 | e.target.classList.add(pageActive) 573 | } 574 | } 575 | 576 | /** Next page request */ 577 | async function nepNextServerPage(page, tableId) { 578 | const n_load_spinner = nepTableMetaData[tableId].n_load_spinner 579 | const tableElement = document.getElementById(tableId).querySelector("tbody") 580 | let urlnepNextPage = nepTableMetaData[tableId].urlPagination 581 | 582 | if(!urlnepNextPage.includes("{page}")) { 583 | console.log("NEPTUNE: The 'n-pagination' attribute must include '{page}'") 584 | return 585 | } 586 | 587 | urlnepNextPage = urlnepNextPage.replace(/\{page\}/, page) 588 | tableElement.parentElement.scrollIntoView(true) 589 | 590 | let spinner = null 591 | if(n_load_spinner) { 592 | spinner = document.getElementById(n_load_spinner).cloneNode(true) 593 | spinner.style.display = "block" 594 | 595 | tableElement.innerHTML = `` 596 | tableElement.firstChild.firstChild.append(spinner) 597 | } 598 | 599 | const res = await nepHandleAsyncReq(urlnepNextPage) 600 | if(spinner) 601 | tableElement.removeChild(tableElement.firstElementChild) 602 | if(res === false) { 603 | tableElement.innerHTML = `` 604 | nepShowDataError(tableElement.firstChild.firstChild, "There was an error while getting the data", () => nepNextServerPage(urlnepNextPage, page, tableId)) 605 | } 606 | 607 | const rowsTable = nepGetRows(res.rows, nepTableMetaData[tableId]) 608 | 609 | tableElement.innerHTML = rowsTable 610 | 611 | } 612 | 613 | function nepNextClientPage(page, tableId) { 614 | const rows = nepRowSlice(tableId, page) 615 | const tableElement = document.getElementById(tableId).querySelector("tbody") 616 | 617 | // Scroll to the top and replace the tbody content with the new data 618 | tableElement.parentElement.scrollIntoView(true) 619 | const rowsTable = nepGetRows(rows, nepTableMetaData[tableId]) 620 | tableElement.innerHTML = rowsTable 621 | } 622 | 623 | function nepRowSlice(tableId, page) { 624 | const n_client_side = nepTableMetaData[tableId].n_client_side // Get the number of rows to display 625 | const nextPage = (page - 1) * n_client_side // Calculate the next page 626 | return nepTableMetaData[tableId].rows.slice(nextPage, nextPage + n_client_side) // Get the elements 627 | } 628 | 629 | /******** Donwload CSV ********/ 630 | function nepDownloadCSV(tableId) { 631 | /****** Fill CSV ******/ 632 | let csvRows = []; 633 | const headers = nepTableMetaData[tableId].titles 634 | const keys = nepTableMetaData[tableId].keys 635 | const rows = nepTableMetaData[tableId].rows 636 | 637 | csvRows.push(headers.join(',')); 638 | 639 | for(let i in rows) { 640 | const values = keys.map(e => { 641 | let row = rows[i] 642 | return row[e] 643 | }) 644 | csvRows.push(values.join(',')) 645 | } 646 | 647 | csvRows = csvRows.join('\n') 648 | 649 | /************ Download action *******************/ 650 | const blob = new Blob([csvRows], { type: 'text/csv' }); 651 | 652 | const url = window.URL.createObjectURL(blob) 653 | const a = document.createElement('a') 654 | 655 | a.setAttribute('href', url) 656 | a.setAttribute('download', 'data.csv'); 657 | a.click() 658 | } 659 | 660 | /***************** SORT TABLE ********************/ 661 | function nepAddSorting(tableId) { 662 | document.querySelectorAll("th").forEach(th => { 663 | if(th.innerText === "Delete") return 664 | 665 | th.classList.add("n-th-cursor") 666 | th.addEventListener("click", () => { 667 | nepTableMetaData[tableId].asc = !nepTableMetaData[tableId].asc 668 | const table = th.closest("table") 669 | 670 | /** Update sort icon **/ 671 | Array.from(table.querySelectorAll("th")).forEach(thElement => { 672 | // For the moment update all the others th elements to the default icon 673 | if(th !== thElement && thElement.innerText !== "Delete") 674 | thElement.querySelector("img").src = "https://cdn.jsdelivr.net/gh/WolfVector/neptune-component/src/sort.svg" 675 | }) 676 | 677 | // Update sort icon of current th 678 | if(nepTableMetaData[tableId].asc) 679 | th.querySelector("img").src = "https://cdn.jsdelivr.net/gh/WolfVector/neptune-component/src/sort-up.svg" 680 | else 681 | th.querySelector("img").src = "https://cdn.jsdelivr.net/gh/WolfVector/neptune-component/src/sort-down.svg" 682 | 683 | const key = Array.from(th.parentNode.children).indexOf(th) 684 | nepTableMetaData[tableId].rows 685 | .sort(nepComparer(nepTableMetaData[tableId].keys[key], nepTableMetaData[tableId].asc)) 686 | 687 | const tableElement = document.getElementById(tableId).querySelector("tbody") 688 | 689 | let rows = nepTableMetaData[tableId].rows 690 | if(nepTableMetaData[tableId].n_client_side) { 691 | const page = document.querySelector(".n-pages-div").querySelector('[class*="active"]').getAttribute("n-page-number") 692 | rows = nepRowSlice(tableId, page) 693 | } 694 | const rowsTable = nepGetRows(rows, nepTableMetaData[tableId]) 695 | 696 | tableElement.innerHTML = rowsTable 697 | }) 698 | }) 699 | } 700 | 701 | function nepComparer(columnIndex, asc) { 702 | return function(a, b) { 703 | let v1 = nepGetCellValue(asc ? a : b, columnIndex) 704 | let v2 = nepGetCellValue(asc ? b : a, columnIndex) 705 | const currencyRegex = /^\$[0-9]\d*(((,\d{3}){1})*(\.\d+)?)$/; 706 | const dateRegex = /^\d{4}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2}$/; 707 | 708 | // Check for currency 709 | if(currencyRegex.test(v1) && currencyRegex.test(v2)) { 710 | v1 = v1.replace(/[,\$]/g, "") 711 | v2 = v2.replace(/[,\$]/g, "") 712 | } 713 | // Check for dates with the format YYYY-Mon-DD 714 | else if(dateRegex.test(v1) && dateRegex.test(v2)) { 715 | let month1 = new Date(v1).getMonth() + 1 716 | let month2 = new Date(v2).getMonth() + 1 717 | 718 | v1 = v1.replace(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/, month1) 719 | v2 = v2.replace(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/, month2) 720 | } 721 | 722 | // if v1 and v2 are numbers 723 | if(v1 !== "" && v2 !== "" && !isNaN(v1) && !isNaN(v2)) 724 | return v1 - v2 725 | else // if they are strings 726 | return v1.toString().localeCompare(v2) 727 | } 728 | } 729 | 730 | function nepGetCellValue(row, index) { 731 | return row[index] 732 | } 733 | 734 | /****************************************************/ 735 | 736 | window.addEventListener("load", function(){ 737 | nepTableComponent() 738 | }); 739 | -------------------------------------------------------------------------------- /src/sort-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sort-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /validations.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolfVector/neptune-component/fd07562fe659e280c6ae7e56e214e13278fff527/validations.gif --------------------------------------------------------------------------------