From 54eaf07e24acd37151f51c43c4efe6524fd64d57 Mon Sep 17 00:00:00 2001 From: eaCe Date: Fri, 28 Apr 2023 12:06:54 +0200 Subject: [PATCH] feat: adds searchable columns --- Makefile | 17 --------- dist/LittleBigTable.min.js | 9 +---- package.json | 17 +++++++-- src/LittleBigTable.js | 74 ++++++++++++++++++++++---------------- webpack.mix.js | 10 ++++++ 5 files changed, 69 insertions(+), 58 deletions(-) delete mode 100644 Makefile create mode 100644 webpack.mix.js diff --git a/Makefile b/Makefile deleted file mode 100644 index f0738db..0000000 --- a/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -# we have a build folder called dist, so mark it as phony to force update -.PHONY: dist - -dist: - # minify javascript - esbuild \ - --minify \ - --outfile=dist/littleBIGtable.min.js \ - src/littleBIGtable.js - # compress SVG sprite - svgo \ - -i src/resources/icons.svg \ - --disable=removeUselessDefs \ - --disable=removeTitle - # fetch latest tested version of AlpineJS - curl "https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.min.js" \ - -o dist/alpine.min.js diff --git a/dist/LittleBigTable.min.js b/dist/LittleBigTable.min.js index 5be77e8..63443b2 100644 --- a/dist/LittleBigTable.min.js +++ b/dist/LittleBigTable.min.js @@ -1,8 +1 @@ -function littleBIGtable(e){return{settings:{url:null,key_prefix:"lBt",limit:10,multisort:!1,args:{limit:"limit",offset:"offset",search:"search",sort:"sort"},messages:{loading:"Loading...",failed:"Loading failed",summary:"rows"},headers:{"Content-Type":"application/json","X-Requested-With":"littleBIGtable"},formatters:{},icons:"../dist/icons.svg"},meta:{loading:!1,status:null},params:{limit:10,offset:0,search:null,total:0},rows:[],sort:{},init:function(){this.params.limit=localStorage.getItem(this.settings.key_prefix+".limit"),(this.params.limit<10||this.params.limit>100)&&(this.params.limit=this.settings.limit);for(i in e)this.settings.hasOwnProperty(i)&&(this.settings[i]=e[i]);this.fetch()},fetch:function(){if(!this.settings.url){this.setStatus("Missing endpoint url, ensure you specify it in settings.");return}this.meta.loading=!0,this.setStatus(this.settings.messages.loading),fetch(this.settings.url+this.getUrlParams(),{headers:this.settings.headers}).then(t=>t.json()).then(t=>{this.rows=[],this.params.total=t.total;for(i in t.data)this.addRow(t.data[i])}).then(()=>{this.meta.loading=!1,this.setStatus(this.getSummary(this.settings.messages.summary))}).catch(t=>{console.error("Network fetch failed: ",t),this.setStatus(this.settings.messages.failed)})},addRow:function(t){let s={};for(i in t)typeof this.settings.formatters[i]=="function"?(fn=this.settings.formatters[i],s[i]=fn(t[i],t)):s[i]=t[i];for(i in this.settings.formatters)s.hasOwnProperty(i)||(fn=this.settings.formatters[i],s[i]=fn(t[i],t));this.rows.push(s)},getUrlParams:function(){let t="?"+this.settings.args.limit+"="+this.params.limit+"&"+this.settings.args.offset+"="+this.params.offset;this.params.search&&(t+="&"+this.settings.args.search+"="+this.params.search);let s=null;for(i in this.sort)s=i+":"+this.sort[i];return s&&(t+="&"+this.settings.args.sort+"="+s),t},getCurrentPage:function(){return this.params.offset==0?1:parseInt(parseInt(this.params.offset)/parseInt(this.params.limit)+1)},getTotalPages:function(){return parseInt(Math.ceil(parseInt(this.params.total)/parseInt(this.params.limit)))},getTotalRows:function(){return parseInt(this.params.total)},getFirstPageOffset:function(){return 0},getPrevPageOffset:function(){let t=parseInt(parseInt(this.getCurrentPage()-2)*parseInt(this.params.limit));return t<0?0:t},getNextPageOffset:function(){return parseInt(parseInt(this.getCurrentPage())*parseInt(this.params.limit))},getLastPageOffset:function(){let t=parseInt(parseInt(this.getTotalPages()-1)*parseInt(this.params.limit));return t<0?0:t},getOffsetForPage:function(){if(this.params.total=this.getPrevPageOffset()&&i<=this.getNextPageOffset())return parseInt(i)+parseInt(this.params.limit);return this.getLastPageOffset()},getFirstDisplayedRow:function(){return this.params.offset+1},getLastDisplayedRow:function(){let t=parseInt(this.params.offset)+parseInt(this.params.limit);return t>this.params.total&&(t=this.params.total),t},getSummary:function(t="rows",s="results"){return this.rows.length?t.toLowerCase()=="pages"?"Showing page "+this.getCurrentPage()+" of "+this.getTotalPages()+"":"Showing "+this.getFirstDisplayedRow()+" to "+this.getLastDisplayedRow()+" of "+this.getTotalRows()+" "+s:"No results"},getSortIcon:function(t){let s="none";return this.sort[t]!==void 0&&(s=this.sort[t]),''},setLimit:function(){(this.params.limit<10||this.params.limit>100)&&(this.params.limit=10),this.params.offset=this.getOffsetForPage(),localStorage.setItem(this.settings.key_prefix+".limit",this.params.limit),this.fetch()},setStatus:function(t){this.meta.status=t},toggleSortColumn:function(t){this.sort[t]==null?this.sort[t]="asc":this.sort[t]=="asc"?this.sort[t]="dsc":this.sort[t]=="dsc"&&delete this.sort[t]},goFirstPage:function(){this.params.offset=this.getFirstPageOffset(),this.fetch()},goLastPage:function(){this.params.offset=this.getLastPageOffset(),this.fetch()},goNextPage:function(){this.params.offset=this.getNextPageOffset(),this.fetch()},goPrevPage:function(){this.params.offset=this.getPrevPageOffset(),this.fetch()},goToPage:function(){},doSearch:function(){this.params.offset=0,this.fetch()},doSort:function(t){if(this.settings.multisort==!1){let s=this.sort[t];this.sort={},this.sort[t]=s}this.toggleSortColumn(t),this.fetch()},debug:function(){return`Params: -`+JSON.stringify(this.params)+` -Sort: -`+JSON.stringify(this.sort)+` -Meta: -`+JSON.stringify(this.meta)+` -Settings: -`+JSON.stringify(this.settings)}}} +window.littleBIGtable=function(t){return{settings:{url:null,key_prefix:"lBt",limit:10,multisort:!1,args:{limit:"limit",offset:"offset",search:"search",sort:"sort",columnSearch:"column_search"},messages:{loading:"Loading...",failed:"Loading failed",summary:"rows"},headers:{"Content-Type":"application/json","X-Requested-With":"littleBIGtable"},formatters:{},icons:"../dist/icons.svg"},meta:{loading:!1,status:null},params:{limit:10,offset:0,search:null,total:0},rows:[],sort:{},columnSearch:{},init:function(){for(i in this.params.limit=localStorage.getItem(this.settings.key_prefix+".limit"),(this.params.limit<10||this.params.limit>100)&&(this.params.limit=this.settings.limit),t)this.settings.hasOwnProperty(i)&&(this.settings[i]=t[i]);this.fetch()},fetch:function(t){function s(){return t.apply(this,arguments)}return s.toString=function(){return t.toString()},s}((function(){var t=this;this.settings.url?(this.meta.loading=!0,this.setStatus(this.settings.messages.loading),fetch(this.settings.url+this.getUrlParams(),{headers:this.settings.headers}).then((function(t){return t.json()})).then((function(s){for(i in t.rows=[],t.params.total=s.total,s.data)t.addRow(s.data[i])})).then((function(){t.meta.loading=!1,t.setStatus(t.getSummary(t.settings.messages.summary))})).catch((function(s){console.error("Network fetch failed: ",s),t.setStatus(t.settings.messages.failed)}))):this.setStatus("Missing endpoint url, ensure you specify it in settings.")})),addRow:function(t){var s={};for(i in t)"function"==typeof this.settings.formatters[i]?(fn=this.settings.formatters[i],s[i]=fn(t[i],t)):s[i]=t[i];for(i in this.settings.formatters)s.hasOwnProperty(i)||(fn=this.settings.formatters[i],s[i]=fn(t[i],t));this.rows.push(s)},getUrlParams:function(){var t=(this.settings.url.indexOf("?")>-1?"&":"?")+this.settings.args.limit+"="+this.params.limit+"&"+this.settings.args.offset+"="+this.params.offset;this.params.search&&(t+="&"+this.settings.args.search+"="+this.params.search);var s=null;for(i in this.sort)s=i+":"+this.sort[i];s&&(t+="&"+this.settings.args.sort+"="+s);var e=null;for(var a in this.columnSearch)e=a+":"+this.columnSearch[a],t+="&"+this.settings.args.columnSearch+"[]="+e;return t},getCurrentPage:function(){return 0==this.params.offset?1:parseInt(parseInt(this.params.offset)/parseInt(this.params.limit)+1)},getTotalPages:function(){return parseInt(Math.ceil(parseInt(this.params.total)/parseInt(this.params.limit)))},getTotalRows:function(){return parseInt(this.params.total)},getFirstPageOffset:function(){return 0},getPrevPageOffset:function(){var t=parseInt(parseInt(this.getCurrentPage()-2)*parseInt(this.params.limit));return t<0?0:t},getNextPageOffset:function(){return parseInt(parseInt(this.getCurrentPage())*parseInt(this.params.limit))},getLastPageOffset:function(){var t=parseInt(parseInt(this.getTotalPages()-1)*parseInt(this.params.limit));return t<0?0:t},getOffsetForPage:function(){if(this.params.total=this.getPrevPageOffset()&&i<=this.getNextPageOffset())return parseInt(i)+parseInt(this.params.limit);return this.getLastPageOffset()},getFirstDisplayedRow:function(){return this.params.offset+1},getLastDisplayedRow:function(){var t=parseInt(this.params.offset)+parseInt(this.params.limit);return t>this.params.total&&(t=this.params.total),t},getSummary:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"rows",s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"results";return this.rows.length?"pages"==t.toLowerCase()?"Showing page "+this.getCurrentPage()+" of "+this.getTotalPages()+"":"Showing "+this.getFirstDisplayedRow()+" to "+this.getLastDisplayedRow()+" of "+this.getTotalRows()+" "+s:"No results"},getSortIcon:function(t){var s="none";return void 0!==this.sort[t]&&(s=this.sort[t]),''},setLimit:function(){(this.params.limit<10||this.params.limit>100)&&(this.params.limit=10),this.params.offset=this.getOffsetForPage(),localStorage.setItem(this.settings.key_prefix+".limit",this.params.limit),this.fetch()},setStatus:function(t){this.meta.status=t},toggleSortColumn:function(t){null==this.sort[t]?this.sort[t]="asc":"asc"==this.sort[t]?this.sort[t]="dsc":"dsc"==this.sort[t]&&delete this.sort[t]},goFirstPage:function(){this.params.offset=this.getFirstPageOffset(),this.fetch()},goLastPage:function(){this.params.offset=this.getLastPageOffset(),this.fetch()},goNextPage:function(){this.params.offset=this.getNextPageOffset(),this.fetch()},goPrevPage:function(){this.params.offset=this.getPrevPageOffset(),this.fetch()},goToPage:function(){},doSearch:function(){this.params.offset=0,this.fetch()},doColumnSearch:function(t){this.columnSearch[t.name]=t.value,this.fetch()},doSort:function(t){if(0==this.settings.multisort){var s=this.sort[t];this.sort={},this.sort[t]=s}this.toggleSortColumn(t),this.fetch()},debug:function(){return"Params:\n"+JSON.stringify(this.params)+"\nSort:\n"+JSON.stringify(this.sort)+"\nMeta:\n"+JSON.stringify(this.meta)+"\nSettings:\n"+JSON.stringify(this.settings)}}}; \ No newline at end of file diff --git a/package.json b/package.json index ff1c611..7ca97f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "littlebigtable", - "version": "0.1.1", + "version": "0.1.2", "description": "A lightweight (~5k gzipped) interactive HTML table built using AlpineJS", "license": "MIT", "author": { @@ -30,10 +30,21 @@ "alpinejs", "lbt" ], + "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "mix watch", + "build": "mix", + "build:production": "mix --production" + }, + "devDependencies": { + "babel-eslint": "^10.1.0", + "browser-sync": "^2.29.1", + "browser-sync-webpack-plugin": "2.3.0", + "eslint": "^8.38.0", + "eslint-plugin-import": "^2.27.5", + "laravel-mix": "^6.0.49" }, "dependencies": { - "alpinejs": "2.8.0" + "alpinejs": "^3.12.0" } } diff --git a/src/LittleBigTable.js b/src/LittleBigTable.js index f75a379..00eadef 100644 --- a/src/LittleBigTable.js +++ b/src/LittleBigTable.js @@ -1,4 +1,4 @@ -function littleBIGtable(settings) { +window.littleBIGtable = (settings) => { return { // settings for the developer to override settings: { @@ -10,7 +10,8 @@ function littleBIGtable(settings) { limit: 'limit', offset: 'offset', search: 'search', - sort: 'sort' + sort: 'sort', + columnSearch: 'column_search' }, messages: { loading: 'Loading...', @@ -40,8 +41,9 @@ function littleBIGtable(settings) { rows: [], // stores the column(s) sorting state sort: {}, + columnSearch: {}, // initial setup before interaction - init: function() { + init() { // set preferences from localStorage this.params.limit = localStorage.getItem(this.settings.key_prefix + '.limit'); if (this.params.limit < 10 || this.params.limit > 100) { @@ -57,7 +59,7 @@ function littleBIGtable(settings) { this.fetch(); }, // fetch and populate data using current state - fetch: function() { + fetch() { if ( ! this.settings.url) { this.setStatus('Missing endpoint url, ensure you specify it in settings.'); return; @@ -82,7 +84,7 @@ function littleBIGtable(settings) { }); }, // adds the data row to the table - addRow: function(data) { + addRow(data) { // todo check for field formatter by name let row = {}; for (i in data) { @@ -103,8 +105,9 @@ function littleBIGtable(settings) { this.rows.push(row); }, // returns the url params for the GET request - getUrlParams: function() { - let str = '?'+this.settings.args.limit+'='+this.params.limit+'&'+this.settings.args.offset+'='+this.params.offset; + getUrlParams() { + const delimiter = (this.settings.url.indexOf('?') > -1) ? '&' : '?'; + let str = delimiter+this.settings.args.limit+'='+this.params.limit+'&'+this.settings.args.offset+'='+this.params.offset; if (this.params.search) { str+= '&'+this.settings.args.search+'='+this.params.search; } @@ -117,44 +120,51 @@ function littleBIGtable(settings) { str+= '&'+this.settings.args.sort+'='+sort; } + let columnSearch = null; + // loop through columnSearch object + for (const key in this.columnSearch) { + columnSearch = key + ':' + this.columnSearch[key]; + str+= '&'+this.settings.args.columnSearch+'[]='+columnSearch; + } + return str; }, // returns the current page number - getCurrentPage: function() { + getCurrentPage() { if (this.params.offset == 0) { return 1; } return parseInt(parseInt(this.params.offset) / parseInt(this.params.limit) + 1); }, // returns the total number of pages in the data set (on the server, requires total to be passed in result) - getTotalPages: function() { + getTotalPages() { return parseInt(Math.ceil(parseInt(this.params.total) / parseInt(this.params.limit))); }, // returns the total number of rows of data on the server - getTotalRows: function() { + getTotalRows() { return parseInt(this.params.total); }, // returns the offset of the first row - getFirstPageOffset: function() { + getFirstPageOffset() { return 0; }, // returns the offset of the first row on the previous page - getPrevPageOffset: function() { + getPrevPageOffset() { let int = parseInt(parseInt(this.getCurrentPage() - 2) * parseInt(this.params.limit)); return (int < 0) ? 0 : int; }, // returns the offset of the first row on the next page - getNextPageOffset: function() { + getNextPageOffset() { let int = parseInt(parseInt(this.getCurrentPage()) * parseInt(this.params.limit)); return int; }, // returns the offset of the first row on the last page - getLastPageOffset: function() { + getLastPageOffset() { let int = parseInt(parseInt(this.getTotalPages() - 1) * parseInt(this.params.limit)); return (int < 0) ? 0 : int; }, // returns the offset for a particular page, (this may be slightly off depending on the limit chosen) - getOffsetForPage: function() { + getOffsetForPage() { // determine correct offset boundary for the current page // loop through pages, if (offset between prev and next) recalculate if (this.params.total < this.params.limit) { @@ -168,11 +178,11 @@ function littleBIGtable(settings) { return this.getLastPageOffset(); }, // returns the index of first row on the page - getFirstDisplayedRow: function() { + getFirstDisplayedRow() { return this.params.offset + 1; }, // returns the index of last row on the page - getLastDisplayedRow: function() { + getLastDisplayedRow() { let int = parseInt(this.params.offset) + parseInt(this.params.limit); if (int > this.params.total) { int = this.params.total; @@ -180,7 +190,7 @@ function littleBIGtable(settings) { return int; }, // returns a status summary, either number of rows or number of pages - getSummary: function(type='rows', name='results') { + getSummary(type='rows', name='results') { if ( ! this.rows.length) { return 'No results'; } @@ -190,7 +200,7 @@ function littleBIGtable(settings) { return 'Showing ' + this.getFirstDisplayedRow() + ' to ' + this.getLastDisplayedRow() + ' of ' + this.getTotalRows() + ' ' + name; }, // returns the required icon for the sort state - getSortIcon: function(col) { + getSortIcon(col) { let icon = 'none'; if (undefined !== this.sort[col]) { icon = this.sort[col]; @@ -199,7 +209,7 @@ function littleBIGtable(settings) { }, // set the number of rows to show per page and saves preference in localStorage // tries to keep the current rows on the page - setLimit: function() { + setLimit() { // sanity check input if (this.params.limit < 10 || this.params.limit > 100) { this.params.limit = 10; @@ -213,11 +223,11 @@ function littleBIGtable(settings) { this.fetch(); }, // sets the statusbar text - setStatus: function(str) { + setStatus(str) { this.meta.status = str; }, // toggle the sort state between 'null', 'asc' and 'dsc' - toggleSortColumn: function(col) { + toggleSortColumn(col) { if (undefined == this.sort[col]) { this.sort[col] = 'asc'; } else if (this.sort[col] == 'asc') { @@ -227,35 +237,39 @@ function littleBIGtable(settings) { } }, // sets the offset to the first page and fetches the data - goFirstPage: function() { + goFirstPage() { this.params.offset = this.getFirstPageOffset(); this.fetch(); }, // sets the offset to the top of the last page and fetches the data - goLastPage: function() { + goLastPage() { this.params.offset = this.getLastPageOffset(); this.fetch(); }, // sets the offset to the top of the next page and fetches the data - goNextPage: function() { + goNextPage() { this.params.offset = this.getNextPageOffset(); this.fetch(); }, // sets the offset to the top of the previous page and fetches the data - goPrevPage: function() { + goPrevPage() { this.params.offset = this.getPrevPageOffset(); this.fetch(); }, // todo jump to a particular page by number - goToPage: function() { + goToPage() { }, // handle the user search input, always returning to the start of the results - doSearch: function() { + doSearch() { this.params.offset = 0; this.fetch(); }, + doColumnSearch($el) { + this.columnSearch[$el.name] = $el.value; + this.fetch(); + }, // handle the column sort - doSort: function(col) { + doSort(col) { if (false == this.settings.multisort) { let state = this.sort[col]; this.sort = {}; @@ -264,7 +278,7 @@ function littleBIGtable(settings) { this.toggleSortColumn(col); this.fetch(); }, - debug: function() { + debug() { return "Params:\n"+JSON.stringify(this.params)+"\nSort:\n"+JSON.stringify(this.sort)+"\nMeta:\n"+JSON.stringify(this.meta)+"\nSettings:\n"+JSON.stringify(this.settings); } } diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 0000000..adfcfbd --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,10 @@ +const mix = require('laravel-mix'); +mix.webpackConfig({ + optimization: { + providedExports: false, + sideEffects: false, + usedExports: false + } +}); +mix.js('src/LittleBigTable.js', 'dist/LittleBigTable.min.js') +mix.disableNotifications(); \ No newline at end of file