├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── testing.yml ├── .gitignore ├── .nojekyll ├── README.md ├── demo ├── index.html ├── styles │ └── main.css └── vendors │ └── highlight │ ├── highlight.pack.js │ └── styles │ └── github.css ├── package-lock.json ├── package.json ├── screenshots └── example-1.png ├── src ├── index.js └── skins │ └── default.css └── test ├── dummies ├── 2-rows.js └── 3-rows.js └── specs └── test.index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | test/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'piecioshka', 3 | 4 | // https://eslint.org/docs/user-guide/configuring#specifying-environments 5 | env: { 6 | es6: true, 7 | browser: true, 8 | node: true, 9 | commonjs: true, 10 | amd: true 11 | // jquery: true, 12 | // jasmine: true 13 | }, 14 | 15 | // https://eslint.org/docs/rules/ 16 | rules: { 17 | 'no-nested-ternary': 'off', 18 | strict: 'off', 19 | 'object-curly-newline': 'off', 20 | 'no-magic-numbers': ['error', { 21 | ignore: [-1, 0, 1, 3] 22 | }], 23 | 'arrow-parens': 'off', 24 | 'valid-jsdoc': 'off' 25 | }, 26 | 27 | // List of global variables. 28 | globals: {} 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Testing 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x, 18.x, 20.x, 22.x] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: "npm" 29 | 30 | - run: npm ci 31 | - run: npm run build --if-present 32 | - run: npm run lint --if-present 33 | - run: npm test 34 | - run: npm run e2e --if-present 35 | env: 36 | CI: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | 4 | .nyc_output/ 5 | coverage/ 6 | coverage.lcov 7 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piecioshka/simple-data-table/070f9e2d28ff9d931f606c4aa161a0e57b8087aa/.nojekyll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-data-table 2 | 3 | [![node version](https://img.shields.io/node/v/simple-data-table.svg)](https://www.npmjs.com/package/simple-data-table) 4 | [![npm version](https://badge.fury.io/js/simple-data-table.svg)](https://badge.fury.io/js/simple-data-table) 5 | [![downloads count](https://img.shields.io/npm/dt/simple-data-table.svg)](https://www.npmjs.com/package/simple-data-table) 6 | [![size](https://packagephobia.com/badge?p=simple-data-table)](https://packagephobia.com/result?p=simple-data-table) 7 | [![license](https://img.shields.io/npm/l/simple-data-table.svg)](https://piecioshka.mit-license.org) 8 | [![github-ci](https://github.com/piecioshka/simple-data-table/actions/workflows/testing.yml/badge.svg)](https://github.com/piecioshka/simple-data-table/actions/workflows/testing.yml) 9 | 10 | 🔨 Lightweight and simple data table with no dependencies 11 | 12 | ## Features 13 | 14 | - ✅ Display any data (array with objects) in simple table layout 15 | - ✅ Support custom skins _(style children of `div.simple-data-table`)_ 16 | - ✅ Small size of package 17 | - ✅ No dependencies 18 | - ✅ Support custom events (`on`, `emit`) 19 | - Updated cell content 20 | - Row removed 21 | - Row added 22 | - Sorted table 23 | - ✅ Fluent API _(not available in all public methods)_ 24 | - ✅ API 25 | - Lazy loading of data (`load()`) 26 | - Read number of rows (`getRowsCount()`) 27 | - Get content from concrete cell (`getCell`) 28 | - Find cells which contains concrete text (`findCellsByContent()`) 29 | - Highlight cells (`highlightCell`, `clearHighlightedCells()`) 30 | - Support put value into single cell (`setInputCellContent()`) 31 | - Sorting by a concrete cell with a given function (`sortByColumn()` & `setSortComparingFn`) 32 | - Define headers, as a first row (`setHeaders()`) 33 | - ✅ Readonly Mode 34 | 35 | ## Usage 36 | 37 | Installation: 38 | 39 | ```bash 40 | npm install simple-data-table 41 | ``` 42 | 43 | ```html 44 | 45 | 46 | ``` 47 | 48 | ```javascript 49 | const $container = document.querySelector('#place-to-render'); 50 | const options = { /* all available options are described below */ }; 51 | const t = new SimpleDataTable($container, options); 52 | t.load([ 53 | { 54 | column1: 'Cell 1', 55 | column2: 'Cell 2', 56 | column3: 'Cell 3' 57 | }, 58 | { 59 | column1: 'Cell 4', 60 | column2: 'Cell 5', 61 | column3: 'Cell 6' 62 | }, 63 | { 64 | column1: 'Cell 7', 65 | column2: 'Cell 8', 66 | column3: 'Cell 9' 67 | }, 68 | { 69 | column1: 'Cell 10', 70 | column2: 'Cell 11', 71 | column3: 'Cell 12' 72 | } 73 | ]); 74 | t.render(); 75 | ``` 76 | 77 | ## Preview 🎉 78 | 79 | 80 | 81 | ![](./screenshots/example-1.png) 82 | 83 | ## Options 84 | 85 | #### `addButtonLabel` _(Default: '✚')_ 86 | 87 | Change value od button which add new row. 88 | 89 | ```js 90 | const t = new SimpleDataTable($container, { 91 | addButtonLabel: 'New record' 92 | }); 93 | t.load(...); 94 | t.render(); 95 | ``` 96 | 97 | #### `defaultColumnPrefix` _(Default: 'column')_ 98 | 99 | Define what "name" should have cells in new added columns. 100 | 101 | ```js 102 | const t = new SimpleDataTable($container, { 103 | defaultColumnPrefix: 'random' 104 | }); 105 | t.load(...); 106 | t.render(); 107 | ``` 108 | 109 | #### `defaultColumnNumber` _(Default: null)_ 110 | 111 | Define how much columns should contain row in empty table. 112 | 113 | By default, use the size of headers or the number of cells in the first row. 114 | 115 | ```js 116 | const t = new SimpleDataTable($container, { 117 | defaultColumnNumber: '7' 118 | }); 119 | t.load(...); 120 | t.render(); 121 | ``` 122 | 123 | #### `defaultHighlightedCellClass` _(Default: 'highlighted-cell')_ 124 | 125 | Define class of highlighted cell. 126 | 127 | ```js 128 | const t = new SimpleDataTable($container, { 129 | defaultHighlightedCellClass: 'my-highlight' 130 | }); 131 | t.load(...); 132 | t.render(); 133 | ``` 134 | 135 | #### `readonly` _(Default: false)_ 136 | 137 | Define class of highlighted cell. 138 | 139 | ```js 140 | const t = new SimpleDataTable($container, { 141 | readonly: true 142 | }); 143 | t.load(...); 144 | t.render(); 145 | ``` 146 | 147 | ## API 148 | 149 | #### `render(): SimpleDataTable` 150 | 151 | Render table into DOM. 152 | 153 | #### `getRowsCount(): number` 154 | 155 | Get number of rows. 156 | 157 | #### `findCellsByContent( ...content: Array ): Array<{ rowIndex: number, cellIndex: number }>` 158 | 159 | Get list of cell positions which contains passed strings. 160 | 161 | #### `getCell( rowIndex: number , cellIndex: number ): HTMLElement || null` 162 | 163 | Get DOM reference of concrete cell. 164 | 165 | #### `highlightCell( rowIndex: number, cellIndex: number )` 166 | 167 | Add class to concrete cell. 168 | 169 | #### `clearHighlightedCells()` 170 | 171 | Remove CSS class of all highlighted cells. 172 | 173 | #### `setInputCellContent( rowIndex: number, cellIndex: number, content: string )` 174 | 175 | Put content into input in concrete cell. 176 | 177 | #### `setHeaders( items: Array )` 178 | 179 | Setup column headers. Sorting is enabled by default. 180 | 181 | #### `load( data: Array )` 182 | 183 | Loading data into table component. 184 | 185 | #### `emit( name: string, payload: any )` 186 | 187 | Trigger event on SimpleDataTable instance. 188 | 189 | #### `on( name: string, handler: Function )` 190 | 191 | Listen on events. 192 | 193 | #### `sortByColumn( columnIndex: number )` 194 | 195 | Sorts data and triggers `DATA_SORTED` event. 196 | 197 | **WARNING**: Function `sortByColumn()` runs `render()` under the hood. 198 | 199 | #### `setSortComparingFn( fn: (val1, val2) => 0, 1, -1 )` 200 | 201 | Set `_sortComparingFn()` which by default use [`String.prototype.localeCompare`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). 202 | 203 | ## Events 204 | 205 | #### `SimpleDataTable.EVENTS.UPDATE` 206 | 207 | Event is dispatching when you change any of input in table. 208 | 209 | ```js 210 | const t = new SimpleDataTable($container); 211 | t.on(SimpleDataTable.EVENTS.UPDATE, (data) => { 212 | // do some stuff with the updated data... 213 | }); 214 | ``` 215 | 216 | #### `SimpleDataTable.EVENTS.ROW_ADDED` 217 | 218 | Event is dispatching when you add new record. 219 | 220 | ```js 221 | const t = new SimpleDataTable($container); 222 | t.on(SimpleDataTable.EVENTS.ROW_ADDED, () => { 223 | // do some stuff... 224 | }); 225 | ``` 226 | 227 | #### `SimpleDataTable.EVENTS.ROW_REMOVED` 228 | 229 | Event is dispatching when you remove any record. 230 | 231 | ```js 232 | const t = new SimpleDataTable($container); 233 | t.on(SimpleDataTable.EVENTS.ROW_REMOVED, () => { 234 | // do some stuff... 235 | }); 236 | ``` 237 | 238 | #### `SimpleDataTable.EVENTS.DATA_SORTED` 239 | 240 | Event is dispatching after data is sorted with `sortByColumn` function. 241 | 242 | ```js 243 | const t = new SimpleDataTable($container); 244 | t.on(SimpleDataTable.EVENTS.DATA_SORTED, () => { 245 | // do some stuff... 246 | }); 247 | ``` 248 | 249 | ## Static 250 | 251 | #### `SimpleDataTable.clearElement( $element: HTMLElement )` 252 | 253 | Recursive remove children from passed HTMLElement. 254 | 255 | ## Tested under browsers 256 | 257 | - Safari v10.1.2 258 | - Firefox v61.0.1 259 | - Chrome v67.0.3396.99 260 | - Opera v51.0.2830.40 261 | 262 | ## License 263 | 264 | [The MIT License](https://piecioshka.mit-license.org) @ 2018 265 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | simple-data-table 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

SimpleDataTable

23 | 24 |
25 |

Example 1: Minimal

26 |
27 | 34 |

 35 | const t = new SimpleDataTable(document.querySelector('#example-1'));
 36 | t.load(...);
 37 | t.render();
 38 |     
39 |
40 | 41 |
42 |

Example 2: Change label of add button

43 |
44 | 53 |

 54 | const t = new SimpleDataTable(document.querySelector('#example-2'), {
 55 |     addButtonLabel: 'Add'
 56 | });
 57 | t.load(...);
 58 | t.render();
 59 |     
60 |
61 | 62 |
63 |

Example 3: Change prefix of default column (use DevTools to watch)

64 |
65 | 76 |

 77 | const t = new SimpleDataTable(document.querySelector('#example-3'), {
 78 |     defaultColumnPrefix: 'col-'
 79 | });
 80 | t.render();
 81 |     
82 |
83 | 84 |
85 |

Example 4: Change default number of columns

86 |
87 | 98 |

 99 | const t = new SimpleDataTable(document.querySelector('#example-4'), {
100 |     defaultColumnNumber: 1
101 | });
102 | t.render();
103 |     
104 |
105 | 106 |
107 |

Example 5: Readonly Mode

108 |
109 | 118 |

119 | const t = new SimpleDataTable(document.querySelector('#example-5'), {
120 |     readonly: true
121 | });
122 | t.render();
123 |     
124 |
125 | 126 |
127 |

Example 6: Headers + Sorting

128 |
129 | 138 |

139 | const t = new SimpleDataTable(document.querySelector('#example-6'));
140 | t.setHeaders(['Value', 'Color', 'Name']);
141 | t.load(...);
142 | t.render();
143 | t.sortByColumn(0);
144 |     
145 |
146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /demo/styles/main.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | width: 620px; 7 | margin: 0 auto; 8 | font-weight: normal; 9 | font-family: 'Overpass', sans-serif; 10 | } 11 | 12 | h1 { 13 | font-family: 'Chivo', sans-serif; 14 | border-bottom: 1px solid; 15 | } 16 | 17 | h2 > em { 18 | background: #000; 19 | color: #fff; 20 | font-style: normal; 21 | padding: 3px; 22 | } 23 | 24 | section { 25 | margin: 0 0 100px 0; 26 | } 27 | -------------------------------------------------------------------------------- /demo/vendors/highlight/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("https://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("https://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}}); 3 | -------------------------------------------------------------------------------- /demo/vendors/highlight/styles/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-data-table", 3 | "description": "🔨 Lightweight and simple data table with no dependencies", 4 | "version": "1.3.7", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Piotr Kowalski", 8 | "email": "piecioshka@gmail.com", 9 | "url": "https://piecioshka.pl/" 10 | }, 11 | "scripts": { 12 | "clear": "rm -rf coverage/ .nyc_output/ coverage.lcov", 13 | "clear:all": "rm -rf node_modules/ && npm run clear", 14 | "test": "ava test/specs/", 15 | "coverage": "nyc npm run test && nyc report --reporter=html", 16 | "lint": "eslint ." 17 | }, 18 | "devDependencies": { 19 | "ava": "^5.3.1", 20 | "eslint": "^8.51.0", 21 | "eslint-config-piecioshka": "^2.2.4", 22 | "jsdom": "^22.1.0", 23 | "nyc": "^15.1.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+ssh://git@github.com/piecioshka/simple-data-table.git" 28 | }, 29 | "files": [ 30 | "src", 31 | "package.json", 32 | "README.md" 33 | ], 34 | "keywords": [ 35 | "simple", 36 | "customizable", 37 | "configurable", 38 | "powerful", 39 | "data", 40 | "table", 41 | "display", 42 | "gui", 43 | "browser", 44 | "lightweight", 45 | "no dependencies" 46 | ], 47 | "main": "./src/index.js" 48 | } 49 | -------------------------------------------------------------------------------- /screenshots/example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piecioshka/simple-data-table/070f9e2d28ff9d931f606c4aa161a0e57b8087aa/screenshots/example-1.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | class SimpleDataTable { 2 | constructor($el, options = {}) { 3 | this.$el = $el; 4 | this.addButtonLabel = options.addButtonLabel || '✚'; 5 | this.readonly = options.readonly || false; 6 | this.defaultColumnPrefix = options.defaultColumnPrefix || 'column'; 7 | this.defaultColumnNumber = options.defaultColumnNumber || null; 8 | this.defaultHighlightedCellClass = options.defaultHighlightedCellClass || 'highlighted-cell'; 9 | this._headers = []; 10 | this.data = []; 11 | this._events = {}; 12 | this._sorted = { 13 | columnIndex: -1, 14 | descending: false, 15 | }; 16 | this._sortComparingFn = (a, b) => a.toString() 17 | .localeCompare(b.toString()); 18 | } 19 | 20 | _renderTHead($table) { 21 | const $thead = document.createElement('thead'); 22 | const $row = document.createElement('tr'); 23 | 24 | this._headers.forEach((name, index) => { 25 | const $cell = this._createEmptyHeaderCell(); 26 | let label = name; 27 | if (this._sorted.columnIndex === index) { 28 | label += ` ${this._sorted.descending ? '\u2191' : '\u2193'}`; 29 | } 30 | $cell.textContent = label; 31 | $cell.addEventListener('click', () => { 32 | this._sorted.descending = (this._sorted.columnIndex === index) 33 | && !this._sorted.descending; 34 | this.sortByColumn(index); 35 | }); 36 | $row.appendChild($cell); 37 | }); 38 | 39 | $thead.appendChild($row); 40 | $table.appendChild($thead); 41 | } 42 | 43 | _renderTBody($table) { 44 | const $tbody = document.createElement('tbody'); 45 | 46 | this.data.forEach((item, rowIndex) => { 47 | const $row = document.createElement('tr'); 48 | 49 | Object.entries(item).forEach(([key, value]) => { 50 | const $cell = this._createCellWithInput(key, value, rowIndex); 51 | $row.appendChild($cell); 52 | }); 53 | 54 | const $cell = this.readonly 55 | ? this._createEmptyCell() 56 | : this._createCellWithRemoveRowButton(); 57 | $row.appendChild($cell); 58 | 59 | $tbody.appendChild($row); 60 | }); 61 | 62 | $table.appendChild($tbody); 63 | } 64 | 65 | render() { 66 | if (!this.$el) { 67 | throw new Error('this.$el is not defined'); 68 | } 69 | 70 | SimpleDataTable.clearElement(this.$el); 71 | 72 | const $wrapper = document.createElement('div'); 73 | $wrapper.classList.add('simple-data-table'); 74 | const $table = document.createElement('table'); 75 | 76 | if (this._headers.length > 0) { 77 | this._renderTHead($table); 78 | } 79 | 80 | this._renderTBody($table); 81 | 82 | $wrapper.appendChild($table); 83 | 84 | if (!this.readonly) { 85 | const $addButton = this._createAddButton(); 86 | $wrapper.appendChild($addButton); 87 | } 88 | 89 | this.$el.appendChild($wrapper); 90 | 91 | return this; 92 | } 93 | 94 | getRowsCount() { 95 | return this.$el.querySelectorAll('tr').length; 96 | } 97 | 98 | findCellsByContent(...content) { 99 | const indexes = []; 100 | const $rows = this.$el.querySelectorAll('tr'); 101 | 102 | $rows.forEach((row, rowIndex) => { 103 | const cells = row.querySelectorAll('td'); 104 | 105 | cells.forEach((cell, cellIndex) => { 106 | const $cellInput = cell.querySelector('input'); 107 | const cellContent = $cellInput 108 | ? $cellInput.value 109 | : cell.textContent; 110 | 111 | content.forEach((item) => { 112 | if (cellContent === item) { 113 | indexes.push({ 114 | rowIndex, 115 | cellIndex, 116 | }); 117 | } 118 | }); 119 | }); 120 | }); 121 | return indexes; 122 | } 123 | 124 | getCell(rowIndex, cellIndex) { 125 | const $rows = this.$el.querySelectorAll('tr'); 126 | const $row = $rows[rowIndex]; 127 | 128 | if (!$row) { 129 | return null; 130 | } 131 | 132 | const $cells = $row.querySelectorAll('td'); 133 | const $cell = $cells[cellIndex]; 134 | 135 | if (!$cell) { 136 | return null; 137 | } 138 | 139 | return $cell; 140 | } 141 | 142 | highlightCell(rowIndex, cellIndex) { 143 | const $cell = this.getCell(rowIndex, cellIndex); 144 | $cell.classList.add(this.defaultHighlightedCellClass); 145 | } 146 | 147 | clearHighlightedCells() { 148 | const $cells = this.$el.querySelectorAll('td'); 149 | $cells.forEach(($cell) => { 150 | $cell.classList.remove(this.defaultHighlightedCellClass); 151 | }); 152 | } 153 | 154 | setInputCellContent(rowIndex, cellIndex, content) { 155 | const $cell = this.getCell(rowIndex, cellIndex); 156 | const $input = $cell.querySelector('input'); 157 | $input.value = content; 158 | } 159 | 160 | _createEmptyCell() { 161 | return document.createElement('td'); 162 | } 163 | 164 | _createEmptyHeaderCell() { 165 | return document.createElement('th'); 166 | } 167 | 168 | _createCellWithRemoveRowButton() { 169 | const $cell = this._createEmptyCell(); 170 | const $removeButton = document.createElement('button'); 171 | $removeButton.classList.add('remove-row'); 172 | $removeButton.textContent = '✖︎'; 173 | $removeButton.addEventListener('click', () => { 174 | const $tr = $cell.parentNode; 175 | this._removeRow($tr); 176 | }); 177 | $cell.appendChild($removeButton); 178 | return $cell; 179 | } 180 | 181 | _removeRow($tr) { 182 | const $siblings = Array.from($tr.parentNode.children); 183 | const index = $siblings.indexOf($tr); 184 | this.data.splice(index, 1); 185 | $tr.remove(); 186 | this.emit(SimpleDataTable.EVENTS.ROW_REMOVED, this.data); 187 | } 188 | 189 | _createAddButton() { 190 | const $addButton = document.createElement('button'); 191 | $addButton.classList.add('add-row'); 192 | $addButton.textContent = this.addButtonLabel; 193 | $addButton.addEventListener('click', this._createEmptyRow.bind(this)); 194 | return $addButton; 195 | } 196 | 197 | _createCellWithInput(name, value, rowIndex) { 198 | const $cell = document.createElement('td'); 199 | const $input = document.createElement('input'); 200 | $input.value = value; 201 | $input.name = name; 202 | 203 | if (this.readonly) { 204 | $input.disabled = true; 205 | } 206 | 207 | $input.addEventListener('change', () => { 208 | this.data[rowIndex][name] = $input.value; 209 | this.emit(SimpleDataTable.EVENTS.UPDATE, this.data); 210 | }); 211 | 212 | $cell.appendChild($input); 213 | return $cell; 214 | } 215 | 216 | _createEmptyRow() { 217 | const $tbody = this.$el.querySelector('tbody'); 218 | const rowsCount = $tbody.querySelectorAll('tr').length; 219 | const $row = document.createElement('tr'); 220 | const columnNames = this._fetchColumnNames(); 221 | 222 | const record = {}; 223 | 224 | columnNames.forEach((cellName) => { 225 | const $cell = this._createCellWithInput(cellName, '', rowsCount); 226 | $row.appendChild($cell); 227 | record[cellName] = ''; 228 | }); 229 | 230 | this.data.push(record); 231 | 232 | $row.appendChild(this._createCellWithRemoveRowButton()); 233 | $tbody.appendChild($row); 234 | 235 | this.emit(SimpleDataTable.EVENTS.ROW_ADDED); 236 | } 237 | 238 | _fetchColumnNames() { 239 | const $tbody = this.$el.querySelector('tbody'); 240 | const $firstRecord = $tbody.querySelector('tr'); 241 | 242 | if (!$firstRecord) { 243 | const size = this.defaultColumnNumber 244 | ? this.defaultColumnNumber 245 | : this._headers 246 | ? this._headers.length 247 | : this.data[0] && this.data[0].length; 248 | if (!size) { 249 | return []; 250 | } 251 | return Array(size) 252 | .fill(this.defaultColumnPrefix) 253 | .map((name, index) => `${name}${index + 1}`); 254 | } 255 | 256 | const $elements = Array.from($firstRecord.children); 257 | return $elements 258 | .map(($cell) => $cell.querySelector('input')) 259 | .filter(($element) => $element) 260 | .map(($input) => $input.name); 261 | } 262 | 263 | /** 264 | * @param {string[]} items 265 | * @returns 266 | */ 267 | setHeaders(items) { 268 | this._headers = items; 269 | return this; 270 | } 271 | 272 | load(data) { 273 | this.data = Array.from(data); 274 | return this; 275 | } 276 | 277 | emit(name, payload) { 278 | if (!this._events[name]) { 279 | return; 280 | } 281 | 282 | this._events[name].forEach((cb) => cb(payload)); 283 | return this; 284 | } 285 | 286 | on(name, handler) { 287 | if (!this._events[name]) { 288 | this._events[name] = []; 289 | } 290 | 291 | this._events[name].push(handler); 292 | return this; 293 | } 294 | 295 | sortByColumn(index) { 296 | this._sorted.columnIndex = index; 297 | const order = this._sorted.descending ? 1 : -1; 298 | this.data.sort((firstRow, secondRow) => 299 | this._sortComparingFn( 300 | Object.values(firstRow)[index], 301 | Object.values(secondRow)[index] 302 | ) * order 303 | ); 304 | this.render(); 305 | this.emit(SimpleDataTable.EVENTS.DATA_SORTED); 306 | } 307 | 308 | setSortComparingFn(fn) { 309 | this._sortComparingFn = fn; 310 | } 311 | 312 | static clearElement($element) { 313 | while ($element.firstElementChild) { 314 | $element.firstElementChild.remove(); 315 | } 316 | } 317 | } 318 | 319 | SimpleDataTable.EVENTS = { 320 | UPDATE: 'SimpleDataTable.EVENTS.UPDATE', 321 | ROW_ADDED: 'SimpleDataTable.EVENTS.ROW_ADDED', 322 | ROW_REMOVED: 'SimpleDataTable.EVENTS.ROW_REMOVED', 323 | DATA_SORTED: 'SimpleDataTable.EVENTS.DATA_SORTED', 324 | }; 325 | 326 | // Exports 327 | if (typeof module === 'object' && module.exports) { 328 | module.exports = { SimpleDataTable }; 329 | } else if (typeof define === 'function' && define.amd) { 330 | define(() => ({ SimpleDataTable })); 331 | } else { 332 | window.SimpleDataTable = SimpleDataTable; 333 | } 334 | -------------------------------------------------------------------------------- /src/skins/default.css: -------------------------------------------------------------------------------- 1 | .simple-data-table th { 2 | border: none; 3 | background-color: #d7d7d7; 4 | padding: 0; 5 | } 6 | 7 | .simple-data-table td { 8 | border: none; 9 | padding: 0; 10 | } 11 | 12 | .simple-data-table input { 13 | border: 1px solid #d7d7d7; 14 | display: inline-block; 15 | padding: 5px; 16 | } 17 | 18 | .simple-data-table input:focus { 19 | border: 1px solid #3498db; 20 | } 21 | 22 | .simple-data-table button.remove-row { 23 | background: transparent; 24 | color: #de5b49; 25 | border: 1px solid #de5b49; 26 | border-radius: 2px; 27 | cursor: pointer; 28 | margin: 2px; 29 | font-size: 15px; 30 | padding: 0; 31 | height: 25px; 32 | width: 25px; 33 | } 34 | 35 | .simple-data-table button.remove-row:hover { 36 | background: #de5b49; 37 | color: #fff; 38 | } 39 | 40 | .simple-data-table button.remove-row:focus { 41 | background: #de5b49; 42 | color: #fff; 43 | outline: none; 44 | } 45 | 46 | .simple-data-table button.remove-row::-moz-focus-inner { 47 | border: 0; 48 | } 49 | 50 | .simple-data-table button.add-row { 51 | background: transparent; 52 | color: #324d5c; 53 | border: 1px solid #324d5c; 54 | border-radius: 2px; 55 | cursor: pointer; 56 | margin: 2px; 57 | font-size: 15px; 58 | height: 25px; 59 | padding: 0 6px; 60 | } 61 | 62 | .simple-data-table button.add-row:hover { 63 | background: #324d5c; 64 | color: #fff; 65 | } 66 | 67 | .simple-data-table button.add-row:focus { 68 | background: #324d5c; 69 | color: #fff; 70 | outline: none; 71 | } 72 | 73 | .simple-data-table button.add-row::-moz-focus-inner { 74 | border: 0; 75 | } 76 | -------------------------------------------------------------------------------- /test/dummies/2-rows.js: -------------------------------------------------------------------------------- 1 | const DUMMY_2_ROWS = [ 2 | { 3 | column1: 'Cell 1', 4 | column2: 'Cell 2', 5 | column3: 'Cell 3' 6 | }, 7 | { 8 | column1: 'Cell 4', 9 | column2: 'Cell 5', 10 | column3: 'Cell 6' 11 | } 12 | ]; 13 | 14 | // Exports 15 | if (typeof module === 'object' && module.exports) { 16 | module.exports = { DUMMY_2_ROWS }; 17 | } else if (typeof define === 'function' && define.amd) { 18 | define(() => ({ DUMMY_2_ROWS })); 19 | } else { 20 | window.DUMMY_2_ROWS = DUMMY_2_ROWS; 21 | } 22 | -------------------------------------------------------------------------------- /test/dummies/3-rows.js: -------------------------------------------------------------------------------- 1 | const DUMMY_3_ROWS = [ 2 | { 3 | value: 34, 4 | backgroundColor: '#3498db', 5 | label: 'Angular' 6 | }, 7 | 8 | { 9 | value: 5, 10 | backgroundColor: '#2980b9', 11 | label: 'Angular v1.x' 12 | }, 13 | 14 | { 15 | value: 13, 16 | backgroundColor: '#c0392b', 17 | label: 'Nie' 18 | } 19 | ]; 20 | 21 | // Exports 22 | if (typeof module === 'object' && module.exports) { 23 | module.exports = { DUMMY_3_ROWS }; 24 | } else if (typeof define === 'function' && define.amd) { 25 | define(() => ({ DUMMY_3_ROWS })); 26 | } else { 27 | window.DUMMY_3_ROWS = DUMMY_3_ROWS; 28 | } 29 | -------------------------------------------------------------------------------- /test/specs/test.index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | 5 | const jsdom = require('jsdom'); 6 | const window = global.window = new jsdom.JSDOM().window; 7 | const document = global.document = window.document; 8 | 9 | const { SimpleDataTable } = require('../../src/index'); 10 | const { DUMMY_3_ROWS } = require('../dummies/3-rows'); 11 | 12 | let $target; 13 | 14 | test.beforeEach(() => { 15 | $target = document.createElement('div'); 16 | }); 17 | 18 | test('should be a class', (assert) => { 19 | assert.regex(String(SimpleDataTable), /^class/); 20 | }); 21 | 22 | test('clear passed container', (assert) => { 23 | $target.innerHTML = '

aaa

'; 24 | assert.is($target.children.length, 1); 25 | assert.not($target.querySelector('p'), null); 26 | 27 | const t = new SimpleDataTable($target); 28 | t.render(); 29 | assert.is($target.children.length, 1); 30 | assert.is($target.querySelector('p'), null); 31 | }); 32 | 33 | test('not passed $target', (assert) => { 34 | const err = assert.throws(() => { 35 | const t = new SimpleDataTable(); 36 | t.render(); 37 | }); 38 | assert.is(err.name, 'Error'); 39 | }); 40 | 41 | test('lazy load data', (assert) => { 42 | const t = new SimpleDataTable($target); 43 | assert.is(t.data.length, 0); 44 | 45 | t.load(DUMMY_3_ROWS); 46 | assert.is(t.data.length, 3); 47 | assert.not(t.data, DUMMY_3_ROWS); 48 | }); 49 | 50 | test('render loaded data into DOM', (assert) => { 51 | const t = new SimpleDataTable($target); 52 | t.load(DUMMY_3_ROWS); 53 | t.render(); 54 | 55 | assert.is($target.querySelectorAll('tr').length, DUMMY_3_ROWS.length); 56 | }); 57 | 58 | test('add new record after clicking a button', (assert) => { 59 | const t = new SimpleDataTable($target); 60 | t.render(); 61 | 62 | assert.is($target.querySelectorAll('tr').length, 0); 63 | 64 | const $addButton = $target.querySelector('button.add-row'); 65 | assert.true($addButton instanceof window.HTMLElement); 66 | 67 | t.on(SimpleDataTable.EVENTS.ROW_ADDED, () => { 68 | assert.is($target.querySelectorAll('tr').length, 1); 69 | }); 70 | $addButton.dispatchEvent(new window.Event('click')); 71 | }); 72 | 73 | test('trigger custom event after changed data', (assert) => { 74 | const t = new SimpleDataTable($target); 75 | t.load([ 76 | { 77 | foo: 'bar' 78 | } 79 | ]); 80 | t.render(); 81 | assert.deepEqual(t.data[0].foo, 'bar'); 82 | 83 | t.on(SimpleDataTable.EVENTS.UPDATE, (data) => { 84 | assert.deepEqual(t.data, data); 85 | }); 86 | 87 | $target.querySelector('input').value = 'xxx'; 88 | $target.querySelector('input') 89 | .dispatchEvent(new window.Event('change')); 90 | assert.deepEqual(t.data[0].foo, 'xxx'); 91 | }); 92 | 93 | test('support fluent API', (assert) => { 94 | let updates = 0; 95 | 96 | new SimpleDataTable($target) 97 | .render() 98 | .setHeaders([]) 99 | .load([]) 100 | .on(SimpleDataTable.EVENTS.UPDATE, () => updates++) 101 | .on(SimpleDataTable.EVENTS.UPDATE, () => updates++) 102 | .emit(SimpleDataTable.EVENTS.UPDATE) 103 | .render(); 104 | 105 | assert.is(updates, 2); 106 | }); 107 | 108 | test('add button text should be configurable', (assert) => { 109 | const label = 'Załaduj'; 110 | const t = new SimpleDataTable($target, { 111 | addButtonLabel: label 112 | }); 113 | t.render(); 114 | const $addButton = $target.querySelector('button.add-row'); 115 | assert.is($addButton.textContent, label); 116 | }); 117 | 118 | test('removing row should be possible', (assert) => { 119 | const t = new SimpleDataTable($target); 120 | t.load([{ foo: 'bar' }]); 121 | t.render(); 122 | 123 | const $removeButton = t.$el.querySelector('button.remove-row'); 124 | assert.true($removeButton instanceof window.HTMLElement); 125 | assert.not($removeButton.textContent, ''); 126 | }); 127 | 128 | test('API: function to get rows count', (assert) => { 129 | const t = new SimpleDataTable($target); 130 | t.load([{ foo: 'bar' }]); 131 | t.render(); 132 | 133 | assert.is(t.getRowsCount(), 1); 134 | t.load([{ foo: 'bar' }, { baz: 'boo' }]); 135 | assert.is(t.getRowsCount(), 1); 136 | t.render(); 137 | assert.is(t.getRowsCount(), 2); 138 | }); 139 | 140 | test('remove row after click button', (assert) => { 141 | const t = new SimpleDataTable($target); 142 | t.load([{ foo: 'bar' }]); 143 | t.render(); 144 | 145 | assert.is(t.getRowsCount(), 1); 146 | assert.is(t.data.length, 1); 147 | 148 | const $removeButton = t.$el.querySelector('button.remove-row'); 149 | $removeButton.dispatchEvent(new window.Event('click')); 150 | 151 | assert.is(t.getRowsCount(), 0); 152 | assert.is(t.data.length, 0); 153 | }); 154 | 155 | test('just added row should have remove button', (assert) => { 156 | const t = new SimpleDataTable($target); 157 | t.load([{ foo: 'bar' }]); 158 | t.render(); 159 | 160 | const $addButton = t.$el.querySelector('button.add-row'); 161 | $addButton.dispatchEvent(new window.Event('click')); 162 | 163 | const buttonsNumber = t.$el.querySelectorAll('button.remove-row').length; 164 | assert.is(buttonsNumber, 2); 165 | }); 166 | 167 | test('remove row action should trigger custom event', (assert) => { 168 | assert.plan(1); 169 | 170 | const t = new SimpleDataTable($target); 171 | t.load([{ foo: 'bar' }]); 172 | t.render(); 173 | 174 | const $removeButton = $target.querySelector('button.remove-row'); 175 | 176 | t.on(SimpleDataTable.EVENTS.ROW_REMOVED, (data) => { 177 | assert.deepEqual(t.data, data); 178 | }); 179 | 180 | $removeButton.dispatchEvent(new window.Event('click')); 181 | }); 182 | 183 | test('default number of columns should be configurable', (assert) => { 184 | const t = new SimpleDataTable($target, { 185 | defaultColumnNumber: 5 186 | }); 187 | t.render(); 188 | 189 | const $addButton = $target.querySelector('button.add-row'); 190 | $addButton.dispatchEvent(new window.Event('click')); 191 | 192 | const $firstRow = t.$el.querySelector('tr'); 193 | const $cells = $firstRow.querySelectorAll('td'); 194 | const $cellsWithInput = [...$cells] 195 | .map($cell => $cell.querySelector('input')) 196 | .filter($element => $element); 197 | 198 | assert.is($cellsWithInput.length, 5); 199 | }); 200 | 201 | test("in readonly mode there is no buttons", (assert) => { 202 | const t = new SimpleDataTable($target, { 203 | readonly: true, 204 | }); 205 | t.render(); 206 | 207 | const $addButton = $target.querySelector("button.add-row"); 208 | const $removeButton = $target.querySelector("button.remove-row"); 209 | 210 | assert.is($addButton, null); 211 | assert.is($removeButton, null); 212 | }); 213 | 214 | test("in readonly mode inputs are disabled", (assert) => { 215 | const t = new SimpleDataTable($target, { 216 | readonly: true, 217 | }); 218 | t.load([{ foo: 'bar' }]); 219 | t.render(); 220 | 221 | const $firstInput = $target.querySelector("input"); 222 | assert.is($firstInput.disabled, true); 223 | }); 224 | 225 | test('API: find cells', (assert) => { 226 | const t = new SimpleDataTable($target, { 227 | defaultColumnNumber: 5 228 | }); 229 | t.load([{ foo: 'bar' }, { foo: 'bar2' }]); 230 | t.render(); 231 | 232 | const indexes1 = t.findCellsByContent('bar2'); 233 | assert.is(indexes1.length, 1); 234 | 235 | const indexes2 = t.findCellsByContent('bar'); 236 | assert.is(indexes2.length, 1); 237 | 238 | const indexes3 = t.findCellsByContent('not exist'); 239 | assert.is(indexes3.length, 0); 240 | 241 | t.load([{ foo: 'bar' }, { foo: 'bar' }]); 242 | t.render(); 243 | 244 | const indexes4 = t.findCellsByContent('bar'); 245 | assert.is(indexes4.length, 2); 246 | }); 247 | 248 | test('API: get DOM reference of cell', (assert) => { 249 | const t = new SimpleDataTable($target, { 250 | defaultColumnNumber: 5 251 | }); 252 | t.load([{ foo: 'bar' }, { foo: 'bar2' }]); 253 | t.render(); 254 | 255 | const $cell = t.getCell(1, 0); 256 | assert.is($cell.firstElementChild.value, 'bar2'); 257 | 258 | const $notExistedCellInRow = t.getCell(0, 99); 259 | assert.is($notExistedCellInRow, null); 260 | 261 | const $notExistedCellAtAll = t.getCell(99, 99); 262 | assert.is($notExistedCellAtAll, null); 263 | }); 264 | 265 | test('API: highlight cell by add special CSS class', (assert) => { 266 | const t = new SimpleDataTable($target, { 267 | defaultColumnNumber: 5, 268 | defaultHighlightedCellClass: 'cookie' 269 | }); 270 | t.load([{ foo: 'bar' }]); 271 | t.render(); 272 | 273 | t.highlightCell(0, 0); 274 | const $cell = t.getCell(0, 0); 275 | assert.true($cell.classList.contains('cookie')); 276 | }); 277 | 278 | test('API: clear highlighted cells', (assert) => { 279 | const t = new SimpleDataTable($target, { 280 | defaultColumnNumber: 5, 281 | defaultHighlightedCellClass: 'cookie' 282 | }); 283 | t.load([{ foo: 'bar' }]); 284 | t.render(); 285 | 286 | t.highlightCell(0, 0); 287 | const $cell = t.getCell(0, 0); 288 | assert.true($cell.classList.contains('cookie')); 289 | 290 | t.clearHighlightedCells(); 291 | assert.false($cell.classList.contains('cookie')); 292 | }); 293 | 294 | test('API: set content into cell', (assert) => { 295 | const t = new SimpleDataTable($target); 296 | t.load([{ foo: 'bar' }]); 297 | t.render(); 298 | 299 | const $cell = t.getCell(0, 0); 300 | assert.is($cell.firstElementChild.value, 'bar'); 301 | 302 | t.setInputCellContent(0, 0, 'baz'); 303 | assert.is($cell.firstElementChild.value, 'baz'); 304 | }); 305 | 306 | test('API: function to set headers', (assert) => { 307 | const t = new SimpleDataTable($target); 308 | t.setHeaders(['Id', 'Value']); 309 | t.load([{ 310 | id: 'ghi', 311 | val: 100, 312 | }, { 313 | id: 'xyz', 314 | val: 1000 315 | }, { 316 | id: 'abc', 317 | val: 10 318 | }]); 319 | t.render(); 320 | 321 | const $header = t.$el.querySelector('thead'); 322 | assert.not($header, null); 323 | assert.is($header.querySelector('th:nth-child(1)').textContent.trim(), 'Id'); 324 | assert.is($header.querySelector('th:nth-child(2)').textContent.trim(), 'Value'); 325 | }); 326 | 327 | test('click on headers will sort the table', (assert) => { 328 | const t = new SimpleDataTable($target); 329 | t.setHeaders(['Id', 'Value']); 330 | t.load([ 331 | { id: 'b', val: 1, }, 332 | { id: 'c', val: 3, }, 333 | { id: 'a', val: 2, }, 334 | ]); 335 | t.render(); 336 | 337 | const $header = t.$el.querySelector('thead'); 338 | const $firstHeaderCell = $header.querySelector('th:nth-child(1)'); 339 | $firstHeaderCell.dispatchEvent(new window.Event('click')); 340 | 341 | assert.deepEqual(t.data, [ 342 | { id: 'c', val: 3, }, 343 | { id: 'b', val: 1, }, 344 | { id: 'a', val: 2, }, 345 | ]); 346 | 347 | const $secondHeaderCell = $header.querySelector('th:nth-child(2)'); 348 | $secondHeaderCell.dispatchEvent(new window.Event('click')); 349 | 350 | assert.deepEqual(t.data, [ 351 | { id: 'c', val: 3, }, 352 | { id: 'a', val: 2, }, 353 | { id: 'b', val: 1, }, 354 | ]); 355 | }); 356 | 357 | test('when defaultColumnNumber is not defined use headers number', (assert) => { 358 | const t = new SimpleDataTable($target); 359 | t.setHeaders(['a', 'b', 'c', 'd']); 360 | t.render(); 361 | 362 | const $addButton = $target.querySelector('button.add-row'); 363 | $addButton.dispatchEvent(new window.Event('click')); 364 | 365 | assert.is(t.$el.querySelectorAll('tbody td input[name^="column"]').length, 4); 366 | }); 367 | 368 | test('when defaultColumnNumber & headers number are not defined use first row of data', (assert) => { 369 | const t = new SimpleDataTable($target); 370 | t.load([ 371 | { name: 'John', surname: 'Brown', age: 16, car: 'Ferrari', hight: 186 }, 372 | ]); 373 | t.render(); 374 | 375 | const $addButton = $target.querySelector('button.add-row'); 376 | $addButton.dispatchEvent(new window.Event('click')); 377 | 378 | assert.is(t.$el.querySelectorAll('tbody tr:nth-child(2) td input').length, 5); 379 | }); 380 | --------------------------------------------------------------------------------