├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── jstable.css ├── jstable.es5.min.js ├── jstable.min.js └── polyfill-fetch.min.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── server ├── data.json └── data.php └── src ├── jstable.js └── jstable.scss /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | npm-publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Publish if version has been updated 14 | uses: JS-DevTools/npm-publish@v1 15 | with: 16 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | # JSTable 2 | 3 | The JSTable is a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive. 4 | 5 | The plugin is similar to the [jQuery datatables](https://datatables.net/) but without the jQuery dependencies. 6 | 7 | The implementation is inspired by [Vanilla-DataTables](https://github.com/Mobius1/Vanilla-DataTables). Unlike Vanilla-Datatables this implementation is using the in ES6 introduced [classes](https://www.w3schools.com/js/js_classes.asp). 8 | Additionally JSTable includes the possibility for server side rendering, which is inspired by [jQuery datatables](https://datatables.net/manual/server-side). 9 | 10 | You can get more information about the usage on [https://jstable.github.io/](https://jstable.github.io/). 11 | 12 | ## Install 13 | 14 | * Clone the [github repository](https://github.com/jstable/JSTable) 15 | * Include the stylesheet and JavaScript files from the `dist` folder: 16 | 17 | ``` 18 | 19 | ``` 20 | ``` 21 | 22 | ``` 23 | * If the target browser does not support not all ES2015+ features you need to include the `es5` version: 24 | ``` 25 | 26 | ``` 27 | * If the target browser does not support fetch you need to include the following polyfills: 28 | ``` 29 | 30 | ``` 31 | 32 | ## Initialize 33 | 34 | The HTML table needs a `thead` and `tbody` section. 35 | 36 | #### Example table 37 | ``` 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ... 55 | 56 |
NameCountryDateNumber
Norman SmallTokelau2020-02-01 07:22:408243
57 | ``` 58 | 59 | #### JavaScript 60 | 61 | The JSTable can be initialized by passing a reference or a CSS3 selector as string: 62 | ``` 63 | let myTable = new JSTable("#basic"); 64 | ``` 65 | or 66 | ``` 67 | let table = document.getElementById('basic'); 68 | let myTable = new JSTable(table); 69 | ``` 70 | [Options](https://jstable.github.io/options.html) can be passed as second argument: 71 | 72 | ``` 73 | let myTable = new JSTable("#basic", { 74 | sortable: true, 75 | searchable: false, 76 | ... 77 | }); 78 | ``` 79 | -------------------------------------------------------------------------------- /dist/jstable.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSTable v1.6.5 3 | */.dt-container{position:relative;display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;overflow-y:hidden}.dt-container .dt-message{text-align:center}.dt-container .dt-loading{position:absolute;top:50%;left:50%;width:100%;margin-left:-50%;margin-top:-20px;height:40px;text-align:center;background-color:white;display:flex;justify-content:center;align-items:center;background:linear-gradient(to right, rgba(255,255,255,0) 0, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dt-top,.dt-bottom{padding:8px 10px;display:flex;justify-content:space-between}.dt-top .dt-selector,.dt-bottom .dt-selector{padding:6px}.dt-top .dt-info,.dt-bottom .dt-info{margin:7px 0}.dt-input{padding:6px 12px}.dt-pagination ul{margin:0;padding-left:0}.dt-pagination ul li{list-style:none;float:left}.dt-pagination a,.dt-pagination span{border:1px solid transparent;float:left;margin-left:2px;padding:6px 12px;position:relative;text-decoration:none;color:inherit}.dt-pagination a:hover{background-color:#d9d9d9}.dt-pagination .active a,.dt-pagination .active a:focus,.dt-pagination .active a:hover{background-color:#d9d9d9;cursor:default}.dt-pagination .dt-ellipsis span{cursor:not-allowed}.dt-pagination .disabled a,.dt-pagination .disabled a:focus,.dt-pagination .disabled a:hover{cursor:not-allowed;opacity:0.4}.dt-pagination .pager a{font-weight:bold}.dt-table{max-width:100%;width:100%;border-spacing:0}.dt-table>tbody>tr>td,.dt-table>tbody>tr>th,.dt-table>tfoot>tr>td,.dt-table>tfoot>tr>th,.dt-table>thead>tr>td,.dt-table>thead>tr>th{vertical-align:top;padding:8px 10px;white-space:nowrap}.dt-table>thead>tr>th,.dt-table>thead>tr>td{vertical-align:bottom;text-align:left;border-bottom:1px solid #d9d9d9}.dt-table>tfoot>tr>th,.dt-table>tfoot>tr>td{vertical-align:bottom;text-align:left;border-top:1px solid #d9d9d9}.dt-table th{vertical-align:bottom;text-align:left}.dt-table th.dt-sorter{position:relative;cursor:pointer;padding-right:20px}.dt-table th.dt-sorter::before,.dt-table th.dt-sorter::after{content:"";height:0;width:0;position:absolute;right:7px;border-left:4px solid transparent;border-right:4px solid transparent;opacity:0.2}.dt-table th.dt-sorter::before{border-top:4px solid #000;top:18px}.dt-table th.dt-sorter::after{border-bottom:4px solid #000;border-top:4px solid transparent;bottom:22px}.dt-table th.dt-sorter.asc::after,.dt-table th.dt-sorter.desc::before{opacity:0.6}.hidden{display:none !important;opacity:0 !important} 4 | -------------------------------------------------------------------------------- /dist/jstable.es5.min.js: -------------------------------------------------------------------------------- 1 | !function(){ 2 | /*! 3 | * JSTable v1.6.5 4 | */ 5 | const e={perPage:5,perPageSelect:[5,10,15,20,25],sortable:!0,searchable:!0,nextPrev:!0,firstLast:!1,prevText:"‹",nextText:"›",firstText:"«",lastText:"»",ellipsisText:"…",truncatePager:!0,pagerDelta:2,classes:{top:"dt-top",info:"dt-info",input:"dt-input",table:"dt-table",bottom:"dt-bottom",search:"dt-search",sorter:"dt-sorter",wrapper:"dt-wrapper",dropdown:"dt-dropdown",ellipsis:"dt-ellipsis",selector:"dt-selector",container:"dt-container",pagination:"dt-pagination",loading:"dt-loading",message:"dt-message"},labels:{placeholder:"Search...",perPage:"{select} entries per page",noRows:"No entries found",info:"Showing {start} to {end} of {rows} entries",loading:"Loading...",infoFiltered:"Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)"},layout:{top:"{select}{search}",bottom:"{info}{pager}"},serverSide:!1,deferLoading:null,ajax:null,ajaxParams:{},queryParams:{page:"page",search:"search",sortColumn:"sortColumn",sortDirection:"sortDirection",perPage:"perPage"},addQueryParams:!0,rowAttributesCreator:null,searchDelay:null,method:"GET"};class t{constructor(e){this.element=e,this.body=this.element.tBodies[0],this.head=this.element.tHead,this.rows=Array.from(this.element.rows).map((function(e,t){return new s(e,e.parentNode.nodeName,t)})),this.dataRows=this._getBodyRows(),this.header=this._getHeaderRow()}_getBodyRows(){return this.rows.filter((function(e){return!e.isHeader&&!e.isFooter}))}_getHeaderRow(){return this.rows.find((function(e){return e.isHeader}))}getColumnCount(){return this.header.getColumnCount()}getFooterRow(){return this.rows.find((function(e){return e.isFooter}))}}class s{constructor(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;this.cells=Array.from(e.cells).map((function(e){return new a(e)})),this.d=this.cells.length,this.isHeader="THEAD"===t,this.isFooter="TFOOT"===t,this.visible=!0,this.rowID=s;var r=this;this.attributes={},[...e.attributes].forEach((function(e){r.attributes[e.name]=e.value}))}getCells(){return Array.from(this.cells)}getColumnCount(){return this.cells.length}getCell(e){return this.cells[e]}getCellTextContent(e){return this.getCell(e).getTextContent()}static createFromData(e){let t=document.createElement("tr");if(e.hasOwnProperty("data")){if(e.hasOwnProperty("attributes"))for(const s in e.attributes)t.setAttribute(s,e.attributes[s]);e=e.data}return e.forEach((function(e){let s=document.createElement("td");if(s.innerHTML=e&&e.hasOwnProperty("data")?e.data:e,e&&e.hasOwnProperty("attributes"))for(const t in e.attributes)s.setAttribute(t,e.attributes[t]);t.appendChild(s)})),new s(t)}getFormatted(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,s=document.createElement("tr");var a=this;for(let e in this.attributes)s.setAttribute(e,this.attributes[e]);let r=t?t.call(this,this.getCells()):{};for(const e in r)s.setAttribute(e,r[e]);return this.getCells().forEach((function(t,r){var i=document.createElement("td");i.innerHTML=t.getInnerHTML(),e.hasOwnProperty(r)&&(i.innerHTML=e[r].call(a,t.getElement(),r)),t.classes.length>0&&(i.className=t.classes.join(" "));for(let e in t.attributes)i.setAttribute(e,t.attributes[e]);s.appendChild(i)})),s}setCellClass(e,t){this.cells[e].addClass(t)}}class a{constructor(e){this.textContent=e.textContent,this.innerHTML=e.innerHTML,this.className="",this.element=e,this.hasSortable=e.hasAttribute("data-sortable"),this.isSortable=this.hasSortable?"true"===e.getAttribute("data-sortable"):null,this.hasSort=e.hasAttribute("data-sort"),this.sortDirection=e.getAttribute("data-sort"),this.classes=[];var t=this;this.attributes={},[...e.attributes].forEach((function(e){t.attributes[e.name]=e.value}))}getElement(){return this.element}getTextContent(){return this.textContent}getInnerHTML(){return this.innerHTML}setClass(e){this.className=e}setSortable(e){this.isSortable=e}addClass(e){this.classes.push(e)}removeClass(e){this.classes.indexOf(e)>=0&&this.classes.splice(this.classes.indexOf(e),1)}addAttribute(e,t){this.attributes[e]=t}}class r{constructor(e){this.instance=e}getPages(){let e=Math.ceil(this.instance.getDataCount()/this.instance.config.perPage);return 0===e?1:e}render(){var e=this.instance.config;let t=this.getPages(),s=document.createElement("ul");if(t>1){let a=1===this.instance.currentPage?1:this.instance.currentPage-1,r=this.instance.currentPage===t?t:this.instance.currentPage+1;e.firstLast&&s.appendChild(this.createItem("pager",1,e.firstText)),e.nextPrev&&s.appendChild(this.createItem("pager",a,e.prevText)),this.truncate().forEach((function(e){s.appendChild(e)})),e.nextPrev&&s.appendChild(this.createItem("pager",r,e.nextText)),e.firstLast&&s.appendChild(this.createItem("pager",t,e.lastText))}return s}createItem(e,t,s,a){let r=document.createElement("li");return r.className=e,r.innerHTML=a?""+s+"":''+s+"",r}isValidPage(e){return e>0&&e<=this.getPages()}truncate(){var e,t=this,s=t.instance.config,a=2*s.pagerDelta,r=t.instance.currentPage,i=r-s.pagerDelta,n=r+s.pagerDelta,o=this.getPages(),l=[],c=[];if(this.instance.config.truncatePager){r<4-s.pagerDelta+a?n=3+a:r>this.getPages()-(3-s.pagerDelta+a)&&(i=this.getPages()-(2+a));for(var h=1;h<=o;h++)(1===h||h===o||h>=i&&h<=n)&&l.push(h);l.forEach((function(a){e&&(a-e==2?c.push(t.createItem("",e+1,e+1)):a-e!=1&&c.push(t.createItem(s.classes.ellipsis,0,s.ellipsisText,!0))),c.push(t.createItem(a==r?"active":"",a,a)),e=a}))}else for(let e=1;e<=this.getPages();e++)c.push(this.createItem(e===r?"active":"",e,e));return c}}window.JSTable=class{constructor(s){let a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=s;"string"==typeof s&&(i=document.querySelector(s)),null!==i&&(this.config=this._merge(e,a),this.table=new t(i),this.currentPage=1,this.columnRenderers=[],this.columnsNotSearchable=[],this.searchQuery=null,this.sortColumn=null,this.sortDirection="asc",this.isSearching=!1,this.dataCount=null,this.filteredDataCount=null,this.searchTimeout=null,this.pager=new r(this),this._build(),this._buildColumns(),this.update(null===this.config.deferLoading),this._bindEvents(),this._emit("init"),this._parseQueryParams())}_build(){let e=this.config;this.wrapper=document.createElement("div"),this.wrapper.className=e.classes.wrapper;var t=["
",e.layout.top,"
","
","","
","
",e.layout.bottom,"
"].join("");if(t=t.replace("{info}","
"),e.perPageSelect){var s=["
","","
"].join(""),a=document.createElement("select");a.className=e.classes.selector,e.perPageSelect.forEach((function(t){var s=t===e.perPage,r=new Option(t,t,s,s);a.add(r)})),s=s.replace("{select}",a.outerHTML),t=t.replace(/\{select\}/g,s)}else t=t.replace(/\{select\}/g,"");if(e.searchable){var r=["
","","
"].join("");t=t.replace(/\{search\}/g,r)}else t=t.replace(/\{search\}/g,"");this.table.element.classList.add(e.classes.table),t=t.replace("{pager}","
"),this.wrapper.innerHTML=t,this.table.element.parentNode.replaceChild(this.wrapper,this.table.element),this.wrapper.querySelector("."+e.classes.container).appendChild(this.table.element),this._updatePagination(),this._updateInfo()}async update(){let e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var t=this;this.currentPage>this.pager.getPages()&&(this.currentPage=this.pager.getPages());let s=t.wrapper.querySelector(" ."+t.config.classes.loading);if(s.classList.remove("hidden"),this.table.header.getCells().forEach((function(e,s){let a=t.table.head.rows[0].cells[s];a.innerHTML=e.getInnerHTML(),e.classes.length>0&&(a.className=e.classes.join(" "));for(let t in e.attributes)a.setAttribute(t,e.attributes[t]);a.setAttribute("data-sortable",e.isSortable)})),e)return this.getPageData(this.currentPage).then((function(e){t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",e.forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")})).then((function(){t.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),t._emit("update")})).then((function(){t._updatePagination(),t._updateInfo()}));t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",this.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),this._getData().forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")}_updatePagination(){let e=this.wrapper.querySelector(" ."+this.config.classes.pagination);e.innerHTML="",e.appendChild(this.pager.render(this.currentPage))}_updateInfo(){let e=this.wrapper.querySelector(" ."+this.config.classes.info),t=this.isSearching?this.config.labels.infoFiltered:this.config.labels.info;if(e&&t.length){var s=t.replace("{start}",this.getDataCount()>0?this._getPageStartIndex()+1:0).replace("{end}",this._getPageEndIndex()+1).replace("{page}",this.currentPage).replace("{pages}",this.pager.getPages()).replace("{rows}",this.getDataCount()).replace("{rowsTotal}",this.getDataCountTotal());e.innerHTML=s}}_getPageStartIndex(){return(this.currentPage-1)*this.config.perPage}_getPageEndIndex(){let e=this.currentPage*this.config.perPage-1;return e>this.getDataCount()-1?this.getDataCount()-1:e}_getData(){return this._emit("getData",this.table.dataRows),this.table.dataRows.filter((function(e){return e.visible}))}_fetchData(){var e=this;let t={searchQuery:this.searchQuery,sortColumn:this.sortColumn,sortDirection:this.sortDirection,start:this._getPageStartIndex(),length:this.config.perPage,datatable:1};t=Object.assign({},this.config.ajaxParams,t);let a=this.config.ajax+"?"+this._queryParams(t);return fetch(a,{method:this.config.method,credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then((function(e){return e.json()})).then((function(t){return e._emit("fetchData",t),e.dataCount=t.recordsTotal,e.filteredDataCount=t.recordsFiltered,t.data})).then((function(e){let t=[];return e.forEach((function(e){t.push(s.createFromData(e))})),t})).catch((function(e){console.error(e)}))}_queryParams(e){return Object.keys(e).map((t=>encodeURIComponent(t)+"="+encodeURIComponent(e[t]))).join("&")}getDataCount(){return this.isSearching?this.getDataCountFiltered():this.getDataCountTotal()}getDataCountFiltered(){return this.config.serverSide?this.filteredDataCount:this._getData().length}getDataCountTotal(){return this.config.serverSide?null!==this.config.deferLoading?this.config.deferLoading:this.dataCount:this.table.dataRows.length}getPageData(){if(this.config.serverSide)return this._fetchData();let e=this._getPageStartIndex();var t=this._getPageEndIndex();return Promise.resolve(this._getData()).then((function(s){return s.filter((function(s,a){return a>=e&&a<=t}))}))}async search(e){var t=this;if(this.searchQuery===e.toLowerCase())return!1;if(this.searchQuery=e.toLowerCase(),this.config.searchDelay){if(this.searchTimeout)return!1;this.searchTimeout=setTimeout((function(){t.searchTimeout=null}),this.config.searchDelay)}return this.currentPage=1,this.isSearching=!0,this.searchQuery.length?(this.config.serverSide||this.table.dataRows.forEach((function(e){e.visible=!1,t.searchQuery.split(" ").reduce((function(s,a){var r;let i=e.getCells();return i=i.filter((function(e,s){if(t.columnsNotSearchable.indexOf(s)<0)return!0})),r=i.some((function(e,t){if(e.getTextContent().toLowerCase().indexOf(a)>=0)return!0})),s&&r}),!0)&&(e.visible=!0)})),this.wrapper.classList.add("search-results"),this.update().then((function(){t._emit("search",e)}))):(this.table.dataRows.forEach((function(e){e.visible=!0})),this.isSearching=!1,t.wrapper.classList.remove("search-results"),t.update(),!1)}sort(e,t){let s=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var a=this;if(this.sortColumn=e||0,this.sortDirection=t,this.sortColumn<0||this.sortColumn>this.table.getColumnCount()-1)return!1;var r=this.table.header.getCell(this.sortColumn),i=this.table.dataRows;this.table.header.getCells().forEach((function(e){e.removeClass("asc"),e.removeClass("desc")})),r.addClass(this.sortDirection),this.config.serverSide||(i=i.sort((function(e,t){var s=e.getCellTextContent(a.sortColumn).toLowerCase(),r=t.getCellTextContent(a.sortColumn).toLowerCase();return s=s.replace(/(\$|\,|\s|%)/g,""),r=r.replace(/(\$|\,|\s|%)/g,""),s=isNaN(s)||""===s?s:parseFloat(s),r=isNaN(r)||""===r?r:parseFloat(r),""===s&&""!==r||!isNaN(s)&&isNaN(r)?"asc"===a.sortDirection?1:-1:""!==s&&""===r||isNaN(s)&&!isNaN(r)?"asc"===a.sortDirection?-1:1:"asc"===a.sortDirection?s===r?0:s>r?1:-1:s===r?0:s'+e+"",this.table.body.innerHTML="",this.table.body.appendChild(s)}_buildColumns(){var e=this;let t=null,s=null;this.config.columns&&this.config.columns.forEach((function(a){isNaN(a.select)||(a.select=[a.select]),a.select.forEach((function(r){var i=e.table.header.getCell(r);if(void 0!==i){if(a.hasOwnProperty("render")&&"function"==typeof a.render&&(e.columnRenderers[r]=a.render),a.hasOwnProperty("sortable")){let r=!1;i.hasSortable?r=i.isSortable:(r=a.sortable,i.setSortable(r)),r&&(i.addClass(e.config.classes.sorter),a.hasOwnProperty("sort")&&1===a.select.length&&(t=a.select[0],s=a.sort))}a.hasOwnProperty("searchable")&&(i.addAttribute("data-searchable",a.searchable),!1===a.searchable&&e.columnsNotSearchable.push(r))}}))})),this.table.header.getCells().forEach((function(a,r){null===a.isSortable&&a.setSortable(e.config.sortable),a.isSortable&&(a.addClass(e.config.classes.sorter),a.hasSort&&(t=r,s=a.sortDirection))})),null!==t&&e.sort(t,s,!0)}_merge(e,t){var s=this;return Object.keys(e).forEach((function(a){!t.hasOwnProperty(a)||"object"!=typeof t[a]||t[a]instanceof Array||null===t[a]?t.hasOwnProperty(a)||(t[a]=e[a]):s._merge(e[a],t[a])})),t}async _parseQueryParams(){const e=new URLSearchParams(window.location.search);let t=e.get(this.config.queryParams.perPage);if(t){t=parseInt(t),this.config.perPage=t,this.wrapper.querySelectorAll("."+this.config.classes.selector).forEach((function(e){e.querySelectorAll("option").forEach((e=>e.removeAttribute("selected"))),e.value=t,e.querySelector(`option[value='${t}']`).setAttribute("selected","")})),this.update()}let s=e.get(this.config.queryParams.search);if(s){this.wrapper.querySelectorAll("."+this.config.classes.input).forEach((function(e){e.value=s})),await this.search(s)}let a=e.get(this.config.queryParams.page);a&&await this.paginate(parseInt(a));let r=e.get(this.config.queryParams.sortColumn);if(r){r=parseInt(r);let t=e.get(this.config.queryParams.sortDirection);t=null==t?"asc":t,this.sort(r,t)}}}}(); -------------------------------------------------------------------------------- /dist/jstable.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSTable v1.6.5 3 | */ 4 | const JSTableDefaultConfig={perPage:5,perPageSelect:[5,10,15,20,25],sortable:!0,searchable:!0,nextPrev:!0,firstLast:!1,prevText:"‹",nextText:"›",firstText:"«",lastText:"»",ellipsisText:"…",truncatePager:!0,pagerDelta:2,classes:{top:"dt-top",info:"dt-info",input:"dt-input",table:"dt-table",bottom:"dt-bottom",search:"dt-search",sorter:"dt-sorter",wrapper:"dt-wrapper",dropdown:"dt-dropdown",ellipsis:"dt-ellipsis",selector:"dt-selector",container:"dt-container",pagination:"dt-pagination",loading:"dt-loading",message:"dt-message"},labels:{placeholder:"Search...",perPage:"{select} entries per page",noRows:"No entries found",info:"Showing {start} to {end} of {rows} entries",loading:"Loading...",infoFiltered:"Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)"},layout:{top:"{select}{search}",bottom:"{info}{pager}"},serverSide:!1,deferLoading:null,ajax:null,ajaxParams:{},queryParams:{page:"page",search:"search",sortColumn:"sortColumn",sortDirection:"sortDirection",perPage:"perPage"},addQueryParams:!0,rowAttributesCreator:null,searchDelay:null,method:"GET"};class JSTable{constructor(e,t={}){let s=e;"string"==typeof e&&(s=document.querySelector(e)),null!==s&&(this.config=this._merge(JSTableDefaultConfig,t),this.table=new JSTableElement(s),this.currentPage=1,this.columnRenderers=[],this.columnsNotSearchable=[],this.searchQuery=null,this.sortColumn=null,this.sortDirection="asc",this.isSearching=!1,this.dataCount=null,this.filteredDataCount=null,this.searchTimeout=null,this.pager=new JSTablePager(this),this._build(),this._buildColumns(),this.update(null===this.config.deferLoading),this._bindEvents(),this._emit("init"),this._parseQueryParams())}_build(){let e=this.config;this.wrapper=document.createElement("div"),this.wrapper.className=e.classes.wrapper;var t=["
",e.layout.top,"
","
","","
","
",e.layout.bottom,"
"].join("");if(t=t.replace("{info}","
"),e.perPageSelect){var s=["
","","
"].join(""),a=document.createElement("select");a.className=e.classes.selector,e.perPageSelect.forEach((function(t){var s=t===e.perPage,r=new Option(t,t,s,s);a.add(r)})),s=s.replace("{select}",a.outerHTML),t=t.replace(/\{select\}/g,s)}else t=t.replace(/\{select\}/g,"");if(e.searchable){var r=["
","","
"].join("");t=t.replace(/\{search\}/g,r)}else t=t.replace(/\{search\}/g,"");this.table.element.classList.add(e.classes.table),t=t.replace("{pager}","
"),this.wrapper.innerHTML=t,this.table.element.parentNode.replaceChild(this.wrapper,this.table.element),this.wrapper.querySelector("."+e.classes.container).appendChild(this.table.element),this._updatePagination(),this._updateInfo()}async update(e=!0){var t=this;this.currentPage>this.pager.getPages()&&(this.currentPage=this.pager.getPages());let s=t.wrapper.querySelector(" ."+t.config.classes.loading);if(s.classList.remove("hidden"),this.table.header.getCells().forEach((function(e,s){let a=t.table.head.rows[0].cells[s];a.innerHTML=e.getInnerHTML(),e.classes.length>0&&(a.className=e.classes.join(" "));for(let t in e.attributes)a.setAttribute(t,e.attributes[t]);a.setAttribute("data-sortable",e.isSortable)})),e)return this.getPageData(this.currentPage).then((function(e){t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",e.forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")})).then((function(){t.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),t._emit("update")})).then((function(){t._updatePagination(),t._updateInfo()}));t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",this.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),this._getData().forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")}_updatePagination(){let e=this.wrapper.querySelector(" ."+this.config.classes.pagination);e.innerHTML="",e.appendChild(this.pager.render(this.currentPage))}_updateInfo(){let e=this.wrapper.querySelector(" ."+this.config.classes.info),t=this.isSearching?this.config.labels.infoFiltered:this.config.labels.info;if(e&&t.length){var s=t.replace("{start}",this.getDataCount()>0?this._getPageStartIndex()+1:0).replace("{end}",this._getPageEndIndex()+1).replace("{page}",this.currentPage).replace("{pages}",this.pager.getPages()).replace("{rows}",this.getDataCount()).replace("{rowsTotal}",this.getDataCountTotal());e.innerHTML=s}}_getPageStartIndex(){return(this.currentPage-1)*this.config.perPage}_getPageEndIndex(){let e=this.currentPage*this.config.perPage-1;return e>this.getDataCount()-1?this.getDataCount()-1:e}_getData(){return this._emit("getData",this.table.dataRows),this.table.dataRows.filter((function(e){return e.visible}))}_fetchData(){var e=this;let t={searchQuery:this.searchQuery,sortColumn:this.sortColumn,sortDirection:this.sortDirection,start:this._getPageStartIndex(),length:this.config.perPage,datatable:1};t=Object.assign({},this.config.ajaxParams,t);let s=this.config.ajax+"?"+this._queryParams(t);return fetch(s,{method:this.config.method,credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then((function(e){return e.json()})).then((function(t){return e._emit("fetchData",t),e.dataCount=t.recordsTotal,e.filteredDataCount=t.recordsFiltered,t.data})).then((function(e){let t=[];return e.forEach((function(e){t.push(JSTableRow.createFromData(e))})),t})).catch((function(e){console.error(e)}))}_queryParams(e){return Object.keys(e).map((t=>encodeURIComponent(t)+"="+encodeURIComponent(e[t]))).join("&")}getDataCount(){return this.isSearching?this.getDataCountFiltered():this.getDataCountTotal()}getDataCountFiltered(){return this.config.serverSide?this.filteredDataCount:this._getData().length}getDataCountTotal(){return this.config.serverSide?null!==this.config.deferLoading?this.config.deferLoading:this.dataCount:this.table.dataRows.length}getPageData(){if(this.config.serverSide)return this._fetchData();let e=this._getPageStartIndex();var t=this._getPageEndIndex();return Promise.resolve(this._getData()).then((function(s){return s.filter((function(s,a){return a>=e&&a<=t}))}))}async search(e){var t=this;if(this.searchQuery===e.toLowerCase())return!1;if(this.searchQuery=e.toLowerCase(),this.config.searchDelay){if(this.searchTimeout)return!1;this.searchTimeout=setTimeout((function(){t.searchTimeout=null}),this.config.searchDelay)}return this.currentPage=1,this.isSearching=!0,this.searchQuery.length?(this.config.serverSide||this.table.dataRows.forEach((function(e){e.visible=!1,t.searchQuery.split(" ").reduce((function(s,a){var r;let i=e.getCells();return i=i.filter((function(e,s){if(t.columnsNotSearchable.indexOf(s)<0)return!0})),r=i.some((function(e,t){if(e.getTextContent().toLowerCase().indexOf(a)>=0)return!0})),s&&r}),!0)&&(e.visible=!0)})),this.wrapper.classList.add("search-results"),this.update().then((function(){t._emit("search",e)}))):(this.table.dataRows.forEach((function(e){e.visible=!0})),this.isSearching=!1,t.wrapper.classList.remove("search-results"),t.update(),!1)}sort(e,t,s=!1){var a=this;if(this.sortColumn=e||0,this.sortDirection=t,this.sortColumn<0||this.sortColumn>this.table.getColumnCount()-1)return!1;var r=this.table.header.getCell(this.sortColumn),i=this.table.dataRows;this.table.header.getCells().forEach((function(e){e.removeClass("asc"),e.removeClass("desc")})),r.addClass(this.sortDirection),this.config.serverSide||(i=i.sort((function(e,t){var s=e.getCellTextContent(a.sortColumn).toLowerCase(),r=t.getCellTextContent(a.sortColumn).toLowerCase();return s=s.replace(/(\$|\,|\s|%)/g,""),r=r.replace(/(\$|\,|\s|%)/g,""),s=isNaN(s)||""===s?s:parseFloat(s),r=isNaN(r)||""===r?r:parseFloat(r),""===s&&""!==r||!isNaN(s)&&isNaN(r)?"asc"===a.sortDirection?1:-1:""!==s&&""===r||isNaN(s)&&!isNaN(r)?"asc"===a.sortDirection?-1:1:"asc"===a.sortDirection?s===r?0:s>r?1:-1:s===r?0:s'+e+"",this.table.body.innerHTML="",this.table.body.appendChild(s)}_buildColumns(){var e=this;let t=null,s=null;this.config.columns&&this.config.columns.forEach((function(a){isNaN(a.select)||(a.select=[a.select]),a.select.forEach((function(r){var i=e.table.header.getCell(r);if(void 0!==i){if(a.hasOwnProperty("render")&&"function"==typeof a.render&&(e.columnRenderers[r]=a.render),a.hasOwnProperty("sortable")){let r=!1;i.hasSortable?r=i.isSortable:(r=a.sortable,i.setSortable(r)),r&&(i.addClass(e.config.classes.sorter),a.hasOwnProperty("sort")&&1===a.select.length&&(t=a.select[0],s=a.sort))}a.hasOwnProperty("searchable")&&(i.addAttribute("data-searchable",a.searchable),!1===a.searchable&&e.columnsNotSearchable.push(r))}}))})),this.table.header.getCells().forEach((function(a,r){null===a.isSortable&&a.setSortable(e.config.sortable),a.isSortable&&(a.addClass(e.config.classes.sorter),a.hasSort&&(t=r,s=a.sortDirection))})),null!==t&&e.sort(t,s,!0)}_merge(e,t){var s=this;return Object.keys(e).forEach((function(a){!t.hasOwnProperty(a)||"object"!=typeof t[a]||t[a]instanceof Array||null===t[a]?t.hasOwnProperty(a)||(t[a]=e[a]):s._merge(e[a],t[a])})),t}async _parseQueryParams(){const e=new URLSearchParams(window.location.search);let t=e.get(this.config.queryParams.perPage);if(t){t=parseInt(t),this.config.perPage=t,this.wrapper.querySelectorAll("."+this.config.classes.selector).forEach((function(e){e.querySelectorAll("option").forEach((e=>e.removeAttribute("selected"))),e.value=t,e.querySelector(`option[value='${t}']`).setAttribute("selected","")})),this.update()}let s=e.get(this.config.queryParams.search);if(s){this.wrapper.querySelectorAll("."+this.config.classes.input).forEach((function(e){e.value=s})),await this.search(s)}let a=e.get(this.config.queryParams.page);a&&await this.paginate(parseInt(a));let r=e.get(this.config.queryParams.sortColumn);if(r){r=parseInt(r);let t=e.get(this.config.queryParams.sortDirection);t=null==t?"asc":t,this.sort(r,t)}}}class JSTableElement{constructor(e){this.element=e,this.body=this.element.tBodies[0],this.head=this.element.tHead,this.rows=Array.from(this.element.rows).map((function(e,t){return new JSTableRow(e,e.parentNode.nodeName,t)})),this.dataRows=this._getBodyRows(),this.header=this._getHeaderRow()}_getBodyRows(){return this.rows.filter((function(e){return!e.isHeader&&!e.isFooter}))}_getHeaderRow(){return this.rows.find((function(e){return e.isHeader}))}getColumnCount(){return this.header.getColumnCount()}getFooterRow(){return this.rows.find((function(e){return e.isFooter}))}}class JSTableRow{constructor(e,t="",s=null){this.cells=Array.from(e.cells).map((function(e){return new JSTableCell(e)})),this.d=this.cells.length,this.isHeader="THEAD"===t,this.isFooter="TFOOT"===t,this.visible=!0,this.rowID=s;var a=this;this.attributes={},[...e.attributes].forEach((function(e){a.attributes[e.name]=e.value}))}getCells(){return Array.from(this.cells)}getColumnCount(){return this.cells.length}getCell(e){return this.cells[e]}getCellTextContent(e){return this.getCell(e).getTextContent()}static createFromData(e){let t=document.createElement("tr");if(e.hasOwnProperty("data")){if(e.hasOwnProperty("attributes"))for(const s in e.attributes)t.setAttribute(s,e.attributes[s]);e=e.data}return e.forEach((function(e){let s=document.createElement("td");if(s.innerHTML=e&&e.hasOwnProperty("data")?e.data:e,e&&e.hasOwnProperty("attributes"))for(const t in e.attributes)s.setAttribute(t,e.attributes[t]);t.appendChild(s)})),new JSTableRow(t)}getFormatted(e,t=null){let s=document.createElement("tr");var a=this;for(let e in this.attributes)s.setAttribute(e,this.attributes[e]);let r=t?t.call(this,this.getCells()):{};for(const e in r)s.setAttribute(e,r[e]);return this.getCells().forEach((function(t,r){var i=document.createElement("td");i.innerHTML=t.getInnerHTML(),e.hasOwnProperty(r)&&(i.innerHTML=e[r].call(a,t.getElement(),r)),t.classes.length>0&&(i.className=t.classes.join(" "));for(let e in t.attributes)i.setAttribute(e,t.attributes[e]);s.appendChild(i)})),s}setCellClass(e,t){this.cells[e].addClass(t)}}class JSTableCell{constructor(e){this.textContent=e.textContent,this.innerHTML=e.innerHTML,this.className="",this.element=e,this.hasSortable=e.hasAttribute("data-sortable"),this.isSortable=this.hasSortable?"true"===e.getAttribute("data-sortable"):null,this.hasSort=e.hasAttribute("data-sort"),this.sortDirection=e.getAttribute("data-sort"),this.classes=[];var t=this;this.attributes={},[...e.attributes].forEach((function(e){t.attributes[e.name]=e.value}))}getElement(){return this.element}getTextContent(){return this.textContent}getInnerHTML(){return this.innerHTML}setClass(e){this.className=e}setSortable(e){this.isSortable=e}addClass(e){this.classes.push(e)}removeClass(e){this.classes.indexOf(e)>=0&&this.classes.splice(this.classes.indexOf(e),1)}addAttribute(e,t){this.attributes[e]=t}}class JSTablePager{constructor(e){this.instance=e}getPages(){let e=Math.ceil(this.instance.getDataCount()/this.instance.config.perPage);return 0===e?1:e}render(){var e=this.instance.config;let t=this.getPages(),s=document.createElement("ul");if(t>1){let a=1===this.instance.currentPage?1:this.instance.currentPage-1,r=this.instance.currentPage===t?t:this.instance.currentPage+1;e.firstLast&&s.appendChild(this.createItem("pager",1,e.firstText)),e.nextPrev&&s.appendChild(this.createItem("pager",a,e.prevText)),this.truncate().forEach((function(e){s.appendChild(e)})),e.nextPrev&&s.appendChild(this.createItem("pager",r,e.nextText)),e.firstLast&&s.appendChild(this.createItem("pager",t,e.lastText))}return s}createItem(e,t,s,a){let r=document.createElement("li");return r.className=e,r.innerHTML=a?""+s+"":''+s+"",r}isValidPage(e){return e>0&&e<=this.getPages()}truncate(){var e,t=this,s=t.instance.config,a=2*s.pagerDelta,r=t.instance.currentPage,i=r-s.pagerDelta,n=r+s.pagerDelta,o=this.getPages(),l=[],c=[];if(this.instance.config.truncatePager){r<4-s.pagerDelta+a?n=3+a:r>this.getPages()-(3-s.pagerDelta+a)&&(i=this.getPages()-(2+a));for(var h=1;h<=o;h++)(1===h||h===o||h>=i&&h<=n)&&l.push(h);l.forEach((function(a){e&&(a-e==2?c.push(t.createItem("",e+1,e+1)):a-e!=1&&c.push(t.createItem(s.classes.ellipsis,0,s.ellipsisText,!0))),c.push(t.createItem(a==r?"active":"",a,a)),e=a}))}else for(let e=1;e<=this.getPages();e++)c.push(this.createItem(e===r?"active":"",e,e));return c}}window.JSTable=JSTable; -------------------------------------------------------------------------------- /dist/polyfill-fetch.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.WHATWGFetch={})}(this,(function(t){"use strict";var e="undefined"!=typeof globalThis&&globalThis||"undefined"!=typeof self&&self||void 0!==e&&e,r="URLSearchParams"in e,o="Symbol"in e&&"iterator"in Symbol,n="FileReader"in e&&"Blob"in e&&function(){try{return new Blob,!0}catch(t){return!1}}(),i="FormData"in e,s="ArrayBuffer"in e;if(s)var a=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],h=ArrayBuffer.isView||function(t){return t&&a.indexOf(Object.prototype.toString.call(t))>-1};function u(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(t)||""===t)throw new TypeError('Invalid character in header field name: "'+t+'"');return t.toLowerCase()}function f(t){return"string"!=typeof t&&(t=String(t)),t}function c(t){var e={next:function(){var e=t.shift();return{done:void 0===e,value:e}}};return o&&(e[Symbol.iterator]=function(){return e}),e}function d(t){this.map={},t instanceof d?t.forEach((function(t,e){this.append(e,t)}),this):Array.isArray(t)?t.forEach((function(t){this.append(t[0],t[1])}),this):t&&Object.getOwnPropertyNames(t).forEach((function(e){this.append(e,t[e])}),this)}function y(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function p(t){return new Promise((function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}}))}function l(t){var e=new FileReader,r=p(e);return e.readAsArrayBuffer(t),r}function b(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function m(){return this.bodyUsed=!1,this._initBody=function(t){var e;this.bodyUsed=this.bodyUsed,this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:n&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:i&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:r&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():s&&n&&((e=t)&&DataView.prototype.isPrototypeOf(e))?(this._bodyArrayBuffer=b(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):s&&(ArrayBuffer.prototype.isPrototypeOf(t)||h(t))?this._bodyArrayBuffer=b(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):r&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},n&&(this.blob=function(){var t=y(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){if(this._bodyArrayBuffer){var t=y(this);return t||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}return this.blob().then(l)}),this.text=function(){var t,e,r,o=y(this);if(o)return o;if(this._bodyBlob)return t=this._bodyBlob,e=new FileReader,r=p(e),e.readAsText(t),r;if(this._bodyArrayBuffer)return Promise.resolve(function(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o-1?o:r),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&n)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(n),!("GET"!==this.method&&"HEAD"!==this.method||"no-store"!==e.cache&&"no-cache"!==e.cache)){var i=/([?&])_=[^&]*/;if(i.test(this.url))this.url=this.url.replace(i,"$1_="+(new Date).getTime());else{this.url+=(/\?/.test(this.url)?"&":"?")+"_="+(new Date).getTime()}}}function E(t){var e=new FormData;return t.trim().split("&").forEach((function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}})),e}function T(t,e){if(!(this instanceof T))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');e||(e={}),this.type="default",this.status=void 0===e.status?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText=void 0===e.statusText?"":""+e.statusText,this.headers=new d(e.headers),this.url=e.url||"",this._initBody(t)}v.prototype.clone=function(){return new v(this,{body:this._bodyInit})},m.call(v.prototype),m.call(T.prototype),T.prototype.clone=function(){return new T(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new d(this.headers),url:this.url})},T.error=function(){var t=new T(null,{status:0,statusText:""});return t.type="error",t};var A=[301,302,303,307,308];T.redirect=function(t,e){if(-1===A.indexOf(e))throw new RangeError("Invalid status code");return new T(null,{status:e,headers:{location:t}})},t.DOMException=e.DOMException;try{new t.DOMException}catch(e){t.DOMException=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function _(r,o){return new Promise((function(i,a){var h=new v(r,o);if(h.signal&&h.signal.aborted)return a(new t.DOMException("Aborted","AbortError"));var u=new XMLHttpRequest;function c(){u.abort()}u.onload=function(){var t,e,r={status:u.status,statusText:u.statusText,headers:(t=u.getAllResponseHeaders()||"",e=new d,t.replace(/\r?\n[\t ]+/g," ").split("\r").map((function(t){return 0===t.indexOf("\n")?t.substr(1,t.length):t})).forEach((function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}})),e)};r.url="responseURL"in u?u.responseURL:r.headers.get("X-Request-URL");var o="response"in u?u.response:u.responseText;setTimeout((function(){i(new T(o,r))}),0)},u.onerror=function(){setTimeout((function(){a(new TypeError("Network request failed"))}),0)},u.ontimeout=function(){setTimeout((function(){a(new TypeError("Network request failed"))}),0)},u.onabort=function(){setTimeout((function(){a(new t.DOMException("Aborted","AbortError"))}),0)},u.open(h.method,function(t){try{return""===t&&e.location.href?e.location.href:t}catch(e){return t}}(h.url),!0),"include"===h.credentials?u.withCredentials=!0:"omit"===h.credentials&&(u.withCredentials=!1),"responseType"in u&&(n?u.responseType="blob":s&&h.headers.get("Content-Type")&&-1!==h.headers.get("Content-Type").indexOf("application/octet-stream")&&(u.responseType="arraybuffer")),!o||"object"!=typeof o.headers||o.headers instanceof d?h.headers.forEach((function(t,e){u.setRequestHeader(e,t)})):Object.getOwnPropertyNames(o.headers).forEach((function(t){u.setRequestHeader(t,f(o.headers[t]))})),h.signal&&(h.signal.addEventListener("abort",c),u.onreadystatechange=function(){4===u.readyState&&h.signal.removeEventListener("abort",c)}),u.send(void 0===h._bodyInit?null:h._bodyInit)}))}_.polyfill=!0,e.fetch||(e.fetch=_,e.Headers=d,e.Request=v,e.Response=T),t.Headers=d,t.Request=v,t.Response=T,t.fetch=_,Object.defineProperty(t,"__esModule",{value:!0})})); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const sass = require( 'gulp-sass' )(require('node-sass')); 3 | const rename = require('gulp-rename'); 4 | const terser = require('gulp-terser'); 5 | const autoprefixer = require('gulp-autoprefixer'); 6 | const webpackStream = require('webpack-stream'); 7 | const TerserPlugin = require("terser-webpack-plugin"); 8 | 9 | function sassTask(cb) { 10 | return gulp.src('src/jstable.scss') 11 | .pipe(sass({ outputStyle: 'compressed' }) 12 | .on('error', printError)) 13 | .pipe(autoprefixer()) 14 | .pipe(gulp.dest('dist')); 15 | } 16 | 17 | function uglifyTask(cb) { 18 | return gulp.src(['src/jstable.js']) 19 | .pipe(terser()) 20 | .on('error', printError) 21 | .pipe(rename({ 22 | suffix: '.min' 23 | })) 24 | .pipe(gulp.dest('dist')) 25 | } 26 | 27 | function uglifyCompatibilityTask(cb) { 28 | return gulp.src(['src/jstable.js']) 29 | .pipe(webpackStream({ 30 | output: { 31 | filename: 'jstable.js', 32 | }, 33 | mode: 'production', 34 | optimization: { 35 | minimize: true, 36 | minimizer: [ 37 | new TerserPlugin({ 38 | extractComments: false, 39 | }), 40 | ], 41 | }, 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.js$/, 46 | exclude: /node_modules/, 47 | use: { 48 | loader: "babel-loader", 49 | options: { 50 | presets: [ 51 | ['@babel/preset-env', { 52 | "useBuiltIns": "usage", 53 | "corejs": 3 54 | }] 55 | ] 56 | } 57 | } 58 | } 59 | ] 60 | } 61 | })) 62 | .pipe(rename({ 63 | suffix: '.es5.min' 64 | })) 65 | .pipe(gulp.dest('dist')) 66 | } 67 | 68 | 69 | function copyPolyfillFetch(cb) { 70 | return gulp.src('node_modules/whatwg-fetch/dist/fetch.umd.js') 71 | .pipe(terser()) 72 | .pipe(rename('polyfill-fetch.min.js')) 73 | .pipe(gulp.dest('dist')) 74 | } 75 | 76 | function watchTask(cb) { 77 | gulp.watch(['src/*.scss'], sassTask) 78 | gulp.watch('src/*.js', gulp.series(uglifyTask, uglifyCompatibilityTask)); 79 | } 80 | 81 | function printError(error) { 82 | console.log('---- Error ----'); 83 | console.log("message", error.cause.message); 84 | console.log("file", error.cause.filename); 85 | console.log("line", error.cause.line); 86 | console.log("col", error.cause.col); 87 | console.log("pos", error.cause.pos); 88 | console.log(""); 89 | 90 | // this will ensure that gulp will stop processing the pipeline without a crash 91 | this.emit('end'); 92 | } 93 | 94 | exports.sass = sassTask; 95 | exports.uglify = gulp.series(uglifyTask, uglifyCompatibilityTask, copyPolyfillFetch); 96 | 97 | exports.default = watchTask; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jstable/jstable", 3 | "version": "1.6.5", 4 | "description": "a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/jstable/JSTable.git" 8 | }, 9 | "license": "MIT", 10 | "bugs": { 11 | "url": "https://github.com/jstable/JSTable/issues" 12 | }, 13 | "homepage": "https://jstable.github.io/", 14 | "devDependencies": { 15 | "@babel/core": "^7.19.3", 16 | "@babel/preset-env": "^7.19.3", 17 | "babel-loader": "^8.2.5", 18 | "core-js": "^3.25.4", 19 | "gulp": "^4.0.2", 20 | "gulp-autoprefixer": "^8.0.0", 21 | "gulp-concat": "^2.6.1", 22 | "gulp-rename": "^2.0.0", 23 | "gulp-sass": "^5.1.0", 24 | "gulp-terser": "^2.1.0", 25 | "node-sass": "^7.0.3", 26 | "terser-webpack-plugin": "^5.3.6", 27 | "vinyl-named": "^1.1.0", 28 | "webpack": "^5.74.0", 29 | "webpack-stream": "^7.0.0", 30 | "whatwg-fetch": "^3.6.2" 31 | }, 32 | "browserslist": [ 33 | "> 1%", 34 | "last 2 versions", 35 | "not dead", 36 | "not ie <= 8" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /server/data.php: -------------------------------------------------------------------------------- 1 | 0) { 33 | return true; 34 | } 35 | } 36 | }); 37 | } 38 | 39 | /* 40 | * Sort 41 | */ 42 | if (!is_null($sortColumnIndex) && $sortColumnIndex !== FALSE && $sortColumnIndex !== "null" && is_numeric($sortColumnIndex)) { 43 | array_multisort(array_column($filtered, $sortColumnIndex), ($sortDirection === "asc" ? SORT_ASC : SORT_DESC), $filtered); 44 | } 45 | 46 | 47 | /** 48 | * Slice 49 | */ 50 | $response = array_slice($filtered, $start, $length); 51 | 52 | echo json_encode([ 53 | "recordsTotal" => count($data), 54 | "recordsFiltered" => count($filtered), 55 | "data" => $response 56 | ]); 57 | -------------------------------------------------------------------------------- /src/jstable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSTable v1.6.5 3 | */ 4 | 5 | const JSTableDefaultConfig = { 6 | perPage: 5, 7 | perPageSelect: [5, 10, 15, 20, 25], 8 | 9 | sortable: true, 10 | searchable: true, 11 | 12 | // Pagination 13 | nextPrev: true, 14 | firstLast: false, 15 | prevText: "‹", 16 | nextText: "›", 17 | firstText: "«", 18 | lastText: "»", 19 | ellipsisText: "…", 20 | truncatePager: true, 21 | pagerDelta: 2, 22 | 23 | classes: { 24 | top: "dt-top", 25 | info: "dt-info", 26 | input: "dt-input", 27 | table: "dt-table", 28 | bottom: "dt-bottom", 29 | search: "dt-search", 30 | sorter: "dt-sorter", 31 | wrapper: "dt-wrapper", 32 | dropdown: "dt-dropdown", 33 | ellipsis: "dt-ellipsis", 34 | selector: "dt-selector", 35 | container: "dt-container", 36 | pagination: "dt-pagination", 37 | loading: "dt-loading", 38 | message: "dt-message" 39 | }, 40 | 41 | // Customise the display text 42 | labels: { 43 | placeholder: "Search...", 44 | perPage: "{select} entries per page", 45 | noRows: "No entries found", 46 | info: "Showing {start} to {end} of {rows} entries", 47 | loading: "Loading...", 48 | infoFiltered: "Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)" 49 | }, 50 | 51 | // Customise the layout 52 | layout: { 53 | top: "{select}{search}", 54 | bottom: "{info}{pager}" 55 | }, 56 | 57 | // server side 58 | serverSide: false, 59 | // total count of elements 60 | deferLoading: null, 61 | // url for queries 62 | ajax: null, 63 | // additional params 64 | ajaxParams: {}, 65 | // query params names 66 | queryParams: { 67 | page: 'page', 68 | search: 'search', 69 | sortColumn: 'sortColumn', 70 | sortDirection: 'sortDirection', 71 | perPage: 'perPage' 72 | }, 73 | // append query params on events 74 | addQueryParams: true, 75 | 76 | rowAttributesCreator: null, 77 | 78 | searchDelay: null, 79 | 80 | // http method request, default is: GET 81 | method: 'GET' 82 | }; 83 | 84 | class JSTable { 85 | 86 | constructor(element, config = {}) { 87 | 88 | let DOMElement = element; 89 | if (typeof element === "string") { 90 | DOMElement = document.querySelector(element); 91 | } 92 | if (DOMElement === null) { 93 | return; 94 | } 95 | 96 | this.config = this._merge(JSTableDefaultConfig, config); 97 | this.table = new JSTableElement(DOMElement); 98 | 99 | // reset values 100 | this.currentPage = 1; 101 | this.columnRenderers = []; 102 | this.columnsNotSearchable = []; 103 | this.searchQuery = null; 104 | this.sortColumn = null; 105 | this.sortDirection = "asc"; 106 | this.isSearching = false; 107 | this.dataCount = null; 108 | this.filteredDataCount = null; 109 | this.searchTimeout = null; 110 | 111 | 112 | // init pager 113 | this.pager = new JSTablePager(this); 114 | 115 | // build wrapper and layout 116 | this._build(); 117 | this._buildColumns(); 118 | 119 | // update table content 120 | this.update(this.config.deferLoading === null); 121 | 122 | // bind events 123 | this._bindEvents(); 124 | 125 | this._emit("init"); 126 | 127 | this._parseQueryParams(); 128 | 129 | } 130 | 131 | _build() { 132 | var that = this; 133 | let options = this.config; 134 | 135 | this.wrapper = document.createElement("div"); 136 | this.wrapper.className = options.classes.wrapper; 137 | 138 | 139 | var inner = [ 140 | "
", options.layout.top, "
", 141 | "
", 142 | "", 143 | "
", 144 | "
", options.layout.bottom, "
" 145 | ].join(""); 146 | 147 | // Info placement 148 | inner = inner.replace( 149 | "{info}", 150 | "
" 151 | ); 152 | 153 | // Per Page Select 154 | if (options.perPageSelect) { 155 | var wrap = [ 156 | "
", 157 | "", 158 | "
" 159 | ].join(""); 160 | 161 | // Create the select 162 | var select = document.createElement("select"); 163 | select.className = options.classes.selector; 164 | 165 | // Create the options 166 | options.perPageSelect.forEach(function (val) { 167 | var selected = val === options.perPage; 168 | var option = new Option(val, val, selected, selected); 169 | select.add(option); 170 | }); 171 | 172 | // Custom label 173 | wrap = wrap.replace("{select}", select.outerHTML); 174 | 175 | // Selector placement 176 | inner = inner.replace(/\{select\}/g, wrap); 177 | } else { 178 | inner = inner.replace(/\{select\}/g, ""); 179 | } 180 | 181 | // Searchable 182 | if (options.searchable) { 183 | var form = [ 184 | "
", 185 | "", 186 | "
" 187 | ].join(""); 188 | 189 | // Search input placement 190 | inner = inner.replace(/\{search\}/g, form); 191 | } else { 192 | inner = inner.replace(/\{search\}/g, ""); 193 | } 194 | 195 | // Add table class 196 | 197 | this.table.element.classList.add(options.classes.table); 198 | 199 | // Pager 200 | 201 | inner = inner.replace( 202 | "{pager}", 203 | "
" 204 | ); 205 | 206 | this.wrapper.innerHTML = inner; 207 | 208 | this.table.element.parentNode.replaceChild(this.wrapper, this.table.element); 209 | 210 | let container = this.wrapper.querySelector("." + options.classes.container); 211 | container.appendChild(this.table.element); 212 | 213 | this._updatePagination(); 214 | this._updateInfo(); 215 | 216 | } 217 | 218 | async update(reloadData = true) { 219 | var that = this; 220 | 221 | // no overlap please 222 | if (this.currentPage > this.pager.getPages()) { 223 | this.currentPage = this.pager.getPages(); 224 | } 225 | 226 | let loading = that.wrapper.querySelector(" ." + that.config.classes.loading); 227 | loading.classList.remove("hidden"); 228 | 229 | // Create Header 230 | this.table.header.getCells().forEach(function (tableHeaderCell, columnIndex) { 231 | 232 | let th = that.table.head.rows[0].cells[columnIndex]; 233 | th.innerHTML = tableHeaderCell.getInnerHTML(); 234 | if (tableHeaderCell.classes.length > 0) { 235 | th.className = tableHeaderCell.classes.join(" "); 236 | } 237 | for (let attr in tableHeaderCell.attributes) { 238 | th.setAttribute(attr, tableHeaderCell.attributes[attr]); 239 | } 240 | th.setAttribute("data-sortable", tableHeaderCell.isSortable); 241 | }); 242 | 243 | if (reloadData) { 244 | 245 | // Change Table Body 246 | return this.getPageData(this.currentPage).then(function (data) { 247 | 248 | that.table.element.classList.remove("hidden"); 249 | that.table.body.innerHTML = ""; 250 | 251 | data.forEach(function (row) { 252 | that.table.body.appendChild(row.getFormatted(that.columnRenderers, that.config.rowAttributesCreator)); 253 | }); 254 | 255 | loading.classList.add("hidden"); 256 | 257 | }).then(function () { 258 | 259 | // No Data 260 | if (that.getDataCount() <= 0) { 261 | that.wrapper.classList.remove("search-results"); 262 | that.setMessage(that.config.labels.noRows); 263 | } 264 | 265 | that._emit("update"); 266 | 267 | }).then(function () { 268 | that._updatePagination(); 269 | that._updateInfo(); 270 | }); 271 | } 272 | // when there is a defer loading (server side) the initial data needs to be formatted 273 | else { 274 | that.table.element.classList.remove("hidden"); 275 | that.table.body.innerHTML = ""; 276 | 277 | // No Data 278 | if (this.getDataCount() <= 0) { 279 | that.wrapper.classList.remove("search-results"); 280 | that.setMessage(that.config.labels.noRows); 281 | } 282 | 283 | this._getData().forEach(function (row) { 284 | that.table.body.appendChild(row.getFormatted(that.columnRenderers, that.config.rowAttributesCreator)); 285 | }); 286 | loading.classList.add("hidden"); 287 | 288 | } 289 | } 290 | 291 | _updatePagination() { 292 | // change Pagination 293 | let pagination = this.wrapper.querySelector(" ." + this.config.classes.pagination); 294 | pagination.innerHTML = ""; 295 | pagination.appendChild(this.pager.render(this.currentPage)); 296 | 297 | 298 | } 299 | 300 | _updateInfo() { 301 | // change info 302 | let info = this.wrapper.querySelector(" ." + this.config.classes.info); 303 | 304 | let infoString = this.isSearching ? this.config.labels.infoFiltered : this.config.labels.info; 305 | if (info && infoString.length) { 306 | var string = infoString 307 | .replace("{start}", this.getDataCount() > 0 ? this._getPageStartIndex() + 1 : 0) 308 | .replace("{end}", this._getPageEndIndex() + 1) 309 | .replace("{page}", this.currentPage) 310 | .replace("{pages}", this.pager.getPages()) 311 | .replace("{rows}", this.getDataCount()) 312 | .replace("{rowsTotal}", this.getDataCountTotal()); 313 | 314 | info.innerHTML = string; 315 | } 316 | } 317 | 318 | _getPageStartIndex() { 319 | return (this.currentPage - 1) * this.config.perPage; 320 | } 321 | _getPageEndIndex() { 322 | let end = this.currentPage * this.config.perPage - 1; 323 | return end > this.getDataCount() - 1 ? this.getDataCount() - 1 : end; 324 | } 325 | 326 | _getData() { 327 | this._emit("getData", this.table.dataRows); 328 | return this.table.dataRows.filter(function (row) { 329 | return row.visible; 330 | }); 331 | } 332 | 333 | _fetchData() { 334 | var that = this; 335 | 336 | let params = { 337 | "searchQuery": this.searchQuery, 338 | "sortColumn": this.sortColumn, 339 | "sortDirection": this.sortDirection, 340 | "start": this._getPageStartIndex(), 341 | "length": this.config.perPage, 342 | "datatable": 1 343 | }; 344 | 345 | params = Object.assign({}, this.config.ajaxParams, params); 346 | 347 | let query = this.config.ajax + '?' + this._queryParams(params); 348 | 349 | return fetch(query, { 350 | method: this.config.method, 351 | credentials: "same-origin", 352 | headers: { 353 | 'Accept': 'application/json', 354 | 'Content-Type': 'application/json' 355 | } 356 | }).then(function (response) { 357 | return response.json(); 358 | }).then(function (json) { 359 | that._emit("fetchData", json); 360 | that.dataCount = json.recordsTotal; 361 | that.filteredDataCount = json.recordsFiltered; 362 | return json.data; 363 | }).then(function (data) { 364 | let rows = []; 365 | // Create Table 366 | data.forEach(function (dataRow) { 367 | rows.push(JSTableRow.createFromData(dataRow)); 368 | }); 369 | return rows; 370 | }).catch(function (error) { 371 | console.error(error); 372 | }); 373 | } 374 | 375 | _queryParams(params) { 376 | return Object.keys(params) 377 | .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 378 | .join('&'); 379 | } 380 | 381 | getDataCount() { 382 | if (this.isSearching) { 383 | return this.getDataCountFiltered(); 384 | } 385 | return this.getDataCountTotal(); 386 | } 387 | 388 | getDataCountFiltered() { 389 | if (this.config.serverSide) { 390 | return this.filteredDataCount; 391 | } 392 | return this._getData().length; 393 | } 394 | 395 | getDataCountTotal() { 396 | if (this.config.serverSide) { 397 | return this.config.deferLoading !== null ? this.config.deferLoading : this.dataCount; 398 | } 399 | return this.table.dataRows.length; 400 | } 401 | 402 | getPageData() { 403 | // return the ajax data with a promise 404 | if (this.config.serverSide) { 405 | return this._fetchData(); 406 | } 407 | 408 | 409 | // filter the table data and return a promise 410 | let start_idx = this._getPageStartIndex(); 411 | var end_idx = this._getPageEndIndex(); 412 | return Promise.resolve(this._getData()).then(function (data) { 413 | return data.filter(function (row, idx) { 414 | return idx >= start_idx && idx <= end_idx; 415 | }); 416 | }); 417 | } 418 | 419 | async search(query) { 420 | var that = this; 421 | 422 | // do nothing if the query has not changed since last search 423 | if (this.searchQuery === query.toLowerCase()) { 424 | return false; 425 | } 426 | 427 | this.searchQuery = query.toLowerCase(); 428 | 429 | // do not perform another search before enough time has passed since the last 430 | if (this.config.searchDelay) { 431 | if (this.searchTimeout) { 432 | return false; 433 | } else { 434 | // trigger search again after the given delay 435 | this.searchTimeout = setTimeout( 436 | function () { 437 | that.searchTimeout = null; 438 | }, 439 | this.config.searchDelay 440 | ); 441 | } 442 | } 443 | 444 | // reset parameters 445 | this.currentPage = 1; 446 | this.isSearching = true; 447 | 448 | // reset search 449 | if (!this.searchQuery.length) { 450 | // reset data to all table data 451 | this.table.dataRows.forEach(function (row) { 452 | row.visible = true; 453 | }); 454 | this.isSearching = false; 455 | that.wrapper.classList.remove("search-results"); 456 | that.update(); 457 | return false; 458 | } 459 | 460 | // search in all the data 461 | if (!this.config.serverSide) { 462 | this.table.dataRows.forEach(function (row) { 463 | row.visible = false; 464 | 465 | var match = that.searchQuery.split(" ").reduce(function (bool, word) { 466 | var hasMatch = false; 467 | 468 | let cells = row.getCells(); 469 | 470 | // only use searchable cells 471 | cells = cells.filter(function (cell, idx) { 472 | if (that.columnsNotSearchable.indexOf(idx) < 0) { 473 | return true; 474 | } 475 | }); 476 | 477 | hasMatch = cells.some(function (cell, idx) { 478 | if (cell.getTextContent().toLowerCase().indexOf(word) >= 0) { 479 | return true; 480 | } 481 | }); 482 | 483 | return bool && hasMatch; 484 | }, true); 485 | 486 | if (match) { 487 | row.visible = true; 488 | } 489 | }); 490 | } 491 | this.wrapper.classList.add("search-results"); 492 | 493 | return this.update().then(function () { 494 | that._emit("search", query); 495 | }); 496 | 497 | } 498 | 499 | sort(column, direction, initial = false) { 500 | var that = this; 501 | this.sortColumn = column || 0; 502 | this.sortDirection = direction; 503 | 504 | if (this.sortColumn < 0 || this.sortColumn > this.table.getColumnCount() - 1) { 505 | return false; 506 | } 507 | 508 | var node = this.table.header.getCell(this.sortColumn); 509 | var rows = this.table.dataRows; 510 | 511 | 512 | //Remove class from previus columns 513 | let tableHeaderCells = this.table.header.getCells(); 514 | tableHeaderCells.forEach(function (tableHeaderCell) { 515 | tableHeaderCell.removeClass("asc"); 516 | tableHeaderCell.removeClass("desc"); 517 | }); 518 | 519 | node.addClass(this.sortDirection); 520 | 521 | if (!this.config.serverSide) { 522 | rows = rows.sort(function (a, b) { 523 | var ca = a.getCellTextContent(that.sortColumn).toLowerCase(); 524 | var cb = b.getCellTextContent(that.sortColumn).toLowerCase(); 525 | 526 | // replace $, coma, whitespace and % 527 | ca = ca.replace(/(\$|\,|\s|%)/g, ""); 528 | cb = cb.replace(/(\$|\,|\s|%)/g, ""); 529 | 530 | ca = !isNaN(ca) && ca !== '' ? parseFloat(ca) : ca; 531 | cb = !isNaN(cb) && cb !== '' ? parseFloat(cb) : cb; 532 | 533 | // Sort empty cells or cells with different content types (numeric/not numeric) to top 534 | if ((ca === '' && cb !== '') || (!isNaN(ca) && isNaN(cb))) { 535 | return that.sortDirection === "asc" ? 1 : -1; 536 | } 537 | if ((ca !== '' && cb === '') || (isNaN(ca) && !isNaN(cb))) { 538 | return that.sortDirection === "asc" ? -1 : 1; 539 | } 540 | 541 | // Otherwise 542 | if (that.sortDirection === "asc") { 543 | return ca === cb ? 0 : ca > cb ? 1 : -1; 544 | } 545 | return ca === cb ? 0 : ca < cb ? 1 : -1; 546 | 547 | }); 548 | 549 | // replace dataRows with sorted rows 550 | this.table.dataRows = rows; 551 | 552 | } 553 | 554 | // initial sorting with serverSide on is not needed 555 | if (!this.config.serverSide || !initial) { 556 | this.update(); 557 | } 558 | 559 | this._emit("sort", this.sortColumn, this.sortDirection); 560 | 561 | } 562 | 563 | async paginate(new_page) { 564 | var that = this; 565 | 566 | this.currentPage = new_page; 567 | return this.update().then(function () { 568 | that._emit("paginate", that.currentPage, new_page); 569 | }); 570 | } 571 | 572 | _setQueryParam(key, value) { 573 | if (!this.config.addQueryParams) return; 574 | 575 | const url = new URL(window.location.href); 576 | url.searchParams.set(this.config.queryParams[key], value); 577 | window.history.replaceState(null, null, url); 578 | } 579 | 580 | _bindEvents() { 581 | var that = this; 582 | 583 | 584 | this.wrapper.addEventListener("click", function (event) { 585 | var node = event.target; 586 | 587 | if (node.hasAttribute("data-page")) { 588 | event.preventDefault(); 589 | let new_page = parseInt(node.getAttribute("data-page"), 10); 590 | that.paginate(new_page); 591 | 592 | that._setQueryParam('page', new_page) 593 | } 594 | 595 | if (node.nodeName === "TH" && node.hasAttribute("data-sortable")) { 596 | 597 | if (node.getAttribute("data-sortable") === "false") 598 | return false; 599 | 600 | event.preventDefault(); 601 | let sortDirection = node.classList.contains("asc") ? "desc" : "asc"; 602 | that.sort(node.cellIndex, sortDirection); 603 | 604 | that._setQueryParam('sortColumn', node.cellIndex) 605 | that._setQueryParam('sortDirection', sortDirection) 606 | } 607 | }); 608 | 609 | if (this.config.perPageSelect) { 610 | this.wrapper.addEventListener("change", function (e) { 611 | var node = e.target; 612 | if (node.nodeName === "SELECT" && node.classList.contains(that.config.classes.selector)) { 613 | e.preventDefault(); 614 | let value = parseInt(node.value, 10); 615 | that._emit("perPageChange", that.config.perPage, value); 616 | that.config.perPage = value; 617 | that.update(); 618 | 619 | that._setQueryParam('perPage', value) 620 | } 621 | }); 622 | } 623 | 624 | if (this.config.searchable) { 625 | this.wrapper.addEventListener("keyup", function (e) { 626 | if (e.target.nodeName === "INPUT" && e.target.classList.contains(that.config.classes.input)) { 627 | e.preventDefault(); 628 | that.search(e.target.value); 629 | 630 | that._setQueryParam('search', e.target.value) 631 | } 632 | }); 633 | } 634 | } 635 | 636 | on(event, callback) { 637 | this.events = this.events || {}; 638 | this.events[event] = this.events[event] || []; 639 | this.events[event].push(callback); 640 | } 641 | 642 | off(event, callback) { 643 | this.events = this.events || {}; 644 | if (event in this.events === false) 645 | return; 646 | this.events[event].splice(this.events[event].indexOf(callback), 1); 647 | } 648 | 649 | _emit(event) { 650 | this.events = this.events || {}; 651 | if (event in this.events === false) 652 | return; 653 | for (var i = 0; i < this.events[event].length; i++) { 654 | this.events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); 655 | } 656 | } 657 | 658 | setMessage(message) { 659 | var colspan = this.table.getColumnCount(); 660 | 661 | var node = document.createElement("tr"); 662 | node.innerHTML = '' + 665 | message + 666 | ""; 667 | 668 | this.table.body.innerHTML = ""; 669 | 670 | this.table.body.appendChild(node); 671 | } 672 | 673 | _buildColumns() { 674 | var that = this; 675 | 676 | /** 677 | * change sortable attribute of columns: 678 | * - a global definition of the sortable attribute of the table can be overwritten 679 | * by a "data-sortable" attribute on the table header or 680 | * by a custom column definitions attribute "sortable" 681 | * - a custom column definitions attribute "sortable" can be overwritten 682 | * by a "data-sortable" attribute on the table header 683 | * - a "data-sortable" attribute on the table header cannot be overwritten 684 | * 685 | * the initial sort column/direction can be defined on the table header (data-sort) or 686 | * on the custom column definitions (attribute "sort") with possible values "asc"/"desc" 687 | * - a custom column definitions attribute is overwritten by a "data-sort" attribute 688 | * - since sorting is only supported for 1 column at the same time the last defined column is used 689 | */ 690 | 691 | let initialSortColumn = null; 692 | let initialSortDirection = null; 693 | 694 | /** 695 | * Process custom column definitions 696 | */ 697 | if (this.config.columns) { 698 | 699 | this.config.columns.forEach(function (columnsDefinition) { 700 | 701 | // convert single column selection to array 702 | if (!isNaN(columnsDefinition.select)) { 703 | columnsDefinition.select = [columnsDefinition.select]; 704 | } 705 | 706 | // Add the data attributes to the th elements 707 | columnsDefinition.select.forEach(function (column) { 708 | var tableHeaderCell = that.table.header.getCell(column); 709 | 710 | // Skip missing 711 | if (tableHeaderCell === undefined) { 712 | return; 713 | } 714 | 715 | /** 716 | * Rendering 717 | */ 718 | if (columnsDefinition.hasOwnProperty("render") && typeof columnsDefinition.render === "function") { 719 | that.columnRenderers[column] = columnsDefinition.render; 720 | } 721 | 722 | /** 723 | * Sortable 724 | */ 725 | if (columnsDefinition.hasOwnProperty("sortable")) { 726 | 727 | let sortable = false; 728 | if (tableHeaderCell.hasSortable) { 729 | sortable = tableHeaderCell.isSortable; 730 | } else { 731 | sortable = columnsDefinition.sortable; 732 | tableHeaderCell.setSortable(sortable); 733 | } 734 | 735 | 736 | if (sortable) { 737 | 738 | tableHeaderCell.addClass(that.config.classes.sorter); 739 | 740 | // save sortable column/direction 741 | // when there is one selected column in columns definition 742 | // and this column should be sortable 743 | if (columnsDefinition.hasOwnProperty("sort") && columnsDefinition.select.length === 1) { 744 | initialSortColumn = columnsDefinition.select[0]; 745 | initialSortDirection = columnsDefinition.sort; 746 | } 747 | } 748 | } 749 | 750 | /** 751 | * Searchable (not serverside) 752 | */ 753 | if (columnsDefinition.hasOwnProperty("searchable")) { 754 | tableHeaderCell.addAttribute("data-searchable", columnsDefinition.searchable); 755 | 756 | if (columnsDefinition.searchable === false) { 757 | that.columnsNotSearchable.push(column); 758 | } 759 | } 760 | 761 | }); 762 | }); 763 | } 764 | 765 | /** 766 | * Process data-attributes 767 | */ 768 | this.table.header.getCells().forEach(function (tableHeaderCell, columnIndex) { 769 | 770 | if (tableHeaderCell.isSortable === null) { 771 | tableHeaderCell.setSortable(that.config.sortable); 772 | } 773 | 774 | if (tableHeaderCell.isSortable) { 775 | tableHeaderCell.addClass(that.config.classes.sorter); 776 | 777 | if (tableHeaderCell.hasSort) { 778 | initialSortColumn = columnIndex; 779 | initialSortDirection = tableHeaderCell.sortDirection; 780 | } 781 | } 782 | }); 783 | 784 | 785 | // sort the table by the last column which is marked to be sorted 786 | if (initialSortColumn !== null) { 787 | that.sort(initialSortColumn, initialSortDirection, true); 788 | } 789 | 790 | } 791 | 792 | // deep merge two objects 793 | _merge(current, update) { 794 | var that = this; 795 | Object.keys(current).forEach(function (key) { 796 | if (update.hasOwnProperty(key) && typeof update[key] === "object" && !(update[key] instanceof Array) && update[key] !== null) { 797 | that._merge(current[key], update[key]); 798 | } else if (!update.hasOwnProperty(key)) { 799 | update[key] = current[key]; 800 | } 801 | }); 802 | return update; 803 | } 804 | 805 | async _parseQueryParams() { 806 | const urlParams = new URLSearchParams(window.location.search); 807 | 808 | let perPage = urlParams.get(this.config.queryParams.perPage); 809 | if (perPage) { 810 | perPage = parseInt(perPage) 811 | this.config.perPage = perPage; 812 | let selectors = this.wrapper.querySelectorAll('.' + this.config.classes.selector); 813 | selectors.forEach(function (selector) { 814 | selector.querySelectorAll('option').forEach(opt => opt.removeAttribute('selected')) 815 | selector.value = perPage; 816 | selector.querySelector(`option[value='${perPage}']`).setAttribute('selected', '') 817 | }); 818 | this.update() 819 | } 820 | 821 | // parse search param and populate search 822 | let search = urlParams.get(this.config.queryParams.search); 823 | if (search) { 824 | let searchfields = this.wrapper.querySelectorAll('.' + this.config.classes.input); 825 | searchfields.forEach(function (searchfield) { 826 | searchfield.value = search; 827 | }); 828 | await this.search(search); 829 | } 830 | 831 | // parse page param and navigate to page 832 | let page = urlParams.get(this.config.queryParams.page); 833 | if (page) { 834 | await this.paginate(parseInt(page)); 835 | } 836 | 837 | let sortColumn = urlParams.get(this.config.queryParams.sortColumn); 838 | if (sortColumn) { 839 | sortColumn = parseInt(sortColumn) 840 | let sortDirection = urlParams.get(this.config.queryParams.sortDirection); 841 | sortDirection = (sortDirection == undefined) ? "asc" : sortDirection; 842 | this.sort(sortColumn, sortDirection); 843 | } 844 | } 845 | } 846 | 847 | class JSTableElement { 848 | 849 | constructor(element) { 850 | this.element = element; 851 | 852 | this.body = this.element.tBodies[0]; 853 | this.head = this.element.tHead; 854 | 855 | // we are modifying the data of the underlying element so first 856 | // make a copy for row extraction 857 | //let table = this.element.cloneNode(true); 858 | 859 | // Process table rows 860 | this.rows = Array.from(this.element.rows).map(function (row, rowID) { 861 | return new JSTableRow(row, row.parentNode.nodeName, rowID); 862 | }); 863 | 864 | this.dataRows = this._getBodyRows(); 865 | this.header = this._getHeaderRow(); 866 | 867 | } 868 | 869 | _getBodyRows() { 870 | return this.rows.filter(function (row) { 871 | return !row.isHeader && !row.isFooter; 872 | }); 873 | } 874 | 875 | _getHeaderRow() { 876 | return this.rows.find(function (row) { 877 | return row.isHeader; 878 | }); 879 | } 880 | 881 | getColumnCount() { 882 | return this.header.getColumnCount(); 883 | } 884 | 885 | getFooterRow() { 886 | return this.rows.find(function (row) { 887 | return row.isFooter; 888 | }); 889 | } 890 | 891 | } 892 | 893 | class JSTableRow { 894 | 895 | constructor(element, parentName = "", rowID = null) { 896 | // Process row cells 897 | this.cells = Array.from(element.cells).map(function (cell) { 898 | return new JSTableCell(cell); 899 | }); 900 | 901 | this.d = this.cells.length; 902 | 903 | //this.element = element; 904 | this.isHeader = parentName === "THEAD"; 905 | this.isFooter = parentName === "TFOOT"; 906 | this.visible = true; 907 | this.rowID = rowID; 908 | 909 | var that = this; 910 | // parse attributes 911 | this.attributes = {}; 912 | [...element.attributes].forEach(function (attr) { 913 | that.attributes[attr.name] = attr.value; 914 | }); 915 | } 916 | 917 | getCells() { 918 | return Array.from(this.cells); 919 | } 920 | 921 | getColumnCount() { 922 | return this.cells.length; 923 | } 924 | 925 | getCell(cell) { 926 | return this.cells[cell]; 927 | } 928 | 929 | // for sorting 930 | getCellTextContent(cell) { 931 | return this.getCell(cell).getTextContent(); 932 | } 933 | 934 | static createFromData(data) { 935 | let tr = document.createElement("tr"); 936 | 937 | if(data.hasOwnProperty("data")){ 938 | if(data.hasOwnProperty("attributes")){ 939 | for (const attrName in data["attributes"]) { 940 | tr.setAttribute(attrName, data["attributes"][attrName]) 941 | } 942 | } 943 | data = data["data"]; 944 | } 945 | 946 | data.forEach(function (cellData) { 947 | let td = document.createElement("td"); 948 | td.innerHTML = !!cellData && cellData.hasOwnProperty("data") ? cellData["data"]: cellData; 949 | 950 | if(!!cellData && cellData.hasOwnProperty("attributes")){ 951 | for (const attrName in cellData["attributes"]) { 952 | td.setAttribute(attrName, cellData["attributes"][attrName]) 953 | } 954 | } 955 | tr.appendChild(td); 956 | }); 957 | return new JSTableRow(tr); 958 | } 959 | 960 | getFormatted(columnRenderers, rowAttributesCreator = null) { 961 | let tr = document.createElement("tr"); 962 | var that = this; 963 | 964 | for (let attr in this.attributes) { 965 | tr.setAttribute(attr, this.attributes[attr]); 966 | } 967 | 968 | let rowAttributes = !!rowAttributesCreator ? rowAttributesCreator.call(this, this.getCells()): {}; 969 | for (const attrName in rowAttributes) { 970 | tr.setAttribute(attrName, rowAttributes[attrName]) 971 | } 972 | 973 | this.getCells().forEach(function (cell, idx) { 974 | var td = document.createElement('td'); 975 | td.innerHTML = cell.getInnerHTML(); 976 | if (columnRenderers.hasOwnProperty(idx)) { 977 | td.innerHTML = columnRenderers[idx].call(that, cell.getElement(), idx); 978 | } 979 | if (cell.classes.length > 0) { 980 | td.className = cell.classes.join(" "); 981 | } 982 | for (let attr in cell.attributes) { 983 | td.setAttribute(attr, cell.attributes[attr]); 984 | } 985 | tr.appendChild(td); 986 | }); 987 | return tr; 988 | 989 | } 990 | 991 | setCellClass(cell, className) { 992 | this.cells[cell].addClass(className); 993 | } 994 | } 995 | 996 | class JSTableCell { 997 | 998 | constructor(element) { 999 | this.textContent = element.textContent; 1000 | this.innerHTML = element.innerHTML; 1001 | this.className = ""; 1002 | this.element = element; 1003 | 1004 | this.hasSortable = element.hasAttribute("data-sortable"); 1005 | this.isSortable = this.hasSortable ? element.getAttribute("data-sortable") === "true" : null; 1006 | 1007 | this.hasSort = element.hasAttribute("data-sort"); 1008 | this.sortDirection = element.getAttribute("data-sort"); 1009 | 1010 | this.classes = []; 1011 | 1012 | var that = this; 1013 | // parse attributes 1014 | this.attributes = {}; 1015 | [...element.attributes].forEach(function (attr) { 1016 | that.attributes[attr.name] = attr.value; 1017 | }); 1018 | } 1019 | 1020 | getElement() { 1021 | return this.element; 1022 | } 1023 | 1024 | // for sorting 1025 | getTextContent() { 1026 | return this.textContent; 1027 | } 1028 | 1029 | // for rendering 1030 | getInnerHTML() { 1031 | return this.innerHTML; 1032 | } 1033 | 1034 | setClass(className) { 1035 | this.className = className; 1036 | } 1037 | 1038 | setSortable(value) { 1039 | this.isSortable = value; 1040 | //tableHeaderCell.setAttribute("data-sortable", sortable); 1041 | } 1042 | 1043 | addClass(value) { 1044 | this.classes.push(value); 1045 | } 1046 | 1047 | removeClass(value) { 1048 | if (this.classes.indexOf(value) >= 0) { 1049 | this.classes.splice(this.classes.indexOf(value), 1); 1050 | } 1051 | } 1052 | 1053 | addAttribute(key, value) { 1054 | this.attributes[key] = value; 1055 | } 1056 | } 1057 | 1058 | class JSTablePager { 1059 | 1060 | constructor(instance) { 1061 | this.instance = instance; 1062 | } 1063 | 1064 | getPages() { 1065 | let pages = Math.ceil(this.instance.getDataCount() / this.instance.config.perPage); 1066 | return pages === 0 ? 1 : pages; 1067 | } 1068 | 1069 | render() { 1070 | var options = this.instance.config; 1071 | let pages = this.getPages(); 1072 | 1073 | let ul = document.createElement("ul"); 1074 | if (pages > 1) { 1075 | 1076 | let prev = this.instance.currentPage === 1 ? 1 : this.instance.currentPage - 1, 1077 | next = this.instance.currentPage === pages ? pages : this.instance.currentPage + 1; 1078 | 1079 | // first button 1080 | if (options.firstLast) { 1081 | ul.appendChild(this.createItem("pager", 1, options.firstText)); 1082 | } 1083 | 1084 | // prev button 1085 | if (options.nextPrev) { 1086 | ul.appendChild(this.createItem("pager", prev, options.prevText)); 1087 | } 1088 | 1089 | var pager = this.truncate(); 1090 | // append the links 1091 | pager.forEach(function (btn) { 1092 | ul.appendChild(btn); 1093 | }); 1094 | 1095 | // next button 1096 | if (options.nextPrev) { 1097 | ul.appendChild(this.createItem("pager", next, options.nextText)); 1098 | } 1099 | 1100 | // first button 1101 | if (options.firstLast) { 1102 | ul.appendChild(this.createItem("pager", pages, options.lastText)); 1103 | } 1104 | 1105 | 1106 | } 1107 | return ul; 1108 | 1109 | } 1110 | 1111 | createItem(className, pageNum, content, ellipsis) { 1112 | let item = document.createElement("li"); 1113 | item.className = className; 1114 | item.innerHTML = !ellipsis ? '' + content + "" : '' + content + ""; 1115 | return item; 1116 | } 1117 | 1118 | isValidPage(page) { 1119 | return page > 0 && page <= this.getPages(); 1120 | } 1121 | 1122 | truncate() { 1123 | var that = this, 1124 | options = that.instance.config, 1125 | delta = options.pagerDelta * 2, 1126 | currentPage = that.instance.currentPage, 1127 | left = currentPage - options.pagerDelta, 1128 | right = currentPage + options.pagerDelta, 1129 | totalPages = this.getPages(), 1130 | range = [], 1131 | pager = [], 1132 | lastIndex; 1133 | 1134 | if (!this.instance.config.truncatePager) { 1135 | for (let i = 1; i <= this.getPages(); i++) { 1136 | pager.push(this.createItem(i === currentPage ? "active" : "", i, i)); 1137 | } 1138 | 1139 | } else { 1140 | if (currentPage < 4 - options.pagerDelta + delta) { 1141 | right = 3 + delta; 1142 | } else if (currentPage > this.getPages() - (3 - options.pagerDelta + delta)) { 1143 | left = this.getPages() - (2 + delta); 1144 | } 1145 | 1146 | // Get the links that will be visible 1147 | for (var i = 1; i <= totalPages; i++) { 1148 | if (i === 1 || i === totalPages || (i >= left && i <= right)) { 1149 | range.push(i); 1150 | } 1151 | } 1152 | 1153 | range.forEach(function (index) { 1154 | if (lastIndex) { 1155 | if (index - lastIndex == 2) { 1156 | pager.push(that.createItem("", lastIndex + 1, lastIndex + 1)); 1157 | } else if (index - lastIndex != 1) { 1158 | // Create ellipsis node 1159 | pager.push(that.createItem(options.classes.ellipsis, 0, options.ellipsisText, true)); 1160 | } 1161 | } 1162 | 1163 | pager.push(that.createItem(index == currentPage ? "active" : "", index, index)); 1164 | lastIndex = index; 1165 | }); 1166 | } 1167 | 1168 | return pager; 1169 | } 1170 | 1171 | } 1172 | 1173 | window.JSTable = JSTable; 1174 | -------------------------------------------------------------------------------- /src/jstable.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSTable v1.6.5 3 | */ 4 | 5 | .dt-container{ 6 | position:relative; 7 | display: block; 8 | width: 100%; 9 | overflow-x: auto; 10 | -webkit-overflow-scrolling: touch; 11 | -ms-overflow-style: -ms-autohiding-scrollbar; 12 | overflow-y: hidden; 13 | 14 | .dt-message { 15 | text-align: center; 16 | } 17 | 18 | .dt-loading{ 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | width: 100%; 23 | margin-left: -50%; 24 | margin-top: -20px; 25 | height: 40px; 26 | text-align: center; 27 | background-color: white; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | background: linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%); 32 | } 33 | } 34 | 35 | .dt-top, 36 | .dt-bottom { 37 | padding: 8px 10px; 38 | display:flex; 39 | justify-content:space-between; 40 | 41 | .dt-selector { 42 | padding: 6px; 43 | } 44 | .dt-info { 45 | margin: 7px 0; 46 | } 47 | 48 | } 49 | 50 | .dt-input { 51 | padding: 6px 12px; 52 | } 53 | 54 | 55 | /* PAGER */ 56 | .dt-pagination { 57 | 58 | ul { 59 | margin: 0; 60 | padding-left: 0; 61 | li { 62 | list-style: none; 63 | float: left; 64 | } 65 | } 66 | a, span{ 67 | border: 1px solid transparent; 68 | float: left; 69 | margin-left: 2px; 70 | padding: 6px 12px; 71 | position: relative; 72 | text-decoration: none; 73 | color: inherit; 74 | } 75 | 76 | a:hover { 77 | background-color: #d9d9d9; 78 | } 79 | .active a{ 80 | &, &:focus, &:hover{ 81 | background-color: #d9d9d9; 82 | cursor: default; 83 | } 84 | } 85 | 86 | .dt-ellipsis span{ 87 | cursor: not-allowed; 88 | } 89 | .disabled a{ 90 | &, &:focus, &:hover{ 91 | cursor: not-allowed; 92 | opacity: 0.4; 93 | } 94 | } 95 | 96 | .pager a { 97 | font-weight: bold; 98 | } 99 | } 100 | 101 | 102 | .dt-table { 103 | max-width: 100%; 104 | width: 100%; 105 | border-spacing: 0; 106 | 107 | & > tbody, > tfoot, > thead{ 108 | & > tr{ 109 | & > td, & > th{ 110 | vertical-align: top; 111 | padding: 8px 10px; 112 | white-space: nowrap; 113 | } 114 | } 115 | } 116 | 117 | & > thead > tr{ 118 | & > th, & > td{ 119 | vertical-align: bottom; 120 | text-align: left; 121 | border-bottom: 1px solid #d9d9d9; 122 | } 123 | } 124 | 125 | & > tfoot > tr{ 126 | & > th, & > td{ 127 | vertical-align: bottom; 128 | text-align: left; 129 | border-top: 1px solid #d9d9d9; 130 | } 131 | } 132 | 133 | th { 134 | vertical-align: bottom; 135 | text-align: left; 136 | 137 | &.dt-sorter { 138 | position: relative; 139 | cursor: pointer; 140 | padding-right:20px; 141 | 142 | &::before, 143 | &::after { 144 | content: ""; 145 | height: 0; 146 | width: 0; 147 | position: absolute; 148 | right: 7px; 149 | border-left: 4px solid transparent; 150 | border-right: 4px solid transparent; 151 | opacity: 0.2; 152 | } 153 | 154 | &::before { 155 | border-top: 4px solid #000; 156 | top: 18px; 157 | } 158 | 159 | &::after { 160 | border-bottom: 4px solid #000; 161 | border-top: 4px solid transparent; 162 | bottom: 22px; 163 | } 164 | 165 | &.asc::after, 166 | &.desc::before { 167 | opacity: 0.6; 168 | } 169 | 170 | } 171 | } 172 | 173 | } 174 | 175 | .hidden{ 176 | display:none!important; 177 | opacity:0!important; 178 | } 179 | --------------------------------------------------------------------------------